为什么更喜欢组合而不是继承?每种方法都有哪些权衡?什么时候应该选择继承而不是组合?


当前回答

这里没有找到满意的答案,所以我写了一个新的。

为了理解为什么“更喜欢组合而不是继承”,我们需要首先找回这个缩短的习惯用法中省略的假设。

继承有两个好处:子类型化和子类化

子类型化意味着符合类型(接口)签名,即一组API,并且可以覆盖部分签名以实现子类型化多态性。子类化意味着方法实现的隐式重用。

这两个好处带来了两个不同的继承目的:面向子类型和面向代码重用。

如果代码重用是唯一的目的,那么子类化可能会给一个比他所需要的更多的东西,即父类的一些公共方法对于子类来说没有多大意义。在这种情况下,不赞成组合而不是继承,而是要求组合。这也是“is-a”与“has-a”概念的由来。

因此,只有当有了子类型,即以后以多态的方式使用新类时,我们才会面临选择继承或组合的问题。这是在所讨论的缩短成语中被省略的假设。

To子类型要符合类型签名,这意味着组合必须始终公开不少于该类型的API数量。现在开始进行权衡:

继承提供了直接的代码重用(如果不被重写),而组合必须对每个API重新编码,即使这只是一个简单的委托工作。继承通过内部多态站点this提供了直接的开放递归,即在另一个成员函数中调用重写方法(甚至类型),无论是公共的还是私有的(尽管不鼓励)。开放递归可以通过组合来模拟,但它需要额外的努力,并且可能并不总是可行的(?)。对重复问题的回答也有类似之处。继承公开受保护的成员。这打破了父类的封装,如果被子类使用,则会引入子类和父类之间的另一个依赖关系。组合具有控制反转的特性,其依赖性可以动态注入,如装饰器模式和代理模式所示。组合具有面向组合器编程的优点,即以类似于组合模式的方式工作。编程到接口后立即进行组合。组合具有易于多重继承的优点。

考虑到上述权衡,我们因此更喜欢组合而不是继承。然而,对于紧密相关的类,即当隐式代码重用真正带来好处,或者需要开放递归的魔力时,继承应该是选择。

其他回答

将遏制视为一种关系。汽车“有”发动机,人“有”名字等等。

把继承看作是一种关系。汽车“是”车辆,人“是”哺乳动物等。

我不认为这种做法值得赞扬。我直接从史蒂夫·麦康奈尔(Steve McConnell)的《代码大全》第二版第6.3节中得到了答案。

要从新程序员的不同角度解决这个问题:

当我们学习面向对象的编程时,继承经常很早就被教授,因此它被视为一个常见问题的简单解决方案。

我有三个类都需要一些通用功能。所以如果我编写一个基类并让它们全部继承,然后它们将所有这些都有这个功能,我只需要维护一次位置

这听起来很好,但实际上它几乎从未奏效,原因如下:

我们发现,我们希望我们的类具有其他一些功能。如果我们向类添加功能的方式是通过继承,那么我们必须决定——我们是否将其添加到现有基类中,即使不是每个从其继承的类都需要该功能?我们是否创建另一个基类?但是,已经从其他基类继承的类呢?我们发现,对于从基类继承的一个类,我们希望基类的行为稍有不同。所以现在我们回过头来修改我们的基类,可能会添加一些虚拟方法,或者更糟的是,一些代码会说,“如果我是继承的类型A,那么就这样做,但如果我是传承的类型B,那么就那样做。”这是很糟糕的,原因很多。一个是,每次我们改变基类时,我们都在有效地改变每个继承的类。所以我们真的在改变A、B、C和D类,因为我们需要在A类中有一个稍微不同的行为。尽管我们认为我们很小心,但我们可能会因为与这些类无关的原因而破坏其中一个类。我们可能知道为什么我们决定让所有这些类彼此继承,但这对其他必须维护我们代码的人来说可能没有意义。我们可能会迫使他们做出一个艰难的选择——我是做一些非常丑陋和混乱的事情来做出我需要的改变(见前面的要点),还是只是重写一堆内容。

最后,我们把代码绑在一些困难的地方,除了说:“酷,我了解了继承,现在我使用了它。”这并不意味着居高临下,因为我们都这样做了。但我们都这么做了,因为没有人告诉我们不要这样做。

当有人向我解释“喜欢组合而不是继承”时,我回想了一下每次尝试使用继承在类之间共享功能时,我都意识到大多数时候它并没有真正发挥作用。

解药是单一责任原则。将其视为约束。我的班级必须做一件事。我必须能够给我的班级起一个名字,以某种方式描述它所做的一件事。(凡事都有例外,但当我们学习时,绝对规则有时会更好。)因此,我无法编写名为ObjectBaseThatContainsVariousFunctionsNeedByDifferentClasses的基类。我所需要的任何不同的功能都必须在它自己的类中,然后需要该功能的其他类可以依赖于该类,而不是从该类继承。

在过于简化的风险下,这就是组合——组合多个类一起工作。一旦我们养成了这个习惯,我们就会发现它比使用继承更灵活、更可维护、更可测试。

合成与继承是一个广泛的主题。对于什么更好,没有真正的答案,因为我认为这一切都取决于系统的设计。

通常,对象之间的关系类型为选择其中一个对象提供了更好的信息。

如果关系类型是“is-A”关系,则继承是更好的方法。否则关系类型为“HAS-A”关系,则组合将更接近。

这完全取决于实体关系。

你需要看看Bob叔叔的SOLID类设计原则中的Liskov替换原则。:)

如果你明白其中的区别,那么解释起来就更容易了。

程序代码

这方面的一个例子是不使用类的PHP(尤其是在PHP5之前)。所有逻辑都编码在一组函数中。您可以包含包含助手函数等的其他文件,并通过在函数中传递数据来执行业务逻辑。随着应用程序的增长,这可能很难管理。PHP5试图通过提供更面向对象的设计来解决这一问题。

遗产

这鼓励使用类。继承是OO设计的三大原则之一(继承、多态性、封装)。

class Person {
   String Title;
   String Name;
   Int Age
}

class Employee : Person {
   Int Salary;
   String Title;
}

这是工作中的继承。员工“是”个人或继承自个人。所有继承关系都是“is-a”关系。Employee还隐藏Person的Title属性,意思是Employee.Title将返回Employees的Title,而不是Person。

作文

组合比继承更受欢迎。简单地说,你应该:

class Person {
   String Title;
   String Name;
   Int Age;

   public Person(String title, String name, String age) {
      this.Title = title;
      this.Name = name;
      this.Age = age;
   }

}

class Employee {
   Int Salary;
   private Person person;

   public Employee(Person p, Int salary) {
       this.person = p;
       this.Salary = salary;
   }
}

Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);

组成通常是“有”或“使用”关系。这里Employee类有一个Person。它不从Person继承,而是将Person对象传递给它,这就是它“有”Person的原因。

继承上的组合

现在,假设您要创建一个管理器类型,这样您就可以:

class Manager : Person, Employee {
   ...
}

但是,如果Person和Employee都声明了Title,那么这个示例会很好用?经理头衔应该返回“运营经理”还是“先生”?在合成中,这种模糊性得到了更好的处理:

Class Manager {
   public string Title;
   public Manager(Person p, Employee e)
   {
      this.Title = e.Title;
   }
}

Manager对象由Employee和Person组成。标题行为取自雇员。这种显式组合消除了其他问题中的歧义,您将遇到更少的错误。