web-dev-qa-db-ja.com

子の親への参照を初期化する最良の方法は何ですか?

私は多くの異なる親/子クラスを持つオブジェクトモデルを開発しています。各子オブジェクトには、その親オブジェクトへの参照があります。親参照を初期化する方法はいくつか考えられます(試してみました)が、それぞれのアプローチに重大な欠点があることに気づきました。以下で説明するアプローチがどれであるかを考えると、最も優れているものは何ですか。

以下のコードがコンパイルされることを確認するつもりはないので、コードが構文的に正しくない場合、私の意図を確認してください。

常に表示するわけではありませんが、一部の子クラスコンストラクターは(親以外の)パラメーターを取ります。

  1. 呼び出し元は、親を設定し、同じ親に追加する責任があります。

    _class Child {
      public Child(Parent parent) {Parent=parent;}
      public Parent Parent {get; private set;}
    }
    class Parent {
      // singleton child
      public Child Child {get; set;}
      //children
      private List<Child> _children = new List<Child>();
      public List<Child> Children { get {return _children;} }
    }
    _

    Downside:親を設定することは、コンシューマーにとって2段階のプロセスです。

    _var child = new Child(parent);
    parent.Children.Add(child);
    _

    欠点:エラーが発生しやすい。呼び出し元は、子を初期化するために使用されたものとは異なる親に子を追加できます。

    _var child = new Child(parent1);
    parent2.Children.Add(child);
    _
  2. 親は、呼び出し元が初期化された親に子を追加することを確認します。

    _class Child {
      public Child(Parent parent) {Parent = parent;}
      public Parent Parent {get; private set;}
    }
    class Parent {
      // singleton child
      private Child _child;
      public Child Child {
        get {return _child;}
        set {
          if (value.Parent != this) throw new Exception();
          _child=value;
        }
      }
      //children
      private List<Child> _children = new List<Child>();
      public ReadOnlyCollection<Child> Children { get {return _children;} }
      public void AddChild(Child child) {
        if (child.Parent != this) throw new Exception();
        _children.Add(child);
      }
    }
    _

    Downside:呼び出し側には、親を設定するための2つのステップのプロセスがあります。

    Downside:ランタイムチェック–パフォーマンスを低下させ、すべてのアド/セッターにコードを追加します。

  3. 子が親に追加/割り当てられると、親は子の親参照を(それ自体に)設定します。親セッターは内部です。

    _class Child {
      public Parent Parent {get; internal set;}
    }
    class Parent {
      // singleton child
      private Child _child;
      public Child Child {
        get {return _child;}
        set {
          value.Parent = this;
          _child = value;
        }
      }
      //children
      private List<Child> _children = new List<Child>();
      public ReadOnlyCollection<Child> Children { get {return _children;} }
      public void AddChild(Child child) {
        child.Parent = this;
        _children.Add(child);
      }
    }
    _

    Downside:子は親参照なしで作成されます。場合によっては、初期化/検証で親が必要になることがあります。つまり、一部の初期化/検証は子の親セッターで実行する必要があります。コードは複雑になる可能性があります。常に親参照がある場合は、子を実装する方がはるかに簡単です。

  4. 親はファクトリのaddメソッドを公開するため、子は常に親参照を持ちます。子役は内部にいます。親セッターはプライベートです。

    _class Child {
      internal Child(Parent parent, init-params) {Parent = parent;}
      public Parent Parent {get; private set;}
    }
    class Parent {
      // singleton child
      public Child Child {get; private set;}
      public void CreateChild(init-params) {
          var child = new Child(this, init-params);
          Child = value;
      }
      //children
      private List<Child> _children = new List<Child>();
      public ReadOnlyCollection<Child> Children { get {return _children;} }
      public Child AddChild(init-params) {
        var child = new Child(this, init-params);
        _children.Add(child);
        return child;
      }
    }
    _

    Downside:new Child(){prop = value}などの初期化構文は使用できません。代わりに行う必要があります:

    _var c = parent.AddChild(); 
    c.prop = value;
    _

    Downside: add-factoryメソッドで子コンストラクターのパラメーターを複製する必要があります。

    欠点:シングルトンの子にはプロパティセッターを使用できません。値を設定するためのメソッドが必要ですが、プロパティゲッターを介して読み取りアクセスを提供するのは不十分なようです。偏っている。

  5. 子は、コンストラクターで参照される親に自分自身を追加します。子役は公開されています。親からのパブリック追加アクセスはありません。

    _//singleton
    class Child{
      public Child(ParentWithChild parent) {
        Parent = parent;
        Parent.Child = this;
      }
      public ParentWithChild Parent {get; private set;}
    }
    class ParentWithChild {
      public Child Child {get; internal set;}
    }
    
    //children
    class Child {
      public Child(ParentWithChildren parent) {
        Parent = parent;
        Parent._children.Add(this);
      }
      public ParentWithChildren Parent {get; private set;}
    }
    class ParentWithChildren {
      internal List<Child> _children = new List<Child>();
      public ReadOnlyCollection<Child> Children { get {return _children;} }
    }
    _

    Downside:呼び出し構文は良くありません。通常、次のようにオブジェクトを作成するだけでなく、親でaddメソッドを呼び出します。

    _var parent = new ParentWithChildren();
    new Child(parent); //adds child to parent
    new Child(parent);
    new Child(parent);
    _

    次のようにオブジェクトを作成するだけでなく、プロパティを設定します。

    _var parent = new ParentWithChild();
    new Child(parent); // sets parent.Child
    _

...

SEがいくつかの主観的な質問を許可していないことを知りました。明らかにこれは主観的な質問です。しかし、多分それは良い主観的な質問です。

