Jaki jest najlepszy sposób na zainicjowanie odniesienia dziecka do jego rodzica?

35

Tworzę model obiektowy, który ma wiele różnych klas nadrzędnych / podrzędnych. Każdy obiekt podrzędny ma odniesienie do swojego obiektu nadrzędnego. Mogę wymyślić (i próbowałem) kilka sposobów na zainicjowanie referencji nadrzędnej, ale uważam, że każde podejście ma znaczące wady. Biorąc pod uwagę opisane poniżej podejścia, które są najlepsze ... lub jeszcze lepsze.

Nie zamierzam się upewnić, że poniższy kod się skompiluje, więc spróbuj zobaczyć mój zamiar, jeśli kod nie jest poprawny pod względem składniowym.

Zauważ, że niektóre z moich konstruktorów klas potomnych pobierają parametry (inne niż nadrzędne), chociaż nie zawsze je wyświetlam.

  1. Dzwoniący jest odpowiedzialny za ustawienie rodzica i dodanie do tego samego rodzica.

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

    Wada: ustawienie rodzica jest procesem dwuetapowym dla konsumenta.

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

    Wada: podatność na błędy. Dzwoniący może dodać dziecko do innego rodzica niż ten, którego użyto do zainicjowania dziecka.

    var child = new Child(parent1);
    parent2.Children.Add(child);
    
  2. Rodzic sprawdza, czy dzwoniący dodaje dziecko do rodzica, dla którego zostało zainicjowane.

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

    Wada: dzwoniący nadal ma dwuetapowy proces ustawiania rodzica.

    Wada: sprawdzanie w czasie wykonywania - zmniejsza wydajność i dodaje kod do każdego dodającego / ustawiającego.

  3. Rodzic ustawia odwołanie do rodzica dziecka (do siebie), gdy dziecko jest dodawane / przypisywane do rodzica. Setter nadrzędny jest wewnętrzny.

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

    Wada: dziecko jest tworzone bez odniesienia rodzica. Czasami inicjalizacja / walidacja wymaga rodzica, co oznacza, że ​​pewna inicjalizacja / walidacja musi zostać wykonana w seterze rodzica dziecka. Kod może się skomplikować. O wiele łatwiej byłoby zaimplementować dziecko, gdyby zawsze miało ono odniesienie do rodzica.

  4. Rodzic udostępnia fabryczne metody dodawania, dzięki czemu dziecko zawsze ma odniesienie rodzica. Dziecko jest wewnętrzne. Seter nadrzędny jest prywatny.

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

    Wada: nie można użyć składni inicjującej, takiej jak new Child(){prop = value}. Zamiast tego musisz zrobić:

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

    Wada: trzeba powielić parametry konstruktora potomnego w metodach add-factory.

    Wada: nie można użyć narzędzia do ustawiania właściwości dla pojedynczego dziecka. Wydaje się kiepskie, że potrzebuję metody ustawiania wartości, ale zapewniam dostęp do odczytu za pośrednictwem modułu pobierania właściwości. Jest krzywy.

  5. Dziecko dodaje się do rodzica, do którego odwołuje się jego konstruktor. Dziecko jest publiczne. Brak publicznego dodawania dostępu od rodzica.

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

    Wada: wywołanie składni nie jest świetne. Zwykle wywołuje się addmetodę nadrzędną zamiast po prostu utworzyć taki obiekt:

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

    I ustawia właściwość zamiast tworzyć taki obiekt:

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

...

Właśnie dowiedziałem się, że SE nie pozwala na pewne subiektywne pytania i wyraźnie jest to pytanie subiektywne. Ale może to dobre pytanie subiektywne.

