web-dev-qa-db-ja.com

プロパティをサブクラスに追加し、スーパークラスからキャストせずにそれらにアクセスするにはどうすればよいですか?

C#で多次元ポイントクラスをモデル化しようとしています。ポイントは8種類くらいありますが、今後さらに増えるかもしれません。現在、私はすべての一般的な動作とデータを保持するスーパークラス(PointBase)を持っています。このデータには、double、string、enum、DateTimesなどの単純なプロパティのみが含まれます(これらはディメンションです)。サブクラス(たとえば、1Point2Pointなど)はすべて、少なくとも次のものをオーバーライドします。これらのディメンションの2つを計算するための動作を持つメソッド。この動作が唯一の変化するものである場合、私はStrategy + Factoryパターンを使用してそれで済ますことができます(正直なところ、おそらくこれら2つの単純な一般的な動作を後でリファクタリングします)。

残念ながら、いくつかのポイントは、タイプに基づいて、またはスーパークラスの他のプロパティの値に基づいて、1つまたは2つのプロパティを追加するので行き詰まっています。たとえば、ポイントタイプ1〜7の場合、追加のBreakevenプロパティは必要ありませんが、タイプ8の場合は必要です(これは問題#1です)。

さらに(これは問題#2です)、Productプロパティ(これは存在し、スーパークラスで設定され、列挙型です)に応じて、Term enumプロパティを追加する必要がある場合がありますそれはすべてのサブクラスポイントに適用されます。ただし、私のProduct列挙値の1つだけに用語が必要です。他の人はしません。

追加のパブリックプロパティを非表示にするList<PointBase>データ構造にこれらのポイントのリストを格納する必要があるため、この状況は複雑になります。各PointBase1Pointまたは2Pointまたはそれを取り出したときにキャストする必要があり、これは(もちろん)いくつかの設計原則(分離、少なくとも、私は思う)。

残念ながら、これらの異なるポイントを同じWPF DataGridに表示しようとしています(ユーザーが1Pointまたは2Pointなどを選択して、ポイントのパブリックプロパティをDataGrid)。これまでのところ、このコードはすべてモデル内(別のDLL)に含まれていますが、DataGridのビューでこのポリモーフィズムをどのように表すかについてもわかりません。 DataGridがポイントのパブリックプロパティに基づいて列を自動生成するだけで十分ですが、これらのポイントをList<PointBase>で渡すと、追加する必要のある追加のプロパティを取得する方法がなくなります。 。代わりに、追加のプロパティを含める必要があるたびに文字列キーと文字列値を追加する、CustomAttributesと呼ばれるスーパークラスに仮想ディクショナリを追加することを考えました。ただし、DataGridのディクショナリをフラット化するのは非常に難しいため、これは非常に煩わしいものです。これが唯一の方法だと心配しています...

私が知っていること(ソフトウェアエンジニアリングには制限がある)から、私はこのすべてのポリモーピズムをキャプチャするための抽象的なファクトリのようなものが必要です。しかし、正確にはわかりません。要約すると、私は必要です:

  1. タイプに基づいて特定のサブクラスにパブリックプロパティを追加する機能。たとえば、8Pointサブクラスの場合、Breakevenプロパティを追加する必要がありますが、このプロパティは他のサブクラスでは意味がありません。
  2. スーパークラスのプロパティの値に基づいて、すべてのサブクラスにパブリックプロパティを追加する機能。たとえば、Product(スーパークラスで設定)がAの場合、Termという名前のすべてのサブクラスに追加のプロパティを追加する必要があります。このプロパティは、他の製品には適用されないか、意味がありません。

私はここで継承と結婚していません(実際には、可能な場合は委任+インターフェイスなど、よりエレガントで簡単な方法を実行したいと思っています)。前もって感謝します!

編集:もう少しコンテキストを与えるために、ここに私のセットアップが今どのように見えるかがあります:

public abstract class StatisticPointBase 
{
    public DateTime EventTime;
    public Product Product; // an enum
    protected double _delta;
    public string Name;
    public double FooSensitivity;
    public double BarSensitivity;
    public double BazSensitivity;
    public double FlopRatio;
    public double FlapCoefficient;
    ...
    public double Value;     // set by overridden CalculateValue
    public double Velocity;  // set by overridden CalculateVelocity        

    public PointBase(IDataRepository d, string name, double delta, Product p) 
    { 
        Name = name;   // just the name, like "Alpha" or "Beta"
        _delta = delta; // follows name, e.g. for Alpha it's always 0.63
        Product = p;
        FooSensitivity = d.GetFooSensitivity(p);
        ... // other setup
    }

    public abstract double CalculateValue();
    public abstract double CalculateVelocity();
}

public class AlphaStatisticPoint 
{
    public SimpleStatisticPoint()
    {
        Value = CalculateValue();
        Velocity = CalculateVelocity();
    }

    override double CalculateValue()
    {
        return (fooSensitivity + barSensitivity + bazSensitivity) * Velocity / 2;
    }

    override double CalculateVelocity()
    {
        return FlopRatio * (_delta / FlapCoefficient);
    }
}

public class BetaStatisticPoint 
{
    public SimpleStatisticPoint()
    {
        Value = CalculateValue();
        Velocity = CalculateVelocity();
    }

    override double CalculateVelocity()
    {
        return (FlopRatio * 100) * (_delta);
    }

    override double CalculateValue()
    {
        return fooSensitivity * Velocity;
    }
}

// other points...

public class ZetaStatisticPoint 
{
    public double Breakeven;

    public SimpleStatisticPoint()
    {
        Value = CalculateValue();
        Velocity = CalculateVelocity();
        Breakeven = CalculateBreakeven();
    }

    override double CalculateVelocity()
    {
        return (FlopRatio * 100) * (_delta);
    }

    override double CalculateValue()
    {
        return fooSensitivity * Velocity;
    }

    override double CalculateBreakeven()
    {
        return 2 * Math.sqrt(fooSensitivity / barSensitivity);
        // This is not defined for other types of statistics, and is only relevant for Zeta-type statistics. 
        // For other statistics, we can't even calculate the breakeven; there's no formula for it.
    }
}

public enum Product 
{ 
    Widget, 
    Watchet, 
    /* Woffet will be added in future, and needs a Term */
}

NameDeltaのようなものは、各サブクラスに1つあり、各値は各サブクラスに一意であるため、サブクラスで設定する必要があります。たとえば、Alphaのデルタ値は常に0.63、Zetaは常に0.21などです。ただし、継承を回避しようとしていますが、実際の問題は、BreakevenがZetaにのみ存在することです(これは問題#1です)。 )。

さらに(これが問題#2です)、Termプロパティを作成して設定する必要があるかもしれませんが、Product値がWoffetのポイントに対してのみです。他の製品には意味がありません。このプロパティTermはすべてのポイントに追加されるため、継承を行う場合は、スーパークラスに配置します。 Productをクラスに展開してから、Termプロパティを指定するだけでよいと考え始めています。しかし、新しいProductクラスに対して同じ問題が発生します。Termがゼータ統計にのみ適用されるのと同じように、BreakevenはWoffet製品にのみ適用されます。

この振る舞いに遭遇すると、通常は継承が答えではない(または私が集めたのか?)場合がよくあります。それで、私はこれをモデル化するより良い方法を探しています。 Zetaを除くすべてのサブクラスを削除し、スーパークラスにコンストラクターでValueCalculationStrategyおよびVelocityCalculationStrategyを与えることを考えていました(これらはValueおよびVelocityを計算します)。しかし、この場合、私はまだゼータをStatisticPointBaseと呼んでおり、クライアントはそれがZetaStatisticPointであることを知りません。だから私はまだ行き詰っています。多分問題は私が統計とポイントを概念的に混ぜていることですか?別のZetaStatisticクラスとZetaBreakevenクラスを作成し、これらを使用してZetaPointクラスを作成することもできますが、これが正しいことかどうかはわかりません。

うまくいけば、私はこれを適切に説明しました。ご不明な点がございましたら、コメント欄でお寄せください。詳しくご説明いたします。

5
TheIntern

説明された方法で戦略オブジェクトを使用することは、PointBaseクラスを単純化するための良い出発点です。BreakEvenプロパティの問題を解決しなくても、遠慮なく紹介してください。

カスタムプロパティを許可するために、PointBaseクラスに何らかの「拡張メカニズム」を提供することができます(再設計後、名前をPointに変更します)。最も単純な形式では、これは各Pointオブジェクトのカスタムプロパティのリストとそれらの値のリストです(デフォルトではリストは空です)。これはある種の EAVモデル に要約されるかもしれません。 EAVはアンチパターンと見なされることもありますが、この特定のケースでは、「特別なカスタムプロパティ」に対してのみ、大部分のプロパティに使用しない限り、その使用法は問題ありません。

カスタムフィールドに関する「プログラマー」に関する以前の質問 も参照してください。私の回答で、私は言及しました Martin Fowlerの本「分析パターン」 -(カスタム)プロパティと同様のもの。

1
Doc Brown

私は投稿しました ここでの質問 同じ問題によって引き起こされました:両方のサブクラスが追加のプロパティを使用すると同時に、基本クラスでオブジェクトを参照する便利さを両方利用しようとしています含む。

私にとって最も便利な解決策は、 訪問者パターン を使用することでした。 Point基本クラスで、抽象メソッドvoid Accept(Visitor)を定義します。 Visitorクラスは各サブクラスのメソッドを定義します(そのため、すべてを追跡する必要があるため、新しいサブクラスではビジタークラスを変更する必要があります)。たとえば、訪問者は次のようになります。

interface Visitor {

    void on1Point(1Point);

    void on2Point(2Point);

    ... etc
}

次に、各BasePointサブクラスはacceptメソッドを実装して、正しいVisitorメソッドに委任します。したがって、1Pointクラス、acceptは次のように定義されます。

void accept(Visitor visitor) {
    visitor.on1Point(this);
}

BasePointがあり、それが持つ追加のプロパティに応じて何かを実行したい場合は常に、ビジターを作成し、可能な各サブクラスの場合に何をしたいかを理解し、そこにロジック。 Visitorメソッドの本体では、渡されるオブジェクトは正しい型に「キャスト」されるため、追加のプロパティにアクセスできます。一連のinstanceofチェックを実行するのと似ていますが、よりクリーンで、再利用性が高く、エレガントです。

5
codebreaker

私はあなたの要件を完全には理解していませんが、私には、C#の型システムが実際に役立つよりも邪魔になるところに到達したように思えます。

私は単にクラスと継承を使用するという考えを捨て、keyがプロパティの名前またはタイプであり、valueobjectを含む一般的なKey-Valueストアを作成するだけですプロパティの値。次に、実行時にどのタイプのポイントにどのプロパティが含まれるかについての不変条件が正​​しいことを確認するコードを作成します。 C#dynamicタイプを実際に使用することもできます。これは基本的に同じことですが、構文が優れています。

0
Euphoric