Wie kann der Verweis eines Kindes auf seinen Elternteil am besten initialisiert werden?

35

Ich entwickle ein Objektmodell mit vielen verschiedenen Eltern / Kind-Klassen. Jedes untergeordnete Objekt hat einen Verweis auf sein übergeordnetes Objekt. Ich kann mir mehrere Möglichkeiten überlegen (und habe sie ausprobiert), um die übergeordnete Referenz zu initialisieren, finde aber bei jedem Ansatz erhebliche Nachteile. In Anbetracht der unten beschriebenen Ansätze, welche am besten ... oder was noch besser ist.

Ich werde nicht sicherstellen, dass der folgende Code kompiliert wird. Bitte versuchen Sie, meine Absicht zu erkennen, wenn der Code syntaktisch nicht korrekt ist.

Beachten Sie, dass einige meiner untergeordneten Klassenkonstruktoren (andere als übergeordnete) Parameter verwenden, obwohl ich nicht immer welche zeige.

  1. Der Anrufer ist dafür verantwortlich, das übergeordnete Element festzulegen und es demselben übergeordneten Element hinzuzufügen.

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

    Nachteil: Das Festlegen des übergeordneten Elements ist für den Verbraucher ein zweistufiger Prozess.

    var child = new Child(parent);
    parent.Children.Add(child);
    

    Nachteil: fehleranfällig. Der Anrufer kann einem anderen übergeordneten Element ein untergeordnetes Element hinzufügen, das nicht zum Initialisieren des untergeordneten Elements verwendet wurde.

    var child = new Child(parent1);
    parent2.Children.Add(child);
    
  2. Übergeordnet überprüft, ob der Anrufer dem übergeordneten Element, für das die Initialisierung durchgeführt wurde, ein untergeordnetes Element hinzufügt.

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

    Nachteil: Der Anrufer hat immer noch einen zweistufigen Prozess zum Einstellen des übergeordneten Elements.

    Nachteil: Laufzeitprüfung - reduziert die Leistung und fügt jedem Add / Setter Code hinzu.

  3. Das Elternteil legt die Elternreferenz des Kindes (auf sich selbst) fest, wenn das Kind einem Elternteil hinzugefügt / zugewiesen wird. Eltern-Setter ist intern.

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

    Nachteil: Das untergeordnete Element wird ohne einen übergeordneten Verweis erstellt. Manchmal erfordert die Initialisierung / Validierung das übergeordnete Element, was bedeutet, dass einige Initialisierungen / Validierungen im übergeordneten Element des untergeordneten Elements vorgenommen werden müssen. Der Code kann kompliziert werden. Es wäre so viel einfacher, das Kind zu implementieren, wenn es immer einen Elternverweis hätte.

  4. Parent macht Factory-Add-Methoden verfügbar, sodass ein Kind immer einen Elternverweis hat. Child ctor ist intern. Eltern-Setter ist privat.

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

    Nachteil: Die Initialisierungssyntax kann nicht verwendet werden, z new Child(){prop = value}. Stattdessen müssen Sie Folgendes tun:

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

    Nachteil: Sie müssen die Parameter des untergeordneten Konstruktors in den Add-Factory-Methoden duplizieren.

    Nachteil: Es kann kein Eigenschaftssetter für ein Singleton-Kind verwendet werden. Es scheint lahm, dass ich eine Methode zum Festlegen des Werts benötige, aber Lesezugriff über einen Eigenschafts-Getter bereitstelle. Es ist schief.

  5. Child fügt sich dem übergeordneten Element hinzu, auf das in seinem Konstruktor verwiesen wird. Kinder ctor ist öffentlich. Kein öffentlicher Zugriff durch übergeordnetes Element.

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

    Nachteil: Aufrufsyntax ist nicht so toll. Normalerweise ruft man eine addMethode auf dem Parent auf, anstatt nur ein Objekt wie dieses zu erstellen:

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

    Und legt eine Eigenschaft fest, anstatt nur ein Objekt wie das folgende zu erstellen:

    var parent = new ParentWithChild();
    new Child(parent); // sets parent.Child
    

...

Ich habe gerade erfahren, dass SE einige subjektive Fragen nicht zulässt und dies eindeutig eine subjektive Frage ist. Aber vielleicht ist es eine gute subjektive Frage.

