初始化孩子对其父母的引用的最佳方法是什么?


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;

    缺点:必须在add-factory方法中复制子构造函数的参数。

    缺点:不能为单身儿童使用属性设置器。似乎很la脚,我需要一种方法来设置值,但需要通过属性获取器提供读取访问权限。它不平衡。

  5. 子级将自身添加到其构造函数中引用的父级。儿童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
最佳做法是孩子不应该了解父母。
Telastyn 2014年


2
@Telastyn我忍不住把它当成是脸颊上的舌头,这很有趣。也完全死了血腥准确。史蒂文(Steven),要研究的术语是“非循环的”,因为那里有大量的文献说明为什么要尽可能使图非循环。
2014年

10
@Telastyn,您应该尝试在parenting.stackexchange上使用该评论
Fabio Marcolini

2
嗯 不知道如何移动帖子(看不到标记控件)。我被重新发布给程序员,因为有人告诉我它属于那里。
Steven Broshar 2014年

Answers:


18

我会避免任何可能需要孩子了解父母的情况。

有一些方法可以通过事件将消息从子级传递到父级。这样,添加后,父级只需要将子级触发的事件注册,而无需子级直接了解父级。毕竟,这可能是孩子了解其父母的预期用途,以便能够有效使用父母。除非您不希望孩子执行父母的工作,所以您真正要做的只是告诉父母发生了什么事。因此,您需要处理的是子级事件,父级可以利用该事件。

如果此事件对其他类有用,则此模式的伸缩性也很好。也许这有点过分,但是它也阻止了以后用脚射击自己,因为很想在您的孩子班级中使用父级,这只会使这两个班级更多地结合在一起。之后,此类类的重构非常耗时,并且很容易在程序中创建错误。

希望有帮助!


6
远离任何必然要求孩子了解父母的情况 ” – 为什么?您的答案取决于圆形对象图是一个坏主意的假设。尽管有时是这种情况(例如,通过幼稚的引用计数进行内存管理-在C#中不是这种情况),但通常这并不是一件坏事。值得注意的是,“观察者模式”(通常用于调度事件)涉及可Child观察者(Parent)维护一组观察者(),这以向后的方式重新引入了循环性(并引入了许多问题)。
阿蒙2014年

1
因为循环依赖关系意味着以某种方式构造您的代码,以至于没有其他东西就不能拥有一个。根据亲子关系的性质,它们应该是单独的实体,否则您将冒着两个紧密耦合的类的风险,它们可能是一个巨大的类,并且在其设计中列出了所有细心的注意事项。我看不到观察者模式与父子模式如何相同,除了一个类引用了其他几个之外。对我来说,亲子是对孩子有强烈依赖性的父母,但对孩子却没有依赖性。
尼尔

我同意。在这种情况下,事件是处理父子关系的最佳方法。这是我经常使用的模式,它使代码非常易于维护,而不必担心子类通过引用对父级所做的事情。
Eternal21 '11

@Neil:对象实例的相互依赖性是许多数据模型的自然组成部分。在汽车的机械仿真中,汽车的不同部分将需要相互传递力。通常,通过使汽车的所有零件都将仿真引擎本身视为“父”对象,比通过使所有组件具有循环依赖关系(但如果组件能够响应来自父级外部的任何刺激)更好地解决这一问题。他们需要一种方法来通知父母,如果这样的刺激有父母需要知道的任何影响。
supercat 2014年

2
@Neil:如果域包含具有不可简化的循环数据依赖性的林对象,则域的任何模型也将这样做。在许多情况下,这将进一步暗示森林无论是否愿意,都将表现为单个巨型对象。聚合模式用于将森林的复杂性集中到称为“聚合根”的单个类对象中。根据要建模的域的复杂度,聚合根可能会变得有些大而笨拙,但是如果不可避免(如某些域那样),则更好……
supercat 2014年

10

我认为您的选择3可能是最干净的。你写了。

缺点:创建子级时没有父级引用。

我不认为这是不利的。实际上,程序的设计可以受益于子对象,这些子对象可以在没有父对象的情况下创建。例如,它可以使孤立地测试孩子变得容易得多。如果您的模型用户忘记了将一个子代添加到其父代,并调用了一个期望在您的子代类中初始化了父代属性的方法,那么他将得到一个null ref异常-这正是您想要的:一个错误的早期崩溃用法。

并且,如果出于技术原因您认为子级在所有情况下都需要在构造函数中初始化其父级属性,请使用“空父级对象”之类的默认值(尽管这样做有掩盖错误的风险)。


如果子对象在没有父对象的情况下无法做任何有用的事情,那么让子对象在没有父对象的情况下开始将要求它具有一种SetParent方法,该方法要么需要支持更改现有子对象的父对象(这可能很难做到, /或无意义)或只能调用一次。将这种情况建模为聚合(如Mike Brown所建议的)比让孩子开始没有父母的情况要好得多。
2014年

如果用例甚至要求一次,则设计必须始终考虑到这一点。然后,将这种能力限制在特殊情况下就很容易了。但是,随后添加这种功能通常是不可能的。最佳解决方案是选项3。可能在父级和子级之间使用第三个“ Relationship”对象,使得[Parent] ---> [Relationship(Parent拥有子级)] <--- [Child]。这也允许多个[Relationship]实例,例如[Child] ---> [Relationship(Child由父级拥有)] <--- [Parent]。
DocSalvager

3

没有什么可以防止通常一起使用的两个类之间的高内聚性(例如,Order和LineItem通常会相互引用)。然而,在这些情况下,我倾向于坚持领域驱动设计规则,它们建模为一个聚合与母公司作为聚合根。这告诉我们,AR负责聚合中所有对象的生命周期。

因此,这最像您的方案四,其中父级公开一个方法来创建其子级,该方法接受任何必要的参数来正确初始化子级并将其添加到集合中。


1
我将使用一个稍微宽松的聚合定义,该定义允许在聚合的除根以外的部分中存在外部引用,但前提是(从外部观察者的角度来看)行为是一致的集合的每个部分仅包含对根的引用,而不包含对任何其他部分的引用。在我看来,关键原则是每个可变对象都应拥有一个所有者。集合是对象的集合,这些对象全部由一个对象(“集合根”)拥有,该对象应该知道存在于其各个部分的所有引用。
supercat 2014年

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>)or addChildrenNamed(List<String>)loadChildrenFrom(String)or newTwins(String, String)or or的信息Child.replicate(int)

如果您的问题确实与强迫一对多关系有关,那么您应该

  • 将其强行塞入二传手,可能会导致混乱或丢球
  • 您删除设置器并创建特殊的复制或移动方法-这是富有表现力和易于理解的

这不是答案,但我希望您在阅读本文时能找到答案。


0

我很欣赏如上所述,从孩子到父母的联系有其不利之处。

但是,在许多情况下,事件和其他“脱节”机制的变通办法也带来了自己的复杂性和额外的代码行。

例如,从Child引发一个事件以使其被Parent接收,尽管这两种事件都是松散耦合的,但它们会绑定在一起。

也许在许多情况下,所有开发人员都清楚Child.Parent属性的含义。对于我研究过的大多数系统来说,它都工作得很好。过度设计可能很耗时,并且…。令人困惑!

具有Parent.AttachChild()方法,该方法执行将Child绑定到其父级所需的所有工作。每个人都清楚这是什么意思

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.