Каков наилучший способ инициализации ссылки ребенка на его родителя?

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
Лучшая практика заключается в том, что дети не должны знать о своих родителях.
Теластин
4
пожалуйста, не кросс-пост : stackoverflow.com/questions/26643528/…
Гнат
2
@Telastyn Я не могу не читать это как язык в щеке, и это весело. Также абсолютно чертовски точно. Стивен, термин, который нужно изучить, является «ациклическим», так как существует множество литературы о том, почему вы должны делать графики ациклическими, если это вообще возможно.
Джимми Хоффа
10
@Telastyn, вы должны попытаться использовать этот комментарий на parenting.stackexchange
Фабио Марколини
2
Хм. Не знаю, как переместить сообщение (не вижу флажок управления). Я повторно отправил сообщение программистам, так как кто-то сказал мне, что это там.
Стивен Брошар

Ответы:

19

Я бы держался подальше от любого сценария, который обязательно требует, чтобы ребенок знал о родителе.

Существуют способы передачи сообщений от ребенка к родителю через события. Таким образом, родительский элемент после добавления просто должен зарегистрироваться в событии, которое вызывает дочерний элемент, при этом дочерний элемент не должен знать непосредственно о родительском элементе. В конце концов, это, вероятно, предполагаемое использование ребенком знания о его родителе, чтобы иметь возможность использовать родителя для какого-либо эффекта. За исключением того, что вы не хотите, чтобы ребенок выполнял работу родителя, поэтому на самом деле вы просто хотите сказать родителю, что что-то произошло. Поэтому вам нужно обработать событие для ребенка, которым родитель может воспользоваться.

Этот шаблон также очень хорошо масштабируется, если это событие станет полезным для других классов. Возможно, это немного излишне, но это также не позволяет вам выстрелить себе в ногу позже, поскольку становится заманчивым желание использовать родителя в вашем дочернем классе, который только больше объединяет эти два класса. Рефакторинг таких классов впоследствии занимает много времени и может легко создавать ошибки в вашей программе.

Надеюсь, это поможет!