Steven Broshar
źródło
14
Najlepszą praktyką jest to, że dzieci nie powinny wiedzieć o swoich rodzicach.
Telastyn
2
@Telastyn Nie mogę powstrzymać się przed czytaniem tego jak język w policzek, i to jest przezabawne. Również całkowicie śmiertelnie dokładny. Steven, terminem, który należy zbadać, jest „acykliczny”, ponieważ istnieje mnóstwo literatury na temat tego, dlaczego warto tworzyć wykresy acykliczne, jeśli to w ogóle możliwe.
Jimmy Hoffa
10
@Telastyn powinieneś spróbować użyć tego komentarza na parenting.stackexchange
Fabio Marcolini
2
Hmm Nie wiem, jak przenieść post (nie widzę kontrolki flagi). Przesłałem ponownie do programistów, ponieważ ktoś powiedział mi, że tam należy.
Steven Broshar

Odpowiedzi:

19

Trzymałbym się z dala od jakiegokolwiek scenariusza, który wymagałby, aby dziecko wiedziało o rodzicu.

Istnieją sposoby przekazywania wiadomości od dziecka do rodzica poprzez zdarzenia. W ten sposób rodzic, po dodaniu, musi po prostu zarejestrować się w zdarzeniu, które dziecko wyzwala, bez konieczności bezpośredniego kontaktu dziecka z rodzicem. W końcu jest to prawdopodobnie zamierzone użycie dziecka, które wie o swoim rodzicu, aby móc z niego skorzystać. Tyle że nie chcesz, aby dziecko wykonywało pracę rodzica, więc tak naprawdę to po prostu powiedz rodzicowi, że coś się stało. Dlatego musisz obsłużyć zdarzenie na dziecku, z którego rodzic może skorzystać.

Ten wzór skaluje się również bardzo dobrze, jeśli to wydarzenie stanie się przydatne dla innych klas. Być może jest to trochę przesada, ale uniemożliwia później strzelanie sobie w stopę, ponieważ kusi chęć skorzystania z rodzica w klasie dziecka, która jeszcze bardziej łączy dwie klasy. Refaktoryzacja takich klas jest później czasochłonna i może łatwo powodować błędy w twoim programie.

Mam nadzieję, że to pomaga!

