Який найкращий спосіб ініціалізувати посилання дитини на свого батька?


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

    Знизу: встановлення батьків - це двоетапний процес для споживача.

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

    Знизу: абонент все ще має двоетапний процес встановлення батьків.

    Нижня сторона: перевірка часу виконання - знижує продуктивність та додає код до кожного додавання / налаштування.

  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. Батько виставляє фабричні методи додавання, щоб дитина завжди мала посилання на батьків. Дитячий 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;
    

    Нижня сторона: доведеться дублювати параметри дочірнього конструктора у методах додаткової фабрики.

    Нижня сторона: Неможливо використовувати налаштування властивостей для одинокої дитини. Здається, кульгавим, що мені потрібен метод встановити значення, але забезпечити доступ для читання через геть властивості. Це однобічний.

  5. Child додає себе до батьків, на які посилається у своєму конструкторі. Дитячий ctor є загальнодоступним. Немає загальнодоступного доступу з батьків.

    //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
Найкраща практика полягає в тому, що діти не повинні знати про своїх батьків.
Теластин


2
@Telastyn Я не можу не прочитати це як мову в щоках, і це весело. Також абсолютно мертвий криваво точний. Стівен, термін, на який слід звернути увагу, - це "ациклічний", оскільки там багато літератури про те, чому слід робити графіки ациклічними, якщо це взагалі можливо.
Джиммі Хоффа

10
@Telastyn вам слід спробувати скористатись цим коментарем щодо parenting.stackexchange
Фабіо Марколіні

2
Хм. Не знаєте, як перемістити публікацію (не бачите прапорець). Я повторно розмістив програмістів, оскільки хтось сказав, що він там є.
Стівен Брошар

Відповіді:


19

Я б тримався осторонь будь-якого сценарію, який обов'язково вимагає від дитини знати про батьків.

Існують способи передачі повідомлень від дитини до батьків через події. Таким чином, батько, додаючи, просто повинен зареєструватися на подію, яка викликає дитину, без того, щоб дитина мала знати про батьків. Якщо, мабуть, це можливо використання дитини, яка знає про свого батька, щоб мати змогу використовувати батьків з певним ефектом. За винятком того, що ви не хочете, щоб дитина виконувала роботу батька, тому дійсно те, що ви хочете зробити, - це просто сказати батькові, що щось сталося. Тому те, що потрібно впоратися, - це подія на дитину, з якої батько може скористатися.

Ця закономірність також дуже масштабується, якщо ця подія стане корисною для інших класів. Можливо, це трохи непосильне, але це також заважає пізніше себе стріляти в ногу, оскільки стає спокусливим бажати використовувати батьків у своєму дошкільному класі, який лише з’єднує два класи ще більше. Рефакторинг таких класів після цього забирає багато часу і може легко створювати помилки у вашій програмі.

Сподіваюся, що це допомагає!


6
" Тримайтеся подалі від будь-якого сценарію, який обов'язково вимагає від дитини знати про батьків " - чому? Ваша відповідь залежить від припущення, що кругові графіки об'єктів - це погана ідея. Незважаючи на те, що це іноді (наприклад, коли управління пам'яттю за допомогою наївного перерахунку - це не так у C #), це взагалі не погано. Зокрема, Шаблон спостерігача (який часто використовується для диспетчеризації подій) передбачає спостережуване ( Child) підтримання набору спостерігачів ( Parent), який вводить циркулярність назад (і вносить ряд власних питань).
амон

1
Тому що циклічні залежності означають структурування коду таким чином, що ви не можете мати один без іншого. За характером стосунків батько-дитина вони повинні бути окремими сутностями, інакше ви ризикуєте мати два щільно зв'язані класи, які також можуть бути єдиним гігантським класом зі списком для ретельного догляду, вкладеного в його розробку. Я не бачу, як шаблон спостерігача такий же, як батько-дитина, крім того, що один клас має посилання на кілька інших. Для мене батько-дитина - це батько, який має сильну залежність від дитини, але не зворотна.
Ніл

Я згоден. Події - найкращий спосіб вирішити стосунки батько-дитина в цьому випадку. Це шаблон, який я використовую дуже часто, і він робить код дуже простим у підтримці, замість того, щоб турбуватися про те, що дочірній клас робить з батьком через посилання.
Вічний21

@Neil: Взаємні залежності екземплярів об'єктів є природною частиною багатьох моделей даних. При механічному моделюванні автомобіля різні частини автомобіля повинні будуть передавати сили один одному; це, як правило, краще вирішувати, якщо всі частини автомобіля розглядають сам двигун моделювання як "батьківський" об'єкт, ніж через циклічні залежності між усіма компонентами, але якщо компоненти здатні реагувати на будь-які подразники поза батьків їм знадобиться спосіб сповістити батьків, якщо такі стимули мають якісь наслідки, про які батько повинен знати.
supercat

2
@Neil: Якщо домен включає лісові об'єкти з невідмінними циклічними залежностями даних, будь-яка модель домену також буде робити це. У багатьох випадках це ще більше означатиме, що ліс буде вести себе як єдиний гігантський об'єкт, хоче він цього чи ні . Сукупний малюнок служить для концентрації складності лісу в об'єкт одного класу, який називається корінним агрегатом. Залежно від складності модельованого домену цей агрегатний корінь може стати дещо великим і громіздким, але якщо складність неминуча (як це стосується деяких доменів), то краще ...
supercat

