web-dev-qa-db-ja.com

C#で「特性」デザインパターンをどのように実装しますか?

この機能はC#には存在しないことは知っていますが、PHP最近 Traits という機能を追加しました。 。

Clientという基本クラスがあるとします。 Clientには、Nameという単一のプロパティがあります。

現在、私は多くの異なる顧客によって使用される再利用可能なアプリケーションを開発しています。すべての顧客は、クライアントに名前を付ける必要があることに同意します。したがって、名前は基本クラスに属します。

ここで、顧客Aが来て、クライアントの体重も追跡する必要があると言います。顧客Bは体重を必要としませんが、身長を追跡したいと考えています。顧客Cは、体重と身長の両方を追跡したいと考えています。

特性を使用すると、WeightおよびHeight機能特性の両方を作成できます。

class ClientA extends Client use TClientWeight
class ClientB extends Client use TClientHeight
class ClientC extends Client use TClientWeight, TClientHeight

クラスに余計なものを追加することなく、すべての顧客のニーズを満たすことができます。顧客が後で戻ってきて「ああ、私はその機能が本当に好きです、私もそれを手に入れることができますか?」と言ったら、クラス定義を更新して追加の特性を含めます。

C#でこれをどのように実現しますか?

プロパティと関連するメソッドの具体的な定義が必要であり、クラスのバージョンごとにそれらを再実装したくないため、インターフェイスはここでは機能しません。

(「顧客」とは、私を開発者として雇った文字通りの人を意味し、「クライアント」とはプログラミングクラスを指します。各顧客には、情報を記録したいクライアントがいます)

48
mpen

マーカーインターフェイスと拡張メソッドを使用して構文を取得できます。

前提条件:インターフェースは、後で拡張メソッドで使用されるコントラクトを定義する必要があります。基本的に、インターフェイスは特性を「実装」できるようにするためのコントラクトを定義します。理想的には、インターフェースを追加するクラスには、インターフェースのすべてのメンバーがすでに存在している必要があるため、no追加の実装が必要です。

public class Client {
  public double Weight { get; }

  public double Height { get; }
}

public interface TClientWeight {
  double Weight { get; }
}

public interface TClientHeight {
  double Height { get; }
}

public class ClientA: Client, TClientWeight { }

public class ClientB: Client, TClientHeight { }

public class ClientC: Client, TClientWeight, TClientHeight { }

public static class TClientWeightMethods {
  public static bool IsHeavierThan(this TClientWeight client, double weight) {
    return client.Weight > weight;
  }
  // add more methods as you see fit
}

public static class TClientHeightMethods {
  public static bool IsTallerThan(this TClientHeight client, double height) {
    return client.Height > height;
  }
  // add more methods as you see fit
}

次のように使用します。

var ca = new ClientA();
ca.IsHeavierThan(10); // OK
ca.IsTallerThan(10); // compiler error

Edit:追加データをどのように保存できるかという質問が提起されました。これは、追加のコーディングを行うことでも対処できます。

public interface IDynamicObject {
  bool TryGetAttribute(string key, out object value);
  void SetAttribute(string key, object value);
  // void RemoveAttribute(string key)
}

public class DynamicObject: IDynamicObject {
  private readonly Dictionary<string, object> data = new Dictionary<string, object>(StringComparer.Ordinal);

  bool IDynamicObject.TryGetAttribute(string key, out object value) {
    return data.TryGet(key, out value);
  }

  void IDynamicObject.SetAttribute(string key, object value) {
    data[key] = value;
  }
}

そして、「トレイトインターフェイス」がIDynamicObjectを継承する場合、トレイトメソッドはデータを追加および取得できます。

public class Client: DynamicObject { /* implementation see above */ }

public interface TClientWeight, IDynamicObject {
  double Weight { get; }
}

public class ClientA: Client, TClientWeight { }

public static class TClientWeightMethods {
  public static bool HasWeightChanged(this TClientWeight client) {
    object oldWeight;
    bool result = client.TryGetAttribute("oldWeight", out oldWeight) && client.Weight.Equals(oldWeight);
    client.SetAttribute("oldWeight", client.Weight);
    return result;
  }
  // add more methods as you see fit
}

注: IDynamicMetaObjectProvider を実装することで、オブジェクトはDLRを介して動的データを公開することさえ可能になり、dynamicキーワードと共に使用すると、追加のプロパティへのアクセスが透過的になります。

51
Lucero

C#言語(少なくともバージョン5まで)はTraitsをサポートしていません。

ただし、Scalaには特性があり、ScalaはJVM(およびCLR)で実行されます。したがって、実行時の問題ではなく、単に言語。