Neil
źródło
6
Trzymaj się z dala od scenariusza, który koniecznie wymaga od dziecka wiedzy o rodzicu ” - dlaczego? Twoja odpowiedź opiera się na założeniu, że okrągłe wykresy obiektów są złym pomysłem. Chociaż czasami tak jest (np. W przypadku zarządzania pamięcią za pomocą naiwnego liczenia odwołań - nie w przypadku C #), ogólnie nie jest to zła rzecz. W szczególności wzorzec obserwatora (który jest często używany do wywoływania zdarzeń) obejmuje obserwowalne ( Child) utrzymanie zestawu obserwatorów ( Parent), który przywraca cykliczność do tyłu (i wprowadza wiele własnych problemów).
amon
1
Ponieważ cykliczne zależności oznaczają taką strukturę kodu, że nie można mieć jednej bez drugiej. Z natury relacji rodzic-dziecko powinny być oddzielnymi bytami, w przeciwnym razie ryzykujesz posiadaniem dwóch ściśle ze sobą powiązanych klas, które równie dobrze mogłyby być pojedynczą gigantyczną klasą z listą wszystkich starannych starań włożonych w jej projekt. Nie widzę, jak wzorzec obserwatora jest taki sam jak rodzic-dziecko, poza tym, że jedna klasa ma odniesienia do kilku innych. Dla mnie rodzic-dziecko jest rodzicem silnie uzależnionym od dziecka, ale nie odwrotnym.
Neil
Zgadzam się. W tym przypadku zdarzenia są najlepszym sposobem obsługi relacji rodzic-dziecko. Jest to wzorzec, którego używam bardzo często i sprawia, że ​​kod jest bardzo łatwy w utrzymaniu, zamiast martwić się o to, co klasa potomna robi rodzicowi poprzez referencję.
Eternal21
@Neil: Wzajemne zależności instancji obiektów stanowią naturalną część wielu modeli danych. W mechanicznej symulacji samochodu różne części samochodu będą musiały przenosić na siebie siły; zazwyczaj lepiej sobie z tym poradzić, gdy wszystkie części samochodu traktują silnik symulacji jako obiekt „macierzysty”, niż poprzez cykliczne zależności między wszystkimi komponentami, ale jeśli komponenty są w stanie reagować na bodźce spoza rodzica będą potrzebować sposobu, aby powiadomić rodzica, jeśli takie bodźce mają jakiekolwiek skutki, o których powinien wiedzieć.
supercat
2
@ Neil: Jeśli domena zawiera obiekty leśne z nieredukowalnymi cyklicznymi zależnościami danych, zrobi to również każdy model domeny. W wielu przypadkach będzie to oznaczało, że las będzie zachowywał się jak pojedynczy gigantyczny obiekt, niezależnie od tego, czy tego chce, czy nie . Wzorzec agregujący służy do skoncentrowania złożoności lasu w pojedynczym obiekcie klasy o nazwie Korzeń agregacji. W zależności od złożoności modelowanej domeny ten zagregowany katalog główny może stać się nieco duży i nieporęczny, ale jeśli złożoność jest nieunikniona (jak w przypadku niektórych domen), lepiej ...
supercat
10

Myślę, że Twoja opcja 3 może być najczystsza. Napisałeś.

Wada: dziecko jest tworzone bez odniesienia rodzica.

Nie uważam tego za wadę. W rzeczywistości projekt twojego programu może korzystać z obiektów potomnych, które można najpierw utworzyć bez elementu nadrzędnego. Na przykład może znacznie ułatwić testowanie dzieci w izolacji. Jeśli użytkownik Twojego modelu zapomni o dodaniu dziecka do jego rodzica i wywoła metody, które oczekują, że właściwość nadrzędna zostanie zainicjowana w klasie podrzędnej, wówczas otrzyma wyjątek zerowy - co jest dokładnie tym, czego chcesz: wczesną awarią niewłaściwego stosowanie.

A jeśli uważasz, że dziecko potrzebuje zainicjowania atrybutu nadrzędnego w konstruktorze we wszystkich okolicznościach ze względów technicznych, użyj czegoś takiego jak „zerowy obiekt nadrzędny” jako wartość domyślna (chociaż istnieje ryzyko maskowania błędów).

Doktor Brown
źródło
Jeśli obiekt podrzędny nie może zrobić nic użytecznego bez obiektu nadrzędnego, uruchomienie obiektu podrzędnego bez obiektu nadrzędnego będzie wymagało posiadania SetParentmetody, która albo będzie musiała obsługiwać zmianę rodzicielstwa istniejącego obiektu podrzędnego (co może być trudne i / lub bezsensowne) lub będzie można je wywołać tylko raz. Modelowanie takich sytuacji jako agregatów (jak sugeruje Mike Brown) może być znacznie lepsze niż posiadanie dzieci bez rodziców.
supercat
Jeśli przypadek użycia wymaga czegoś nawet raz, projekt musi na to zawsze pozwalać. Ograniczenie tej możliwości do szczególnych okoliczności jest zatem łatwe. Dodanie takiej możliwości później jest jednak zwykle niemożliwe. Najlepszym rozwiązaniem jest Opcja 3. Prawdopodobnie z trzecim obiektem „Relacji” między rodzicem a dzieckiem, tak że [Parent] ---> [Relacja (Parent posiada dziecko)] <--- [Child]. Pozwala to również na wiele instancji [Związek], takich jak [Dziecko] ---> [Związek (dziecko jest własnością Nadrzędny)] <--- [Nadrzędny].
DocSalvager
3

Nic nie stoi na przeszkodzie wysokiej spójności między dwiema klasami, które są powszechnie używane razem (np. Order i LineItem będą się nawzajem odwoływać). Jednak w tych przypadkach staram się przestrzegać reguł projektowania opartego na domenie i modelować je jako agregat, a rodzic jest korzeniem agregatu. To mówi nam, że AR jest odpowiedzialny za czas życia wszystkich obiektów w swojej agregacji.

Byłoby to najbardziej podobne do scenariusza czwartego, w którym rodzic udostępnia metodę tworzenia swoich elementów podrzędnych, akceptując wszelkie parametry niezbędne do prawidłowej inicjalizacji elementów podrzędnych i dodając je do kolekcji.

Michael Brown
źródło
1
Użyłbym nieco luźniejszej definicji agregatu, która pozwoliłaby na istnienie zewnętrznych odniesień do części agregatu innych niż root, pod warunkiem, że - z punktu widzenia zewnętrznego obserwatora - zachowanie byłoby spójne każda część agregatu zawiera tylko odniesienie do katalogu głównego, a nie do żadnej innej części. Moim zdaniem kluczową zasadą jest to, że każdy obiekt zmienny powinien mieć jednego właściciela ; agregat to zbiór obiektów, które wszystkie są własnością jednego obiektu („agregatu głównego”), które powinny znać wszystkie odniesienia do jego części.
supercat
3

Sugerowałbym posiadanie obiektów „child-factory”, które są przekazywane do metody nadrzędnej, która tworzy dziecko (przy użyciu obiektu „child factory”), dołącza je i zwraca widok. Sam obiekt potomny nigdy nie zostanie odsłonięty poza rodzicem. To podejście może działać dobrze w przypadku symulacji. W symulacji elektronicznej jeden konkretny obiekt „fabryki podrzędnej” może reprezentować specyfikacje dla pewnego rodzaju tranzystora; inny może reprezentować specyfikację rezystora; obwód, który potrzebuje dwóch tranzystorów i czterech rezystorów, może zostać utworzony z użyciem kodu takiego jak:

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

Zauważ, że symulator nie musi zachowywać żadnego odniesienia do obiektu twórcy podrzędnego i AddComponentnie zwróci odniesienia do obiektu utworzonego i przechowywanego przez symulator, ale raczej obiekt reprezentujący widok. Jeśli AddComponentmetoda jest ogólna, obiekt widoku może zawierać funkcje specyficzne dla komponentu, ale nie ujawnia elementów, których rodzic używa do zarządzania załącznikiem.

supercat
źródło
2

Świetna oferta. Nie wiem, która metoda jest „najlepsza”, ale tutaj można znaleźć najbardziej ekspresyjne metody.

Zacznij od najprostszej możliwej klasy nadrzędnej i podrzędnej. Napisz do nich swój kod. Gdy zauważysz duplikację kodu, który można nazwać, umieść go w metodzie.

Może dostaniesz addChild(). Może dostajesz coś takiego jak addChildren(List<Child>)lub addChildrenNamed(List<String>)lub loadChildrenFrom(String)lub newTwins(String, String)lub Child.replicate(int).

Jeśli twoim problemem jest narzucenie relacji jeden do wielu, może powinieneś

  • wrzucaj go do seterów, co może prowadzić do zamieszania lub klauzul o rzucaniu
  • usuwasz seterów i tworzysz specjalne metody kopiowania lub przenoszenia - co jest wyraziste i zrozumiałe

To nie jest odpowiedź, ale mam nadzieję, że znajdziesz ją podczas czytania.

Użytkownik
źródło
0

Rozumiem, że posiadanie powiązań między dzieckiem a rodzicem miało swoje wady, jak wspomniano powyżej.

Jednak w wielu scenariuszach obejścia zdarzeń i inne „odłączone” mechanizmy również przynoszą własne złożoności i dodatkowe wiersze kodu.

Na przykład podniesienie zdarzenia od Dziecka, które ma być odebrane przez Rodzica, wiąże oba razem, choć w luźny sposób.

Być może dla wielu scenariuszy dla wszystkich programistów jasne jest, co oznacza właściwość Child.Parent. W przypadku większości systemów, nad którymi pracowałem, działało to dobrze. Nadmiar inżynierii może być czasochłonny i…. Mylące!

Mieć metodę Parent.AttachChild (), która wykonuje całą pracę potrzebną do powiązania dziecka z rodzicem. Wszyscy mają jasność co do tego, co to znaczy

Rax
źródło