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


当前回答

你什么时候可以用作文?

你可以一直使用合成。在某些情况下,继承也是可能的,并可能导致更强大和/或更直观的API,但组合始终是一种选择。

什么时候可以使用继承?

人们常说,如果“bar是foo”,那么bar类可以继承foo类。不幸的是,仅此测试并不可靠,请改用以下方法:

bar是foo,AND酒吧可以做foos所能做的一切。

第一个测试确保Foo的所有getter在Bar中都有意义(=共享的财产),而第二个测试确保所有Foo的setter都在Bar(=共享功能)中有意义。

示例:狗/动物

狗是一种动物,狗可以做动物所能做的一切(如呼吸、移动等)。因此,狗类可以继承动物类。

反例:圆/椭圆

圆是椭圆,但圆不能做椭圆所能做的一切。例如,圆不能拉伸,而椭圆可以拉伸。因此,Circle类不能继承Ellipse类。

这被称为圆椭圆问题,这并不是一个真正的问题,但更多的是表明“一个条就是一个foo”本身不是一个可靠的测试。特别是,这个例子强调了派生类应该扩展基类的功能,而不是限制它。否则,基类不能以多态的方式使用。添加测试“bar可以做foos所能做的一切”确保了多态性的使用是可能的,这相当于Liskov替换原则:

使用基类指针或引用的函数必须能够在不知道的情况下使用派生类的对象

什么时候应该使用继承?

即使你可以使用继承,也不意味着你应该这样做:使用组合总是一种选择。继承是一个强大的工具,允许隐式代码重用和动态分派,但它确实有一些缺点,这就是为什么组合经常被首选的原因。继承和组合之间的权衡并不明显,在我看来,在lcn的回答中可以得到最好的解释。

作为经验法则,当多态性使用被认为非常普遍时,我倾向于选择继承而不是组合,在这种情况下,动态分派的功能可以导致更可读和更优雅的API。例如,在GUI框架中有一个多态类Widget,或者在XML库中有一种多态类Node,这样就可以使用一个比纯基于组合的解决方案更易读、更直观的API。

其他回答

当你在两个类之间有一个is-a关系(例如狗是狗),你就去继承。

另一方面,当你在两个班级(学生有课程)或(老师学习课程)之间有一种或某种形容词关系时,你选择了作文。

尽管作文是首选,但我想强调继承的优点和作文的缺点。

继承优势:

它建立了逻辑“IS a”关系。如果汽车和卡车是两种类型的车辆(基类),则子类是一个基类。即汽车就是车辆卡车是一种车辆通过继承,您可以定义/修改/扩展功能基类不提供实现,子类必须重写完整方法(抽象)=>您可以实现合约基类提供默认实现,子类可以更改行为=>您可以重新定义合约子类通过调用super.methodName()作为第一条语句来向基类实现添加扩展=>您可以扩展合约基类定义了算法的结构,子类将覆盖算法的一部分=>您可以在不改变基类骨架的情况下实现Template_method

组成的缺点:

在继承中,子类可以直接调用基类方法,即使它由于IS A关系而没有实现基类方法。如果使用组合,则必须在容器类中添加方法以公开包含的类API

例如,如果汽车包含车辆,并且您必须获得汽车的价格(已在车辆中定义),您的代码将如下

class Vehicle{
     protected double getPrice(){
          // return price
     }
} 

class Car{
     Vehicle vehicle;
     protected double getPrice(){
          return vehicle.getPrice();
     }
} 

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

程序代码

这方面的一个例子是不使用类的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组成。标题行为取自雇员。这种显式组合消除了其他问题中的歧义,您将遇到更少的错误。

继承是非常强大的,但你不能强迫它(参见:圆-椭圆问题)。如果你真的不能完全确定一个真正的“is-a”子类型关系,那么最好是组合。

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

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

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

这完全取决于实体关系。