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.
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);
Ü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.
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.
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.
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
add
Methode 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.
quelle
Antworten:
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!
quelle
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).Ich denke, Ihre Option 3 könnte die sauberste sein. Sie schrieben.
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).
quelle
SetParent
Methode 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.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.
quelle
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:
Beachten Sie, dass der Simulator keinen Verweis auf das untergeordnete Erstellerobjekt beibehalten muss und keinen Verweis auf das
AddComponent
vom Simulator erstellte und verwaltete Objekt zurückgibt, sondern auf ein Objekt, das eine Ansicht darstellt. Wenn dieAddComponent
Methode generisch ist, kann das Ansichtsobjekt komponentenspezifische Funktionen enthalten, macht jedoch nicht die Elemente verfügbar, mit denen das übergeordnete Element die Anlage verwaltet.quelle
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 wieaddChildren(List<Child>)
oderaddChildrenNamed(List<String>)
oderloadChildrenFrom(String)
odernewTwins(String, String)
oderChild.replicate(int)
.Wenn es bei Ihrem Problem wirklich darum geht, eine Eins-zu-Viele-Beziehung zu erzwingen, sollten Sie es vielleicht tun
Dies ist keine Antwort, aber ich hoffe, Sie finden eine, während Sie dies lesen.
quelle
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"
quelle