10

Я думаю, що ваш варіант 3 може бути найчистішим. Ви написали.

Нижня сторона: дитина створюється без батьківських довідок.

Я не бачу це як зворотний бік. Фактично, дизайн вашої програми може виграти дочірні об’єкти, які можна створити спочатку без батьків. Наприклад, це може значно полегшити тестування дітей на самоті. Якщо користувач вашої моделі забуває додати дитину до свого батька і викликає методи, які очікують, що батьківська властивість ініціалізована у вашому дочірньому класі, то він отримує нульове виключення - саме цього ви хочете: ранній збій за неправильну використання.

І якщо ви думаєте, що дитині потрібен атрибут батьків, який ініціалізується в конструкторі за будь-яких обставин з технічних причин, використовуйте щось на зразок "нульового батьківського об'єкта" як значення за замовчуванням (хоча це має ризик маскування помилок).


Якщо дочірній об’єкт не може зробити нічого корисного без батька, для запуску дочірнього об’єкта без батьків знадобиться SetParentметод, який повинен або підтримувати зміну батьківства існуючої дитини (що може бути важко зробити і / або безглуздо) або може бути надіслана лише один раз. Моделювання таких ситуацій, як агрегати (як пропонує Майк Браун), може бути набагато кращим, ніж діти починати без батьків.
supercat

Якщо випадок використання вимагає чогось навіть одного разу, дизайн повинен це робити завжди. Тоді обмежити цю здатність до особливих обставин легко. Після цього, однак, звичайно неможливо додати таку можливість. Найкраще рішення - Варіант 3. Можливо, з третім об'єктом "Зв'язок" між батьком і дитиною таким, що [Батько] ---> [Відносини (Батько належить дитині)] <--- [Дитина]. Це також дозволяє декілька екземплярів [Відносини], таких як [Дитина] ---> [Відносини (дитиною належить батько)] <--- [Батько].
DocSalvager

3

Ніщо не заважає високій згуртованості між двома класами, які використовуються спільно (наприклад, Order і LineItem зазвичай посилаються один на одного). Однак у цих випадках я схильний дотримуватися правил дизайну, керованих доменом, і моделюю їх як агрегат, при цьому батьків є корінним агрегатом. Це говорить нам про те, що АР відповідає за час життя всіх об'єктів в їх сукупності.

Тож було б найбільше, як у вашому сценарії чотири, коли батько відкриває метод створення своїх дітей, які приймають будь-які необхідні параметри, щоб правильно ініціалізувати дітей та додавати їх до колекції.


1
Я б застосував дещо розпущене визначення сукупності, яке дозволило б існувати зовнішні посилання на частини сукупності, відмінні від кореня, за умови, що - з точки зору стороннього спостерігача - поведінка буде послідовною з кожною частиною сукупності міститься лише посилання на корінь, а не на будь-яку іншу частину. На мій погляд, ключовим принципом є те, що кожен об'єкт, що змінюється, повинен мати одного власника ; сукупність - це сукупність об'єктів, які належать одному об'єкту ("сукупний корінь"), який повинен знати всі посилання, що існують на його частини.
supercat

3

Я б запропонував мати об'єкти "фабрика дочірніх", які передаються батьківському методу, який створює дитину (використовуючи об'єкт "дочірня фабрика"), додає її та повертає вигляд. Сам дочірній предмет ніколи не буде виставлений поза батьків. Такий підхід може добре працювати для таких речей, як моделювання. При моделюванні електроніки один конкретний об'єкт "дитячої фабрики" може представляти специфікації для певного транзистора; інший може представляти технічні характеристики резистора; схема, якій потрібні два транзистори та чотири резистори, може бути створена з таким кодом:

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>)або addChildrenNamed(List<String>)чи loadChildrenFrom(String)або newTwins(String, String)чи Child.replicate(int).

Якщо ваша проблема справді полягає у вимушенні стосунків один до багатьох, можливо, вам слід

  • примушуйте це в сетерах, що може призвести до плутанини або застережень
  • ви видаляєте сетери та створюєте спеціальні методи копіювання чи переміщення - що є виразним та зрозумілим

Це не відповідь, але я сподіваюся, що ви знайдете її, читаючи це.


0

Я вдячний, що наявність зв’язків від дитини до батьків мала свої мінуси, як зазначено вище.

Однак для багатьох сценаріїв обхідні події та інші «відключені» механіки також приносять свої складності та додаткові рядки коду.

Наприклад, підняття події від Child, яку він отримає від Батьків, буде зв'язувати обидва разом, хоча і в розв'язаному вигляді.

Можливо, для багатьох сценаріїв усім розробникам зрозуміло, що означає властивість Child.Parent. Для більшості систем, над якими я працював, працював чудово. Над технікою може бути багато часу і…. Заплутано!

Запропонуйте метод Parent.AttachChild (), який виконує всю роботу, необхідну для прив’язки дитини до свого батька. Всім зрозуміло, що це "означає"

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.