Steven Broshar
quelle
14
Best Practice ist, dass Kinder nichts über ihre Eltern wissen sollten.
Telastyn
2
@Telastyn Ich kann nicht anders, als das als ein Augenzwinkern zu lesen, und es ist komisch. Auch absolut akkurat. Steven, der Begriff, den man untersuchen sollte, ist "azyklisch", da es eine Menge Literatur gibt, warum man Graphen möglichst azyklisch machen sollte.
Jimmy Hoffa
10
@ Telastyn Sie sollten versuchen, diesen Kommentar auf parenting.stackexchange
Fabio Marcolini
2
Hmm. Ich weiß nicht, wie ich einen Beitrag verschieben soll (sehe kein Flag-Steuerelement). Ich habe an Programmierer weitergeleitet, da mir jemand sagte, dass es dort hingehört.
Steven Broshar

Antworten:

19

Ich würde mich von jedem Szenario fernhalten, in dem das Kind unbedingt über die Eltern Bescheid wissen muss.

Es gibt verschiedene Möglichkeiten, Nachrichten über Ereignisse von einem Kind an ein Elternteil weiterzuleiten. Auf diese Weise muss sich der Elternteil beim Hinzufügen lediglich bei einem Ereignis registrieren, das das Kind auslöst, ohne dass das Kind direkt über den Elternteil Bescheid wissen muss. Schließlich ist dies wahrscheinlich die beabsichtigte Verwendung eines Kindes, das etwas über sein Elternteil weiß, um das Elternteil in gewisser Weise verwenden zu können. Außer Sie würde das Kind nicht wollen , um den Job des Mutterdurchgeführt wird , so wirklich , was Sie einfach tun möchte , ist das übergeordnete sagen , dass etwas passiert ist. Daher müssen Sie ein Ereignis für das Kind behandeln, von dem die Eltern profitieren können.

Dieses Muster lässt sich auch sehr gut skalieren, wenn dieses Ereignis für andere Klassen nützlich wird. Vielleicht ist es ein bisschen übertrieben, aber es hindert Sie auch daran, sich später in den Fuß zu schießen, da es verlockend wird, Eltern in Ihrer Kinderklasse zu verwenden, die nur die beiden Klassen noch mehr verbindet. Das spätere Refactoring solcher Klassen ist zeitaufwändig und kann leicht zu Fehlern in Ihrem Programm führen.

Hoffentlich hilft das!

