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


当前回答

除了是/有一个考虑因素之外,还必须考虑对象必须经历的继承的“深度”。任何超过五或六级继承深度的内容都可能会导致意外的强制转换和装箱/拆箱问题,在这种情况下,编写对象可能是明智的。

其他回答

你什么时候可以用作文?

你可以一直使用合成。在某些情况下,继承也是可能的,并可能导致更强大和/或更直观的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。

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

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

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

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

程序代码

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

继承是一种非常强大的代码重用机制。但需要正确使用。如果子类也是父类的子类型,我会说继承是正确使用的。如上所述,利斯科夫替代原则是这里的关键点。

子类与子类型不同。您可以创建不是子类型的子类(此时应该使用组合)。为了理解什么是子类型,让我们开始解释什么是类型。

当我们说数字5是整数类型时,我们说明5属于一组可能的值(例如,请参阅Java原语类型的可能值)。我们还声明,我可以对值执行一组有效的方法,如加法和减法。最后,我们要说明的是,有一组财产总是可以满足的,例如,如果我将值3和5相加,结果会得到8。

举另一个例子,考虑抽象数据类型,整数集合和整数列表,它们可以保存的值仅限于整数。它们都支持一组方法,如add(newValue)和size()。而且它们都有不同的财产(类不变量),Sets不允许重复,而List允许重复(当然还有它们都满足的其他财产)。

子类型也是一种类型,它与另一种类型(称为父类型(或父类型))有关系。子类型必须满足父类型的功能(值、方法和财产)。这种关系意味着在任何期望超类型的上下文中,它都可以被子类型替代,而不会影响执行的行为。让我们去看一些代码来举例说明我在说什么。假设我写了一个整数列表(用某种伪语言):

class List {
  data = new Array();

  Integer size() {
    return data.length;
  }

  add(Integer anInteger) {
    data[data.length] = anInteger;
  }
}

然后,我将整数集合写成整数列表的子类:

class Set, inheriting from: List {
  add(Integer anInteger) {
     if (data.notContains(anInteger)) {
       super.add(anInteger);
     }
  }
}

我们的整数集合类是整数列表的子类,但不是子类型,因为它不满足列表类的所有特性。满足了方法的值和签名,但不满足财产。add(Integer)方法的行为已经明显改变,没有保留父类型的财产。从类的客户端的角度思考。他们可能会收到一组整数,其中需要一个整数列表。客户端可能希望添加一个值,并将该值添加到列表中,即使该值已经存在于列表中。但如果价值存在,她就不会有这种行为。她大吃一惊!

这是一个不正确使用继承的典型例子。在这种情况下使用合成。

(片段来自:正确使用继承)。

在可以枚举不变量的情况下,子类型更合适,功能更强大,否则使用函数组合实现可扩展性。