web-dev-qa-db-ja.com

C#でのジェネリック/ダイナミックカスタムプロパティシステムの実装

このサイトにふさわしいと思う建築設計上の問題があります。

私が作成したことに注意してください[〜#〜]編集[〜#〜]この問題への私の最新の潜在的な解決策を反映する以下のこの投稿へ。

一般的な問題の説明:

私の主な目標は、非常に特殊なタイプの顕微鏡実験を自動化するソフトウェアライブラリ/プログラム(C#)を設計することです。私たちの実験的なセットアップは、一般に限られた数のカテゴリに分類される多くの異なるハードウェアデバイスで構成されています。

  • ポイント検出器
  • XYサンプルステージ
  • オートフォーカスデバイス
  • ...

自然な選択は、インターフェースIDeviceを設計することです。たとえば、 IDevice.Initialize()、_IDevice.Name_、...

IDeviceを継承して、抽象クラスを設計することもできます。 DeviceBaseは、すべてのデバイスに共通の機能を実装しています。

同様に、共通のものを実装するたびに、特定のデバイス用のインターフェースを設計することもできます(IXYSampleStageは、たとえばIXYSampleStage.Move(posX, posY)または_IXYSampleStage.AxisXStepSize_を保持する場合があります)。

最終的に、特定のデバイスを表す最終クラスでこれらすべてのインターフェースを実装できます。そこで何をすべきか知っていると思います。

ただし、クラスのすべてのデバイスが完全に同じであるとは限りません。一部のメーカーは、標準のものに加えて豊富な機能を提供しています。 XYステージには、たとえば設定するオプション/非標準のパラメーター(例:チャネル遅延、PID制御値など。

これに取り組むために、

特定のデバイスクラスに追加のプロパティ(メソッドではなく、プロパティで十分です)を追加するメカニズムが必要です。デバイスと対話する高レベルのコードは、これらの追加されたプロパティを理解し、それらを取得/設定できる必要がありますが、多くの場合があります。

基本的な設計問題についてはこれで終わりです。

既存のもの

OSソフトウェアプログラム Micro Manager があり、C++でこのようなシステムが実装されています(完全なソース here で、いいえ、このSWを直接使用することはできません)目的):

_class PropertyBase
{
public:
   virtual ~PropertyBase() {}

   // property type
   virtual PropertyType GetType() = 0;

   // setting and getting values
   virtual bool Set(double dVal) = 0;
   virtual bool Set(long lVal) = 0;
   virtual bool Set(const char* Val) = 0;

   virtual bool Get(double& dVal) const = 0;
   virtual bool Get(long& lVal) const = 0;
   virtual bool Get(std::string& strVal) const = 0;

   // Limits
   virtual bool HasLimits() const = 0;
   virtual double GetLowerLimit() const = 0;
   virtual double GetUpperLimit() const = 0;
   virtual bool SetLimits(double lowerLimit, double upperLimit) = 0;

   // Some more stuff left out for brevity
   ...
};
_

その後、Propertyクラスは、制限に関連するいくつかの純粋仮想関数を実装します。

次に、StringPropertyIntegerPropertyFloatPropertyはGet()/ Set()メソッドを実装します。これらの種類のプロパティは、これらの3種類のプロパティのみを許可する列挙型で定義されます。

次に、基本的にデバイスクラスの一部である一種の辞書であるPropertyCollectionにプロパティを追加できます。

これはすべてうまく動作します。ラボでは他の種類の実験のためにMMをよく使用しますが、C#ソリューションで同様のことを行おうとすると、いくつかの基本的な質問に出くわしました。これらのほとんどは、おそらく既存のC++実装を改善するために、C#/。NETによって提供される特定の機能(汎用ですが、ダイナミクスも...)を活用する方法が明確でないことに関係しています。

質問:

最初の質問

許可されるプロパティのタイプを制限するために、MMは列挙型を定義します。

_    enum PropertyType {
      Undef,
      String,
      Float,
      Integer
    };
_

一方、C#にはリフレクションと型/ジェネリックの多くの組み込みサポートがあります。したがって私はこれを検討しました:

_public interface IProperty<T>
{
   public T value { get; set; }

   ...
}
_

次に、抽象的な_PropertyBase<T>_を実行し、最終的に

_IntegerProperty : PropertyBase<int>
StringProperty : PropertyBase<string>
FloatProperty : PropertyBase<double>
_

しかし、.NETでは以下が 不可能 であるため、これらをコレクションに入れようとすると問題が発生します。

_PropertyCollection : Dictionary<string, IProperty<T>>
_

おそらく、私が この戦略 を採用しない限り、つまり、タイプを返すだけでなく、実際にプロパティシステムを実際に実装するためのさらに多くのベースクラスを返す非ジェネリックベースクラスがあります。しかし、これは私には複雑なようです。 もっと良い方法はありますか

プロパティ値のバッキングタイプを知ることができるため、ジェネリック言語機能を利用することで、上記のMMの例でこれらのGet()/ Set()メソッドをすべて不要にすることができると思います。

これは非常に幅広い質問だと思いますが、一部の言語機能の基本を理解することと、最初から正しい設計決定を完全に行うことができることの間には大きなギャップがあります。

2番目の質問

私は私の最終的なデバイスクラスをすべて一種のDynamicDictionaryから継承することを考えています(例えば here からのものに類似しています):

_    public class DynamicDictionary : DynamicObject
    {
        internal readonly Dictionary<string, object> SourceItems;

        public DynamicDictionary(Dictionary<string, object> sourceItems)
        {
            SourceItems = sourceItems;
        }
        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            if (SourceItems.ContainsKey(binder.Name))
            {
                SourceItems[binder.Name.ToLower()] = value;
            }
            else
            {
                SourceItems.Add(binder.Name.ToLower(), value);
            }
            return true;
        }
        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            if (SourceItems != null)
            {
                if (SourceItems.TryGetValue(binder.Name.ToLower(), out result))
                {
                    return true;
                }
            }
            result = null;
            return true;
        }
    }
}
_