Neil
quelle
6
Vermeiden Sie Szenarien, in denen das Kind unbedingt über die Eltern Bescheid wissen muss “ - warum? Ihre Antwort hängt von der Annahme ab, dass kreisförmige Objektgraphen eine schlechte Idee sind. Dies ist zwar manchmal der Fall (z. B. bei der Speicherverwaltung über naives Ref-Counting - in C # nicht der Fall), aber im Allgemeinen keine schlechte Sache. Insbesondere beinhaltet das Beobachtermuster (das häufig zum Versenden von Ereignissen verwendet wird), dass observable ( Child) eine Reihe von Beobachtern ( Parent) verwaltet, wodurch die Zirkularität rückwärts wieder eingeführt wird (und eine Reihe eigener Probleme eingeführt werden).
amon
1
Weil zyklische Abhängigkeiten bedeuten, Ihren Code so zu strukturieren, dass Sie keinen ohne den anderen haben können. Aufgrund der Art der Eltern-Kind-Beziehung sollten sie getrennte Einheiten sein, andernfalls besteht die Gefahr, dass zwei eng miteinander verbundene Klassen vorhanden sind, die ebenso gut eine einzige gigantische Klasse mit einer Liste für all die Sorgfalt sein können, die in ihre Gestaltung investiert wird. Ich verstehe nicht, wie das Observer-Muster mit dem Eltern-Kind-Muster identisch ist, abgesehen von der Tatsache, dass eine Klasse Verweise auf mehrere andere hat. Für mich ist Eltern-Kind ein Elternteil mit einer starken Abhängigkeit vom Kind, aber nicht umgekehrt.
Neil
Genau. Ereignisse sind in diesem Fall die beste Möglichkeit, die Eltern-Kind-Beziehung zu behandeln. Es ist das Muster, das ich sehr oft verwende, und es macht die Pflege des Codes sehr einfach, anstatt sich Gedanken darüber zu machen, was die untergeordnete Klasse über die Referenz mit den übergeordneten Klassen macht.
Eternal21
@Neil: Gegenseitige Abhängigkeiten von Objektinstanzen bilden einen natürlichen Bestandteil vieler Datenmodelle. In einer mechanischen Simulation eines Autos müssen verschiedene Teile des Autos Kräfte aufeinander übertragen. Dies ist in der Regel besser zu handhaben, wenn alle Teile des Fahrzeugs die Simulationsmaschine selbst als "übergeordnetes" Objekt betrachten, als wenn alle Komponenten zyklische Abhängigkeiten aufweisen, wenn die Komponenten jedoch in der Lage sind, auf Stimuli von außerhalb des übergeordneten Objekts zu reagieren Sie müssen den Eltern mitteilen können, ob solche Stimuli Auswirkungen haben, über die die Eltern Bescheid wissen müssen.
Supercat
2
@Neil: Wenn die Domäne Gesamtstrukturobjekte mit irreduziblen zyklischen Datenabhängigkeiten enthält, ist dies auch für jedes Modell der Domäne möglich. In vielen Fällen wird dies weiterhin bedeuten, dass sich der Wald wie ein einzelnes riesiges Objekt verhält, ob man es will oder nicht . Das Aggregatmuster dient dazu, die Komplexität der Gesamtstruktur auf ein einzelnes Klassenobjekt zu konzentrieren, das als Aggregatstamm bezeichnet wird. Abhängig von der Komplexität der Domain, die modelliert wird, kann diese Gesamtwurzel etwas groß und unhandlich werden, aber wenn die Komplexität unvermeidbar ist (wie es bei einigen Domains der Fall ist), ist es besser ...
supercat
10

Ich denke, Ihre Option 3 könnte die sauberste sein. Sie schrieben.

Nachteil: Das untergeordnete Element wird ohne einen übergeordneten Verweis erstellt.

Ich sehe das nicht als Nachteil. Tatsächlich kann der Entwurf Ihres Programms von untergeordneten Objekten profitieren, die zuerst ohne übergeordnetes Element erstellt werden können. Dies kann zum Beispiel das Testen von Kindern für sich viel einfacher machen. Wenn ein Benutzer Ihres Modells vergisst, seinem übergeordneten Element ein untergeordnetes Element hinzuzufügen, und eine Methode aufruft, die die in Ihrer untergeordneten Klasse initialisierte übergeordnete Eigenschaft erwartet, erhält er eine Nullreferenz-Ausnahme Verwendung.

Und wenn Sie der Meinung sind, dass ein Kind aus technischen Gründen unter allen Umständen sein übergeordnetes Attribut im Konstruktor initialisieren muss, verwenden Sie als Standardwert so etwas wie ein "null übergeordnetes Objekt" (obwohl dies das Risiko von Maskierungsfehlern birgt).

Doc Brown
quelle
Wenn ein untergeordnetes Objekt ohne ein übergeordnetes Objekt nichts Sinnvolles tun kann, muss es zum Starten eines untergeordneten Objekts ohne ein übergeordnetes Objekt über eine SetParentMethode verfügen , die entweder das Ändern der Elternschaft eines vorhandenen untergeordneten Objekts unterstützt (was schwierig sein kann und erforderlich ist) / oder unsinnig) oder kann nur einmal aufgerufen werden. Das Modellieren solcher Situationen als Aggregate (wie Mike Brown vorschlägt) kann viel besser sein, als wenn Kinder ohne Eltern beginnen.
Supercat
Wenn ein Anwendungsfall auch nur einmal etwas erfordert, muss das Design dies immer berücksichtigen. Dann ist es einfach, diese Fähigkeit auf bestimmte Umstände zu beschränken. Ein nachträgliches Hinzufügen einer solchen Fähigkeit ist jedoch normalerweise nicht möglich. Die beste Lösung ist Option 3. Wahrscheinlich mit einem dritten "Beziehungs" -Objekt zwischen dem Elternteil und dem Kind, so dass [Elternteil] ---> [Beziehung (Elternteil besitzt Kind)] <--- [Kind]. Dies ermöglicht auch mehrere [Beziehung] -Instanzen wie [Kind] ---> [Beziehung (das Kind gehört den Eltern)] <--- [Eltern].
DocSalvager
3

Es gibt nichts, was eine hohe Kohäsion zwischen zwei gemeinsam verwendeten Klassen verhindern könnte (z. B. referenzieren sich ein Auftrag und ein LineItem normalerweise gegenseitig). In diesen Fällen neige ich jedoch dazu, domänengesteuerte Entwurfsregeln einzuhalten und sie als Aggregat zu modellieren, wobei das übergeordnete Element das aggregierte Stammverzeichnis ist. Dies sagt uns, dass der AR für die Lebensdauer aller Objekte in seinem Aggregat verantwortlich ist.

