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


35

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

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

常に表示するわけではありませんが、一部の子クラスコンストラクターは(親以外の)パラメーターを取ることに注意してください。

  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;} }
    }

    欠点:親の設定は、消費者にとって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);
      }
    }

    欠点:発信者には、親を設定するための2段階のプロセスがまだあります。

    欠点:実行時チェック–パフォーマンスが低下し、すべての追加/設定者にコードが追加されます。

  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);
      }
    }

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

  4. 親はファクトリのaddメソッドを公開し、子が常に親参照を持つようにします。子ctorは内部です。親セッターはプライベートです。

    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;
      }
    }

    欠点:などの初期化構文を使用できませんnew Child(){prop = value}。代わりに:

    var c = parent.AddChild(); 
    c.prop = value;

    欠点: 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;} }
    }

    欠点:構文を呼び出すのは良くありません。通常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は主観的な質問を許可していないことを知りましたが、これは明らかに主観的な質問です。しかし、多分それは良い主観的な質問です。


14
ベストプラクティスは、子供が親について知らないことです。
テラスティン14年


2
@Telastyn私はそれを頬の舌として読むしかありませんが、それは陽気です。また、完全に死んだ血の正確。スティーブン、調べるべき用語は「非周期的」です。可能な限りグラフを非周期にする理由については多くの文献があります。
ジミーホファ14年

10
@Telastyn parenting.stackexchangeでそのコメントを使用しようとする必要があります
ファビオマルコリーニ14年

2
うーん 投稿の移動方法がわからない(フラグコントロールが表示されない)。誰かがそれがそこに属していると言ったので、私はプログラマーに再投稿しました。
スティーブンブロシャー14年

回答:


19

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

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

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

お役に立てば幸いです!


6
子供が親について知ることを必然的に必要とするシナリオに近づかない」- なぜですか?あなたの答えは、円形オブジェクトグラフは悪い考えであるという仮定にかかっています。これは時々当てはまりますが(C#の場合ではなく、単純なref-countingによるメモリ管理の場合など)、一般的には悪いことではありません。特に、Observerパターン(イベントのディスパッチによく使用されます)には、Child一連のオブザーバー(Parent)を維持するobservable()が含まれます。
アモン14年

1
循環依存関係とは、一方が他方なしでは存在できないような方法でコードを構造化することを意味するためです。親子関係の性質上、それら別々のエンティティである必要があります。そうでない場合は、2つの密結合クラスが存在する危険があります。1つのクラスが他のいくつかのクラスへの参照を持っているという事実を除いて、Observerパターンが親子と同じである方法を確認できません。私にとって、親子は、子に強い依存関係を持っているが、その逆ではない親です。
ニール14年

同意する。この場合、イベントは親子関係を処理する最良の方法です。これは私が非常に頻繁に使用するパターンであり、子クラスが参照を介して親に何をしているのかを心配する代わりに、コードの保守が非常に簡単になります。
Eternal21

@Neil:オブジェクトインスタンスの相互依存関係は、多くのデータモデルの自然な部分を形成します。車の機械シミュレーションでは、車の異なる部分が互いに力を伝達する必要があります。これは通常、すべてのコンポーネント間に周期的な依存関係を持たせるよりも、車のすべての部分でシミュレーションエンジン自体を「親」オブジェクトと見なすことで処理しやすくなりますが、コンポーネントが親の外部からの刺激に応答できる場合そのような刺激が親が知る必要がある効果がある場合、親に通知する方法が必要です。
supercat

2
@Neil:ドメインに既約の循環データ依存性を持つフォレストオブジェクトが含まれる場合、ドメインのモデルも同様に含まれます。多くの場合、これは、森が望んでいるかどうかにかかわらず、森が単一の巨大なオブジェクトとして振る舞うことをさらに暗示するでしょう。集約パターンは、フォレストの複雑さを集約ルートと呼ばれる単一のクラスオブジェクトに集中させるのに役立ちます。ドメインの複雑さに応じては...それは良いでしょう複雑さが避けられない場合(一部のドメインの場合のように)集約ルートが多少大きくて扱いにくい得るが、こと、モデル化される
supercat

10

あなたのオプション3が一番きれいだと思います。あなたが書いた。

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

私はそれを欠点とは思わない。実際、プログラムの設計は、最初に親なしで作成できる子オブジェクトの恩恵を受けることができます。たとえば、孤立した子のテストをはるかに簡単にすることができます。モデルのユーザーが子を親に追加するのを忘れて、子クラスで初期化された親プロパティを期待するメソッドを呼び出すと、null ref例外を受け取ります-これはまさにあなたが望むものです:間違ったための早期クラッシュ使用法。

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


子オブジェクトが親なしでは何もできない場合、親なしで子オブジェクトを開始SetParentするには、既存の子の親子関係の変更をサポートする必要があるメソッドが必要です/または無意味)または1回だけ呼び出し可能です。そのような状況を集合体としてモデリングすることは(マイクブラウンが示唆するように)、子供を親なしで開始させるよりもはるかに優れている可能性があります。
supercat 14

ユースケースが1回でも何かを要求する場合、デザインは常にそれを許可する必要があります。次に、その機能を特別な状況に制限するのは簡単です。ただし、そのような機能を後で追加することは通常不可能です。最適なソリューションはオプション3です。おそらく、[Parent] ---> [Relationship(Parent owns Child)] <--- [Child]のように、親と子の間に3番目の「Relationship」オブジェクトを配置するのがよいでしょう。これにより、[子] ---> [関係(子は親が所有))<--- [親]など、複数の[関係]インスタンスも許可されます。
DocSalvager

3

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

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


1
集約のわずかに緩い定義を使用します。これにより、ルート以外の集約の部分への外部参照の存在が可能になります。ただし、外部の観測者の観点からは、動作は一貫しています。集約のすべての部分はルートへの参照のみを保持し、他の部分への参照は保持しません。私の考えでは、重要な原則は、各可変オブジェクトには1人の所有者がいるということです。集合体は、単一のオブジェクト(「集合体ルート」)がすべて所有するオブジェクトのコレクションであり、その部分に存在するすべての参照を知っている必要があります。
supercat 14

3

「子ファクトリ」オブジェクトを使用して、子を作成し(「子ファクトリ」オブジェクトを使用して)、それをアタッチしてビューを返す親メソッドに渡すことをお勧めします。子オブジェクト自体が親の外部に公開されることはありません。このアプローチは、シミュレーションなどの場合にうまく機能します。電子シミュレーションでは、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この方法が一般的であり、ビューオブジェクトは、コンポーネント固有の機能を含めることができるだろうがない親が添付ファイルを管理するために使用するメンバーを公開します。


2

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

最も単純な可能な親および子クラスから始めます。それらを使用してコードを記述します。名前を付けられるコードの重複に気付いたら、それをメソッドに入れます。

たぶんあなたは得るaddChild()。たぶん、あなたは、addChildren(List<Child>)or addChildrenNamed(List<String>)or loadChildrenFrom(String)or newTwins(String, String)orのようなものを得ますChild.replicate(int)

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

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

これは答えではありませんが、これを読んでいるうちに見つけていただければ幸いです。


0

子から親へのリンクがあると、上記のようにマイナス面があることを感謝しています。

ただし、多くのシナリオでは、イベントやその他の「切断された」メカニズムによる回避策によって、独自の複雑さと余分なコード行が発生します。

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

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.