少なくともScalaの意味でのTraitsは、「プロキシメソッドでコンパイルするためのかなりの魔法」と考えることができます(notはMROに影響しますが、 RubyのMixinsとは異なります。C#では、この動作を実現する方法は、インターフェイスと「手動プロキシメソッドのロット」(例:合成)を使用することです。

この退屈なプロセスは、仮想プロセッサ(おそらくテンプレートを介した部分クラスの自動コード生成?)で実行できますが、それはC#ではありません。

ハッピーコーディング。

8
user166390

ベルン大学(スイス)のSoftware Composition GroupのStefan Reichartによって開発された学術プロジェクトがあり、これはtraitsの真の実装を提供します。 C#言語。

CSharpTに関する論文(PDF) をご覧ください。彼が行ったモノの完全な説明は、モノコンパイラに基づいています。

書き込めるものの例を次に示します。

trait TCircle
{
    public int Radius { get; set; }
    public int Surface { get { ... } }
}

trait TColor { ... }

class MyCircle
{
    uses { TCircle; TColor }
}
7
Pierre Arnaud

NRoles 、C#でのrolesの実験で、rolestraitsと似ています。

NRolesは、ポストコンパイラを使用してILを書き換え、メソッドをクラスに注入します。これにより、次のようなコードを記述できます。

public class RSwitchable : Role
{
    private bool on = false;
    public void TurnOn() { on = true; }
    public void TurnOff() { on = false; }
    public bool IsOn { get { return on; } }
    public bool IsOff { get { return !on; } }
}

public class RTunable : Role
{
    public int Channel { get; private set; }
    public void Seek(int step) { Channel += step; }
}

public class Radio : Does<RSwitchable>, Does<RTunable> { }

ここで、クラスRadioRSwitchableおよびRTunableを実装します。舞台裏では、Does<R>はメンバーを持たないインターフェイスなので、基本的にRadioは空のクラスにコンパイルされます。コンパイル後のILの書き換えは、RSwitchableおよびRTunableのメソッドをRadioに注入します。これは、2つroles(別のアセンブリから):

var radio = new Radio();
radio.TurnOn();
radio.Seek(42);

書き換えが発生する前に(つまり、radio型が宣言されているアセンブリと同じアセンブリで)Radioを直接使用するには、拡張メソッドAs<R>()に頼る必要があります。

radio.As<RSwitchable>().TurnOn();
radio.As<RTunable>().Seek(42);

コンパイラは、TurnOnクラスでSeekまたはRadioを直接呼び出すことを許可しないためです。

6
Pierre Arnaud

既定のインターフェイスメソッドを使用して、C#8で特性を実装できます。 Java 8は、この理由からデフォルトのインターフェースメソッドを導入しました。

C#8を使用すると、質問で提案した内容をほぼ正確に記述できます。特性は、メソッドのデフォルトの実装を提供するIClientWeight、IClientHeightインターフェイスによって実装されます。この場合、それらは0を返します。

_public interface IClientWeight
{
    int getWeight()=>0;
}

public interface IClientHeight
{
    int getHeight()=>0;
}

public class Client
{
    public String Name {get;set;}
}
_

ClientAおよびClientBには特性がありますが、実装しません。 ClientCはIClientHeightのみを実装し、この場合は16の異なる数値を返します。

_class ClientA : Client, IClientWeight{}
class ClientB : Client, IClientHeight{}
class ClientC : Client, IClientWeight, IClientHeight
{
    public int getHeight()=>16;
}
_

インターフェイスを介してClientBgetHeight()が呼び出されると、デフォルトの実装が呼び出されます。 getHeight()は、インターフェースを介してのみ呼び出すことができます。

ClientCはIClientHeightインターフェイスを実装するため、独自のメソッドが呼び出されます。このメソッドは、クラス自体から利用できます。

_public class C {
    public void M() {        
        //Accessed through the interface
        IClientHeight clientB = new ClientB();        
        clientB.getHeight();

        //Accessed directly or through the class
        var clientC = new ClientC();        
        clientC.getHeight();
    }
}
_

このSharpLab.ioの例 は、この例から生成されたコードを示します

PHPの特性の概要 で説明されている多くの特性機能は、デフォルトのインターフェースメソッドを使用して簡単に実装できます。特性(インターフェース)は組み合わせることができます。 abstract メソッドを定義して、クラスに特定の要件を実装させることもできます。

特性に、高さまたは重さの文字列を返すsayHeight()およびsayWeight()メソッドを持たせたいとしましょう。彼らは、身長と体重を返すメソッドを実装するために、展示クラス(PHPガイドから盗まれた用語)を強制する何らかの方法が必要です:

_public interface IClientWeight
{
    abstract int getWeight();
    String sayWeight()=>getWeight().ToString();
}

public interface IClientHeight
{
    abstract int getHeight();
    String sayHeight()=>getHeight().ToString();
}

//Combines both traits
public interface IClientBoth:IClientHeight,IClientWeight{}
_

クライアントは have を使ってgetHeight()またはgetWeight()メソッドを実装しますが、sayメソッドについて何も知る必要はありません。

これにより、よりクリーンな装飾方法が提供されます

SharpLab.io link このサンプルの場合。

3

これは、すべてのストレージが基本クラスに含まれていたLuceroの答えに対する実際に推奨される拡張機能です。

これに依存関係プロパティを使用してはどうですか?

これにより、すべての子孫によって常に設定されるとは限らない多くのプロパティがある場合、実行時にクライアントクラスを軽量にする効果があります。これは、値が静的メンバーに格納されるためです。

using System.Windows;

public class Client : DependencyObject
{
    public string Name { get; set; }

    public Client(string name)
    {
        Name = name;
    }

    //add to descendant to use
    //public double Weight
    //{
    //    get { return (double)GetValue(WeightProperty); }
    //    set { SetValue(WeightProperty, value); }
    //}

    public static readonly DependencyProperty WeightProperty =
        DependencyProperty.Register("Weight", typeof(double), typeof(Client), new PropertyMetadata());


    //add to descendant to use
    //public double Height
    //{
    //    get { return (double)GetValue(HeightProperty); }
    //    set { SetValue(HeightProperty, value); }
    //}

    public static readonly DependencyProperty HeightProperty =
        DependencyProperty.Register("Height", typeof(double), typeof(Client), new PropertyMetadata());
}

public interface IWeight
{
    double Weight { get; set; }
}

public interface IHeight
{
    double Height { get; set; }
}

public class ClientA : Client, IWeight
{
    public double Weight
    {
        get { return (double)GetValue(WeightProperty); }
        set { SetValue(WeightProperty, value); }
    }

    public ClientA(string name, double weight)
        : base(name)
    {
        Weight = weight;
    }
}

public class ClientB : Client, IHeight
{
    public double Height
    {
        get { return (double)GetValue(HeightProperty); }
        set { SetValue(HeightProperty, value); }
    }

    public ClientB(string name, double height)
        : base(name)
    {
        Height = height;
    }
}

public class ClientC : Client, IHeight, IWeight
{
    public double Height
    {
        get { return (double)GetValue(HeightProperty); }
        set { SetValue(HeightProperty, value); }
    }

    public double Weight
    {
        get { return (double)GetValue(WeightProperty); }
        set { SetValue(WeightProperty, value); }
    }

    public ClientC(string name, double weight, double height)
        : base(name)
    {
        Weight = weight;
        Height = height;
    }

}

public static class ClientExt
{
    public static double HeightInches(this IHeight client)
    {
        return client.Height * 39.3700787;
    }

    public static double WeightPounds(this IWeight client)
    {
        return client.Weight * 2.20462262;
    }
}
3
weston

Luceroが提案したもの に基づいて、私はこれを思いつきました:

internal class Program
{
    private static void Main(string[] args)
    {
        var a = new ClientA("Adam", 68);
        var b = new ClientB("Bob", 1.75);
        var c = new ClientC("Cheryl", 54.4, 1.65);

        Console.WriteLine("{0} is {1:0.0} lbs.", a.Name, a.WeightPounds());
        Console.WriteLine("{0} is {1:0.0} inches tall.", b.Name, b.HeightInches());
        Console.WriteLine("{0} is {1:0.0} lbs and {2:0.0} inches.", c.Name, c.WeightPounds(), c.HeightInches());
        Console.ReadLine();
    }
}

public class Client
{
    public string Name { get; set; }

    public Client(string name)
    {
        Name = name;
    }
}

public interface IWeight
{
    double Weight { get; set; }
}

public interface IHeight
{
    double Height { get; set; }
}

public class ClientA : Client, IWeight
{
    public double Weight { get; set; }
    public ClientA(string name, double weight) : base(name)
    {
        Weight = weight;
    }
}

public class ClientB : Client, IHeight
{
    public double Height { get; set; }
    public ClientB(string name, double height) : base(name)
    {
        Height = height;
    }
}

public class ClientC : Client, IWeight, IHeight
{
    public double Weight { get; set; }
    public double Height { get; set; }
    public ClientC(string name, double weight, double height) : base(name)
    {
        Weight = weight;
        Height = height;
    }
}

public static class ClientExt
{
    public static double HeightInches(this IHeight client)
    {
        return client.Height * 39.3700787;
    }

    public static double WeightPounds(this IWeight client)
    {
        return client.Weight * 2.20462262;
    }
}

出力:

Adam is 149.9 lbs.
Bob is 68.9 inches tall.
Cheryl is 119.9 lbs and 65.0 inches.

それは私が望むほどニースではありませんが、それも悪くありません。

2
mpen

これは、アスペクト指向プログラミングのPHPバージョンのように聞こえます。場合によっては、PostSharpやMS Unityなどを支援するツールがあります。ロールインしたい場合は、C#属性を使用したコードインジェクションが1つのアプローチであるか、または限られた場合の拡張方法として推奨されています。

本当に複雑になります。複雑なものを構築しようとしている場合は、これらのツールのいくつかを参考にしてください。

0
RJ Lohan