しかし、_<string, object>_の代わりに、_<string, MyPropertyBase>_を使用します。ここで、文字列はプロパティのエイリアスになります(これは、Q1のデザインの選択に対応します)。

そのようなことはデザインの観点から理にかなっていますか?

実際には、設計時に(つまり、カスタムデバイスドライバーを作成するときに)カスタムプロパティをデバイスに追加するだけで済みます。実行時には、カスタムプロパティを検査するか、それらの値を取得または設定するだけで済みます。

c ++ MMは、デバイスコンストラクターで一連のCreateProperty("alias", Property)メソッドを使用してこれを解決します。これも機能するため、おそらくここでダイナミクスを使用するのはやりすぎです。しかし、繰り返しになりますが、可能性のある利点については誰かが洞察を提供できるのではないかと思いますが、動的ルートをたどることの欠点も確かにあります。

要約するには

上記の特定の問題に焦点を当てて、いくつかのMMの概念をよりエレガントな方法で実装するために活用できる.NET機能(4.5)はありますか?

繰り返しますが、これは非常に幅広い質問であり、おそらく回答のいくつかは他の何よりも好み/スタイルの問題であることに気づきますが、そうであれば(これにより、このプラットフォームには質問が不適切になります)、フィードバック/コメントを提供してください。それに応じて調整できます。