Es ist also am ehesten mit Ihrem vierten Szenario vergleichbar, in dem das übergeordnete Element eine Methode zum Erstellen seiner untergeordneten Elemente bereitstellt, wobei alle erforderlichen Parameter akzeptiert werden, um die untergeordneten Elemente ordnungsgemäß zu initialisieren und der Auflistung hinzuzufügen.

Michael Brown
quelle
1
Ich würde eine etwas lockerere Definition von Aggregat verwenden, die die Existenz von externen Verweisen auf andere Teile des Aggregats als die Wurzel zulässt, vorausgesetzt, das Verhalten wäre aus Sicht eines externen Beobachters konsistent wobei jeder Teil des Aggregats nur einen Verweis auf die Wurzel und keinen anderen Teil enthält. Meines Erachtens ist das Schlüsselprinzip, dass jedes veränderbare Objekt einen Eigentümer haben sollte ; Ein Aggregat ist eine Sammlung von Objekten, die sich alle im Besitz eines einzelnen Objekts befinden (der "aggregierte Stamm"), der alle Verweise auf seine Teile kennen sollte.
Supercat
3

Ich würde vorschlagen, "child-factory" -Objekte zu haben, die an eine übergeordnete Methode übergeben werden, die ein untergeordnetes Objekt erstellt (unter Verwendung des "child factory" -Objekts), es anfügt und eine Ansicht zurückgibt. Das untergeordnete Objekt selbst wird niemals außerhalb des übergeordneten Objekts angezeigt. Dieser Ansatz kann gut für Dinge wie Simulationen funktionieren. In einer Elektroniksimulation könnte ein bestimmtes "Kinderfabrik" -Objekt die Spezifikationen für eine Art Transistor darstellen; ein anderer könnte die Spezifikationen für einen Widerstand darstellen; Eine Schaltung, die zwei Transistoren und vier Widerstände benötigt, könnte mit folgendem Code erstellt werden:

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

Beachten Sie, dass der Simulator keinen Verweis auf das untergeordnete Erstellerobjekt beibehalten muss und keinen Verweis auf das AddComponentvom Simulator erstellte und verwaltete Objekt zurückgibt, sondern auf ein Objekt, das eine Ansicht darstellt. Wenn die AddComponentMethode generisch ist, kann das Ansichtsobjekt komponentenspezifische Funktionen enthalten, macht jedoch nicht die Elemente verfügbar, mit denen das übergeordnete Element die Anlage verwaltet.

Superkatze
quelle
2

Tolles Angebot. Ich weiß nicht, welche Methode die "beste" ist, aber hier ist eine, um die aussagekräftigsten Methoden zu finden.

Beginnen Sie mit der einfachsten Eltern- und Kinderklasse. Schreiben Sie Ihren Code damit. Sobald Sie eine Codeduplizierung bemerken, die benannt werden kann, fügen Sie diese in eine Methode ein.

Vielleicht bekommst du addChild(). Vielleicht bekommst du sowas wie addChildren(List<Child>)oder addChildrenNamed(List<String>)oder loadChildrenFrom(String)oder newTwins(String, String)oder Child.replicate(int).

Wenn es bei Ihrem Problem wirklich darum geht, eine Eins-zu-Viele-Beziehung zu erzwingen, sollten Sie es vielleicht tun

  • zwinge es in die Setter, was zu Verwirrung oder Throw-Klauseln führen kann
  • Sie entfernen die Setter und erstellen spezielle Kopier- oder Verschiebemethoden, die aussagekräftig und verständlich sind

Dies ist keine Antwort, aber ich hoffe, Sie finden eine, während Sie dies lesen.

Benutzer
quelle
0

Ich weiß es zu schätzen, dass Links von Kind zu Eltern die oben genannten Nachteile haben.

Für viele Szenarien bringen die Problemumgehungen mit Ereignissen und anderen "getrennten" Mechaniken jedoch auch ihre eigene Komplexität und zusätzliche Codezeilen mit sich.

Wenn Sie zum Beispiel ein Ereignis von Child auslösen, das von seinem Elternteil empfangen werden soll, werden beide auf lose Weise miteinander verbunden.

Vielleicht ist für viele Szenarien allen Entwicklern klar, was eine Child.Parent-Eigenschaft bedeutet. Bei den meisten Systemen, an denen ich gearbeitet habe, hat das prima funktioniert. Überengineering kann zeitaufwändig sein und ... Verwirrend!

Verfügen Sie über eine Parent.AttachChild () -Methode, die die gesamte Arbeit ausführt, die erforderlich ist, um das untergeordnete Element an das übergeordnete Element zu binden. Allen ist klar, was dies "bedeutet"

Rax
quelle