36
Steven Broshar

私は子供が親について知ることを必然的に必要とするどんなシナリオからも離れます。

イベントを通じて子から親にメッセージを渡す方法があります。このように、親は、追加時に、子がトリガーするイベントに登録するだけでよく、子は親を直接知る必要がありません。結局のところ、これは、親をある程度効果的に使用できるようにするために、親を知っている子供の意図された使用法である可能性があります。子供が親の仕事を実行することを望まない場合を除いて、reallyしたいことは、単に何かが起こったことを親に伝えることです。したがって、処理する必要があるのは、親が利用できる子のイベントです。

このパターンは、このイベントが他のクラスで役立つ場合にも非常によくスケーリングします。少々やり過ぎかもしれませんが、2つのクラスをさらに結合するだけの子クラスで親を使用したくなるので、後で足で自分を撃つこともできなくなります。このようなクラスを後でリファクタリングするには時間がかかり、プログラムにバグを簡単に作成する可能性があります。

お役に立てば幸いです。

19
Neil

オプション3が最もクリーンだと思います。あなたが書いた。

欠点:親参照なしで子が作成されます。

それはマイナス面だとは思わない。実際、プログラムの設計は、最初に親なしで作成できる子オブジェクトから利益を得ることができます。たとえば、孤立した状態で子をテストすることが非常に簡単になります。モデルのユーザーが子を親に追加するのを忘れて、子クラスで初期化された親プロパティを予期するメソッドを呼び出すと、null ref例外が発生します-これはまさにあなたが望んでいることです。使用法。

また、技術的な理由により、子がすべての状況でコンストラクターでparent属性を初期化する必要があると考える場合は、「null親オブジェクト」などをデフォルト値として使用します(ただし、これにはエラーがマスキングされるリスクがあります)。

10
Doc Brown

「子ファクトリ」オブジェクトを使用して子を作成し、それをアタッチしてビューを返す親メソッドに渡される「子ファクトリ」オブジェクトを用意することをお勧めします。子オブジェクト自体が親の外部に公開されることはありません。このアプローチは、シミュレーションなどに適しています。電子シミュレーションでは、1つの特定の「子ファクトリ」オブジェクトが、ある種のトランジスタの仕様を表す場合があります。別のものは抵抗器の仕様を表すかもしれません。 2つのトランジスタと4つの抵抗を必要とする回路は、次のようなコードで作成できます。

var q2N3904 = new TransistorSpec(TransistorType.NPN, 0.691, 40);
var idealResistor4K7 = new IdealResistorSpec(4700.0);
var idealResistor47K = new IdealResistorSpec(47000.0);

var Q1 = Circuit.AddComponent(q2N3904);
var Q2 = Circuit.AddComponent(q2N3904);
var R1 = Circuit.AddComponent(idealResistor4K7);
var R2 = Circuit.AddComponent(idealResistor4K7);
var R3 = Circuit.AddComponent(idealResistor47K);
var R4 = Circuit.AddComponent(idealResistor47K);

シミュレーターは子作成者オブジェクトへの参照を保持する必要がないことに注意してください。AddComponentは、シミュレーターによって作成および保持されているオブジェクトへの参照ではなく、ビューを表すオブジェクトを返します。 AddComponentメソッドがジェネリックの場合、ビューオブジェクトにはコンポーネント固有の関数を含めることができますが、親がアタッチメントを管理するために使用するメンバーをnotで公開します。

3
supercat

一般的に一緒に使用される2つのクラス間の高い凝集性を妨げるものはありません(たとえば、OrderとLineItemは一般に互いに参照します)。ただし、これらのケースでは、ドメインドリブンデザインルールを順守し、親を集約ルートとして Aggregate としてモデル化する傾向があります。これは、ARがその集約内のすべてのオブジェクトの存続期間に責任があることを示しています。

したがって、親が子を作成するメソッドを公開し、必要なパラメーターを受け入れて子を適切に初期化し、コレクションに追加するシナリオ4に最も似ています。

3
Michael Brown

素晴らしいリスト。どの方法が「最良」かはわかりませんが、ここでは最も表現力豊かな方法を見つける方法を紹介します。

最も単純な親クラスと子クラスから始めます。それらであなたのコードを書いてください。名前を付けることができるコードの重複に気付いたら、それをメソッドに入れます。

多分あなたはaddChild()を取得します。多分あなたはaddChildren(List<Child>)またはaddChildrenNamed(List<String>)またはloadChildrenFrom(String)またはnewTwins(String, String)またはChild.replicate(int)のようなものを取得します。

問題が本当に1対多の関係を強制することに関するものである場合は、

  • 混乱やスロー句につながる可能性があるセッターにそれを強制する
  • セッターを削除して、特別なコピーまたは移動メソッドを作成する-これは表現力があり理解しやすい

これは答えではありませんが、これを読んでいる間にあなたが見つけてくれることを願っています。

2
User

子供から親へのリンクがあることには、前述のように欠点があることを感謝しています。

ただし、多くのシナリオでは、イベントやその他の「切断された」メカニズムによる回避策も、独自の複雑さと追加のコード行をもたらします。

たとえば、親から受け取られるように子からイベントを発生させると、疎結合の方法ではありますが、両方が結合されます。

おそらく多くのシナリオで、Child.Parentプロパティの意味はすべての開発者にとって明らかです。私がこれに取り組んだシステムの大部分では、問題なく動作しました。過剰なエンジニアリングは時間がかかる可能性があります。紛らわしい!

Childをその親にバインドするために必要なすべての作業を実行するParent.AttachChild()メソッドがあります。誰もがこれが「意味すること」について明確である

0
Rax