[〜#〜]編集[〜#〜]

私はこれについてさらに考えて、次の方法で解決しようとしました:

まず、許可されたpropertytypesを指定する列挙型:

_public enum PropertyType
{
    Undefined,
    String,
    Float,
    Integer
}
_

次に、特定のタイプのプロパティに渡された値をチェックするIPropertyTypeインターフェース:

_public interface IPropertyType
{   
    Type BackingType { get; }

    PropertyType TypeAlias { get; }

    bool IsValueTypeValid(object value);
}
_

次に、IPropertyTypeを実装する基本クラス:

_public bool IsValueTypeValid(object value)
    {
        if (value == null)
        {
            return true;
        }

        Type type = value.GetType();

        if (type == this.BackingType)
        {
            return true;
        }

        return false;
    }

...

public Type BackingType
    {
        get
        {
            switch (this.TypeAlias)
            {
                case PropertyType.Integer:
                    return typeof(long);
                case PropertyType.Float:
                    return typeof(double);
                case PropertyType.String:
                    return typeof(string);
                default:
                    return null;
            }
        }
    }
_

次に、IPropertyTypeStringPropertyTypeIntegerPropertyTypeの3つのFloatPropertyTypeがあります。ここで、プロパティ_PropertyTypeAlias is set to one of the enum values._

これで、IPropertyTypeをIPropertyタイプに追加できます。

_public interface IProperty
{
    string Alias { get; }

    /// If we want to limit the property to discrete values.
    Dictionary<string, object> AllowedValues { get; }

    bool HasLimits { get; }

    bool HasValue { get; }

    bool IsReadOnly { get; }

    /// Callback for HW operations using the property value.
    Func<PropertyFuncType, bool> PropertyFunction { set; }

    PropertyType TypeAlias { get; }

    object Value { get; set; }

    void AddAllowedValue(string alias, object value);

    void ClearAllowedValues();

    // On numeric properties, it might be usefull to limit the range of values.
    void SetLimits(object lowerLimit, object upperLimit);

    // These will do stuff on the HW...
    bool TryApply();

    bool TryUpdate();
}
_

ここで、値セッターは、IPropertyTypeで定義された入力値に対して検証を実行します。

IPropertyは、PropertyBaseにほぼ完全に実装されており、インスタンスの作成時に設定されるIPropertyTypeのフィールドもあります(その後変更することはできません。プロパティでタイプを変更することはできません)。

基本クラスから:

_    public object Value
    {
        get
        {
            return this.storedvalue;
        }

        set
        {
            if (this.propertyType.IsValueTypeValid(value) && this.IsValueAllowed(value))
            {
                this.storedvalue = value;
            }
        }
    }
_

これは、特定のIPropertyクラス、つまりStringPropertyIntegerProperty、およびFloatPropertyのみを残します。ここで、オーバーライドはSetLimits()のみであり、値の比較に依存しているためです。その他はすべて基本クラスに含めることができます。

_public override void SetLimits(object lowerlimit, object upperlimit)
    {
        // Is false by default
        this.HasLimits = false;

        // Makes no sense to impose limits on a property with discrete values.
        if (this.AllowedValues.Count != 0)
        {
            // If the passed objects are in fact doubles, we can proceed to check them.
            if (this.IsValueTypeValid(lowerlimit) && this.IsValueTypeValid(upperlimit))
            {
                // In order to allow comparison we need to cast objects to double.
                double lowerdouble = (double)lowerlimit;
                double upperdouble = (double)upperlimit;

                // Lower limit cannot be bigger than upper limit.
                if (lowerdouble < upperdouble)
                {
                    // Passed values are OK, so set them.
                    this.LowerLimit = lowerdouble;
                    this.UpperLimit = upperdouble;

                    this.HasLimits = true;
                }
            }
        }
    }
_

このように、IPropertyのタイプごとに実際に値の制限などを実装する機能を持つ、さまざまなタイプ(int、float、またはstring)のデータを実際に保持できるIPropertyのコレクションを許可するシステムがあると思いますデータタイプに応じて異なるもの)...

しかし、私はまだ、この種のことを行うにはもっと良い方法があるか、私が物事をエンジニアリングしているという印象を持っています。

したがって、これに関するフィードバックは引き続き歓迎されます。

編集2

オンラインでさらに検索したところ、前回の編集でここに投稿されたコードの最後の部分は Adaptive Object Model "pattern"(もしそうであれば)に傾いていると思います。

ただし、このトピックに関するほとんどのリソースは非常に古く、これらの種類のことを達成するためのより良い方法があるかどうか疑問に思っています。

6
Kris

既存の型システムの上に型システムを作成しています。コンパイル済みコードで参照できない厳密に型指定されたプロパティには、ほとんどメリットがありません。私はこのアプローチが以前に使用されたのを見てきましたが、それはやっかいなものになってしまいます。

ExtendedPropertiesなどの単一のプロパティでKey-Valueストアを使用するだけです。

つまり、Dictionary<string, object>を回避しようとしていますが、その取り組みでは.Net型システムを再発明しています。

タイプDictionary<string, object>ExtendedPropertiesと呼ばれるプロパティをアタッチし、それで完了です。緩やかに型付けされたデータの制限は、下に行く道よりもコストがかかりません。

9
Chris McCall

プロパティがGUIでのみ使用され、HWの読み取り/設定として使用される場合は、ジェネリックまたはリフレクションの必要はありません。単純なOOPクラスを使用します。焦点を当てるべきは、プロパティの内部がどのようになるかではなく、それらがどのように使用されるかです。これらのプロパティをGUIから編集できるようにするにはそれらをHWデバイスに保存または取得しますか?

public interface PropertyBase
{
    string Name {get;} // name of this property

    void SetToDevice(); // sets the value to device
    void ReadFromDevice(); // read from device

    GUIEditor GetEditor(); // returns editor for this property
}

public class PropertyContainer // device can implement or contain this
{
    public List<PropertyBase> Properties;
}

public interface IntProperty : PropertyBase
{
    public IDevice Device {get;set;}
    public string Name {get;set;} // name of this property

    public int Value {get;set;}

    void SetToDevice()
    {
        Device.SetInt(Name, Value); 
    }
    void ReadFromDevice()
    {
        Value = Device.ReadInt(Name);
    }

    GUIEditor GetEditor()
    {
        return new IntEditor(this); // IntEditor can get/set value itself
    }
}