Нил
источник
6
« Держись подальше от любого сценария, который обязательно требует, чтобы ребенок знал о родителе » - почему? Ваш ответ основан на предположении, что круговые графы объектов - плохая идея. Хотя иногда это имеет место (например, когда управление памятью осуществляется посредством наивного подсчета ссылок, а не в C #), в целом это неплохо. Примечательно, что шаблон наблюдателя (который часто используется для отправки событий) включает в себя наблюдаемое ( Child), поддерживающее набор наблюдателей ( Parent), которое вновь вводит цикличность в обратном направлении (и вводит ряд собственных проблем).
Амон
1
Потому что циклические зависимости означают структурирование вашего кода таким образом, что вы не можете иметь одно без другого. По характеру отношений родитель-ребенок они должны быть отдельными объектами, иначе вы рискуете иметь два тесно связанных класса, которые также могут быть одним гигантским классом со списком для всей тщательности, заложенной в его дизайн. Я не вижу, как шаблон Observer такой же, как родитель-потомок, за исключением того факта, что один класс имеет ссылки на несколько других. Для меня parent-child - это родитель, имеющий сильную зависимость от ребенка, но не обратную.
Нил
Я согласен. В этом случае события являются лучшим способом обработки отношений между родителями и детьми. Это шаблон, который я использую очень часто, и он делает код очень простым в обслуживании, вместо того, чтобы беспокоиться о том, что дочерний класс делает с родителем через ссылку.
Вечное 21
@Neil: взаимные зависимости экземпляров объектов являются естественной частью многих моделей данных. При механическом моделировании автомобиля различные части автомобиля должны будут передавать силы друг другу; это обычно лучше обрабатывать, когда все части автомобиля рассматривают сам механизм моделирования как «родительский» объект, чем при наличии циклических зависимостей между всеми компонентами, но если компоненты способны реагировать на любые стимулы извне родительского объекта им понадобится способ уведомить родителя, если такие стимулы будут иметь какие-либо эффекты, о которых родитель должен знать.
суперкат
2
@Neil: Если домен включает в себя объекты леса с неприводимыми циклическими зависимостями данных, то любая модель домена будет это делать. Во многих случаях это будет также означать, что лес будет вести себя как один гигантский объект , хочет он этого или нет . Агрегированный шаблон служит для концентрации сложности леса в одном объекте класса, называемом Aggregate Root. В зависимости от сложности моделируемого домена, этот совокупный корень может стать несколько большим и громоздким, но если сложность неизбежна (как в случае с некоторыми доменами), это лучше ...
суперкат
10

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

Недостаток: дочерний элемент создается без родительской ссылки.

Я не считаю это недостатком. Фактически, дизайн вашей программы может извлечь выгоду из дочерних объектов, которые могут быть созданы без родителя. Например, это может сделать тестирование детей в изоляции намного проще. Если пользователь вашей модели забывает добавить дочерний элемент к своему родительскому элементу и вызывает методы, которые ожидают инициализации родительского свойства в вашем дочернем классе, он получает исключение null ref - это именно то, что вам нужно: ранний сбой из-за неправильного использование.

И если вы думаете, что дочерний атрибут должен инициализировать свой родительский атрибут в конструкторе при любых обстоятельствах по техническим причинам, используйте что-то вроде «нулевого родительского объекта» в качестве значения по умолчанию (хотя это может привести к ошибкам маскирования).

Док Браун
источник
Если дочерний объект не может сделать ничего полезного без родителя, для запуска дочернего объекта без родителя ему потребуется SetParentметод, который либо должен будет поддерживать изменение происхождения существующего дочернего объекта (что может быть трудно сделать, и / или бессмысленный) или будет вызван только один раз. Моделирование таких ситуаций, как совокупности (как предлагает Майк Браун), может быть намного лучше, чем начинать детей без родителей.
Суперкат
Если вариант использования требует чего-то хотя бы один раз, дизайн должен учитывать это всегда. Тогда ограничить эту способность особыми обстоятельствами легко. Добавление такой возможности впоследствии, однако, обычно невозможно. Лучшее решение - вариант 3. Вероятно, с третьим объектом «Отношения» между родителем и потомком, так что [Parent] ---> [Relationship (Parent принадлежит Child)] <--- [Child]. Это также позволяет использовать несколько экземпляров [Relationship], таких как [Child] ---> [Relationship (Child принадлежит Parent)] <--- [Parent].
DocSalvager
3

Нет ничего, что могло бы предотвратить высокую сплоченность между двумя классами, которые обычно используются вместе (например, Order и LineItem обычно ссылаются друг на друга). Однако в этих случаях я, как правило, придерживаюсь правил разработки, управляемых доменом, и моделирую их как агрегат, в котором родитель является агрегатным корнем. Это говорит нам о том, что AR отвечает за время жизни всех объектов в своей совокупности.

Таким образом, наиболее похоже на ваш четвертый сценарий, где родительский объект предоставляет метод для создания своих дочерних элементов, принимая любые необходимые параметры для правильной инициализации дочерних элементов и добавляя его в коллекцию.

Майкл Браун
источник
1
Я бы использовал слегка более слабое определение агрегата, которое позволило бы существовать внешним ссылкам на части агрегата, отличные от корня, при условии, что - с точки зрения внешнего наблюдателя - поведение будет согласованным каждая часть агрегата содержит только ссылку на корень, а не на любую другую часть. На мой взгляд, основной принцип заключается в том, что каждый изменяемый объект должен иметь одного владельца ; агрегат - это коллекция объектов, которые все принадлежат одному объекту («корень агрегата»), который должен знать обо всех ссылках, которые существуют на его части.
суперкат
3

Я бы предложил иметь объекты "child-factory", которые передаются родительскому методу, который создает дочерний объект (используя объект "child factory"), присоединяет его и возвращает представление. Сам дочерний объект никогда не будет выставлен вне родительского объекта. Этот подход может хорошо работать для таких вещей, как моделирование. В моделировании электроники один конкретный объект «фабрика-потомок» может представлять спецификации для некоторого типа транзистора; другой может представлять спецификации для резистора; схема, для которой нужны два транзистора и четыре резистора, может быть создана с кодом, подобным следующему:

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

Обратите внимание, что симулятору не нужно сохранять какую-либо ссылку на объект child-creator, и AddComponentон не будет возвращать ссылку на объект, созданный и поддерживаемый симулятором, а скорее на объект, который представляет представление. Если AddComponentметод является универсальным, объект представления может включать в себя специфичные для компонента функции, но не отображать элементы, которые родительский элемент использует для управления вложением.

Supercat
источник
2

Отличный список. Я не знаю, какой метод является «лучшим», но вот один, чтобы найти наиболее выразительные методы.

Начните с самого простого родительского и дочернего класса. Напишите свой код с этими. Как только вы заметите дублирование кода, которое может быть названо, вы поместите его в метод.

Может быть, вы получите addChild(). Может быть, вы получаете что-то вроде addChildren(List<Child>)или addChildrenNamed(List<String>)или loadChildrenFrom(String)или newTwins(String, String)или или Child.replicate(int).

Если ваша проблема действительно в навязывании отношений один-ко-многим, возможно, вам следует

  • форсировать его в сеттерах, что может привести к путанице или броскам
  • вы удаляете сеттеры и создаете специальные методы копирования или перемещения - что является выразительным и понятным

Это не ответ, но я надеюсь, что вы найдете его, читая это.

пользователь
источник
0

Я ценю то, что наличие ссылок от ребенка к родителю имело свои недостатки, как отмечалось выше.

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

Например, создание события от Child, которое будет получено его Родителем, свяжет оба вместе, хотя и в стиле слабой связи.

Возможно, для многих сценариев всем разработчикам понятно, что означает свойство Child.Parent. Для большинства систем, над которыми я работал, это работало просто отлично. На проектирование может уйти много времени и…. Заблуждение!

Иметь метод Parent.AttachChild (), который выполняет всю работу, необходимую для привязки дочернего элемента к его родительскому элементу. Всем понятно, что это значит

Ракс
источник