GUIEditorは、GUIが特定のプロパティを編集する方法を知る方法です。依存関係の問題がある場合、Visitorパターンを使用してこのロジックをViewに移動できます。このアプローチの主な利点は、既存のコードを変更することなく、新しいデバイス固有のプロパティを自由に追加できることです。

5
Euphoric

ExpandoObjectを使用してプロパティを含めることができます。これには、コンパイル時に任意のプロパティ名と型を使用できるという利点があるため、混乱を避けることができる限り、必要に応じてそれらを追加して読み取ることができます。欠点は、すべてのプロパティにインターフェイスを定義した場合と同じ静的型チェックが行われないことです。

以下は、FooおよびBarがデバイスの動的なデバイス固有のプロパティである例です。

まず、基本プロパティを持つIDeviceインターフェイスを定義します。

public interface IDevice
{
    string Name { get; }
}

そして、プロパティを含むためにdynamicを公開するインターフェース:

public interface IPropertyContainer 
{
    dynamic Properties { get; }
}

次に、それらを基本クラスで結合します。

public class DeviceBase : IDevice, IPropertyContainer
{
    public string Name { get; set; }

    protected dynamic _properties = new ExpandoObject();

    public dynamic Properties
    {
        get
        {
            return _properties;
        }
    }
}

特定のデバイスクラスを定義するときは、ExpandoObjectに動的プロパティを設定します。

public class MyDeviceA : DeviceBase
{
    public MyDeviceA()
    {
        _properties.Foo = "Foo";
    }
}

public class MyDeviceB : DeviceBase
{
    public MyDeviceB()
    {
        _properties.Bar = "Bar";
    }
}

BarFooが有効なプロパティであることをどこにも宣言していないことに注意してください。それは使用法から推測されます...これは便利であり、少し危険でもあります。

テストプログラム:

public class Program
{
    public static void Main()
    {
        DeviceBase deviceA = new MyDeviceA();
        Console.WriteLine(deviceA.Properties.Foo);

        DeviceBase deviceB = new MyDeviceB();
        Console.WriteLine(deviceB.Properties.Bar);
    }
}

実際の出力:

Foo
Bar

これが DotNetFiddleの使用例 へのリンクです。

0
John Wu

同じような問題がありました。プラグインの形でウィジェットをドラッグアンドドロップできるワークスペースがある「シンプルな」プラグインシステムがあります。プラグインにアタッチされた一連のプロパティを追加するために、プラグインの作成者に柔軟性(まだすべての機能に取り組んでいます)を与えたいと思っていました。 MVPアプローチを使用しているので、プロパティにアクセスするためのインターフェイスを作成しました

public interface IPluginControlSettingsPropertyAccessors<T>
{
    bool Validator(T _Source);

    bool Setter(T _Source);

    bool Getter(out T _Result);
}

各プロパティについて、プラグインは異なるタイプのいくつかのアクセサーを公開し、少なくとも

IPluginControlSettingsPropertyAccessors<string> 

プロパティごとに。

次に、特定のタイプのプロパティを処理する方法を実際に実装するかどうかはビューに依存します。あなたが持つことができます:

if (l_Property is IPluginControlSettingsPropertyAccessors<Enum>)
{
     View.AddEnumHandler(l_Property);
}
else if (l_Property is IPluginControlSettingsPropertyAccessors<String>)
{
    View.AddStringHandler(l_Property);
}

これを見るとわかるように、オブジェクトとそのプロパティは、GUIでの表示から切り離されています。このアプローチを使用すると、単純なGUIは、文字列形式のすべてのプロパティを視覚化のために単に処理し、文字列の処理をプロパティセッターおよびバリデーターに任せることができます。オブジェクトに公開するプロパティがある場合

IPluginControlSettingsPropertyAccessors<String>
IPluginControlSettingsPropertyAccessors<Color>

gUIは文字列タイプのプロパティのみをサポートするため、(文字列バリデーターに応じて)#FFFFFF形式の色のみを受け入れるテキストボックスがあります。より複雑なGUIは、代わりにIPluginControlSettingsPropertyAccessorsをサポートし、カラーピックを提供する場合があります。

私はC#も非常に新しく、MVPパターンに関してこれより優れたソリューションを思いつくことはできませんでしたが、これまでのところ、保守が非常に簡単であることが証明されています。

0
Claudio_G

WPFの依存関係プロパティと添付プロパティのように、あなたが望むものはほとんど聞こえます。基本クラスとしてのSystem.Windows.DependencyObjectの設計と、プロパティクラスとしてのSystem.Windows.DependencyPropertyの設計を調べることをお勧めします。多少のオーバーヘッドが伴いますが、非常に柔軟性があります。

0