language agnostic - 比继承更喜欢构图?




uml composition (20)

为什么喜欢构图而不是继承? 每种方法都有哪些折衷? 你应该什么时候选择继承而不是组合?


“比继承更喜欢构图”是一个设计原则,它说不要在不适合时滥用继承。

如果两个实体之间不存在真实世界的层次关系,请不要使用继承,而应使用合成。 构图代表“HAS”关系。

例如,汽车具有轮子,车身,发动机等,但是如果你继承这里,它就变成汽车是轮子 - 这是不正确的。

有关更多解释,请参阅比继承更喜欢构图


因为它比以后更容易/更容易修改,但不要使用组合总是方法,所以希望组合优于继承。 有了构图,使用依赖注入/设置器可以轻松改变行为。 继承比较严格,因为大多数语言不允许你从多个类型派生。 所以一旦你从TypeA派生出来,鹅就会或多或少地被煮熟了。

我对以上的酸性测试是:

  • TypeB是否希望公开TypeA的完整接口(所有公共方法都不少于),以便可以在需要TypeA的情况下使用TypeB? 表示继承

例如塞斯纳双翼飞机将会暴露飞机的完整界面,如果不是更多的话。 所以这使它适合从飞机派生。

  • TypeB是否只需要TypeA暴露的部分/部分行为? 表示需要合成。

例如,一只鸟可能只需要一架飞机的飞行行为。 在这种情况下,将它作为接口/类/两者提取出来并使其成为这两个类的成员是有意义的。

更新:刚回到我的答案,现在看来,如果没有具体提到芭芭拉·利斯科夫的Liskov替代原则 ,它是不完整的,作为'我应该继承这种类型吗?'的测试。


A simple way to make sense of this would be that inheritance should be used when you need an object of your class to have the same interface as its parent class, so that it can thereby be treated as an object of the parent class (upcasting). Moreover, function calls on a derived class object would remain the same everywhere in code, but the specific method to call would be determined at runtime (ie the low-level implementation differs, the high-level interface remains the same).

Composition should be used when you do not need the new class to have the same interface, ie you wish to conceal certain aspects of the class' implementation which the user of that class need not know about. So composition is more in the way of supporting encapsulation (ie concealing the implementation) while inheritance is meant to support abstraction (ie providing a simplified representation of something, in this case the same interface for a range of types with different internals).


As many people told, I will first start with the check - whether there exists an "is-a" relationship. If it exists I usually check the following:

Whether the base class can be instantiated. That is, whether the base class can be non-abstract. If it can be non-abstract I usually prefer composition

Eg 1. Accountant is an Employee. But I will not use inheritance because a Employee object can be instantiated.

Eg 2. Book is a SellingItem. A SellingItem cannot be instantiated - it is abstract concept. Hence I will use inheritacne. The SellingItem is an abstract base class (or interface in C#)

What do you think about this approach?

Also, I support @anon answer in Why use inheritance at all?

The main reason for using inheritance is not as a form of composition - it is so you can get polymorphic behaviour. If you don't need polymorphism, you probably should not be using inheritance.

@MatthieuM. says in https://softwareengineering.stackexchange.com/questions/12439/code-smell-inheritance-abuse/12448#comment303759_12448

The issue with inheritance is that it can be used for two orthogonal purposes:

interface (for polymorphism)

implementation (for code reuse)

参考

  1. Which class design is better?
  2. Inheritance vs. Aggregation

Composition v/s Inheritance is a wide subject. There is no real answer for what is better as I think it all depends on the design of the system.

Generally type of relationship between object provide better information to choose one of them.

If relation type is "IS-A" relation then Inheritance is better approach. otherwise relation type is "HAS-A" relation then composition will better approach.

Its totally depend on entity relationship.


Even though Composition is preferred, I would like to highlight pros of Inheritance and cons of Composition .

Pros of Inheritance:

  1. It establishes a logical " IS A" relation. If Car and Truck are two types of Vehicle ( base class), child class IS A base class.

    Car is a Vehicle

    Truck is a Vehicle

  2. With inheritance, you can define/modify/extend a capability

    1. Base class provides no implementation and sub-class has to override complete method (abstract) => You can implement a contract
    2. Base class provides default implementation and sub-class can change the behaviour => You can re-define contract
    3. Sub-class adds extension to base class implementation by calling super.methodName() as first statement => You can extend a contract
    4. Base class defines structure of the algorithm and sub-class will override a part of algorithm => You can implement Template_method without change in base class skeleton

Cons of Composition:

  1. In inheritance, subclass can directly invoke base class method even though it's not implementing base class method because of IS A relation. If you use composition, you have to add methods in container class to expose contained class API

eg If Car contains Vehicle and if you have to get price of the Car , which has been defined in Vehicle , your code will be like this

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

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

Inheritance is a very powerfull machanism for code reuse. But needs to be used properly. I would say that inheritance is used correctly if the subclass is also a subtype of the parent class. As mentioned above, the Liskov Substitution Principle is the key point here.

Subclass is not the same as subtype. You might create subclasses that are not subtypes (and this is when you should use composition). To understand what a subtype is, lets start giving an explanation of what a type is.

When we say that the number 5 is of type integer, we are stating that 5 belongs to a set of possible values (as an example, see the possible values for the Java primitive types). We are also stating that there is a valid set of methods I can perform on the value like addition and subtraction. And finally we are stating that there are a set of properties that are always satisfied, for example, if I add the values 3 and 5, I will get 8 as a result.

To give another example, think about the abstract data types, Set of integers and List of integers, the values they can hold are restricted to integers. They both support a set of methods, like add(newValue) and size(). And they both have different properties (class invariant), Sets does not allow duplicates while List does allow duplicates (of course there are other properties that they both satisfy).

Subtype is also a type, which has a relation to another type, called parent type (or supertype). The subtype must satisfy the features (values, methods and properties) of the parent type. The relation means that in any context where the supertype is expected, it can be substitutable by a subtype, without affecting the behaviour of the execution. Let's go to see some code to exemplify what I'm saying. Suppose I write a List of integers (in some sort of pseudo language):

class List {
  data = new Array();

  Integer size() {
    return data.length;
  }

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

Then, I write the Set of integers as a subclass of the List of integers:

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

Our Set of integers class is a subclass of List of Integers, but is not a subtype, due to it is not satisfying all the features of the List class. The values, and the signature of the methods are satisfied but the properties are not. The behaviour of the add(Integer) method has been clearly changed, not preserving the properties of the parent type. Think from the point of view of the client of your classes. They might receive a Set of integers where a List of integers is expected. The client might want to add a value and get that value added to the List even if that value already exist in the List. But her wont get that behaviour if the value exists. A big suprise for her!

This is a classic example of an improper use of inheritance. Use composition in this case.

(a fragment from: use inheritance properly ).


Subtyping is appropriate and more powerful where the invariants can be enumerated , else use function composition for extensibility.


When you have an is-a relation between two classes (example dog is a canine), you go for inheritance.

On the other hand when you have has-a or some adjective relationship between two classes (student has courses) or (teacher studies courses), you chose composition.



另一个非常实用的理由是,偏好合成而不是继承,这与你的领域模型有关,并将其映射到关系数据库。 将继承映射到SQL模型非常困难(您最终会遇到各种各样的变通办法,比如创建并不总是使用的列,使用视图等)。 一些ORML试图解决这个问题,但它总是很快变得复杂。 组合可以通过两个表之间的外键关系轻松建模,但继承难得多。


在Java或C#中,一旦对象被实例化,对象就不能改变它的类型。

所以,如果你的对象需要作为一个不同的对象出现,或者根据对象状态或条件的不同而表现不同,那么使用Composition :参考StateStrategy设计模式。

如果对象需要具有相同的类型,则使用继承或实现接口。


如果你了解其中的差异,则更容易解释。

程序代码

一个例子是没有使用类的PHP(特别是在PHP5之前)。 所有的逻辑都被编码成一组函数。 您可以包含其他包含助手函数等的文件,并通过在函数中传递数据来执行业务逻辑。 随着应用程序的增长,这可能非常难以管理。 PHP5试图通过提供更多面向对象的设计来弥补这一点。

遗产

这鼓励使用类。 继承是面向对象设计的三个原则之一(继承,多态,封装)。

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

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

这是工作中的继承。 员工“是”人或继承人。 所有的继承关系都是“是一种”关系。 员工还会从Person中隐藏Title属性,这意味着Employee.Title将为Employee而非Person返回Title。

组成

构成比继承更受青睐。 简单地说,你会有:

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怎么办? Manager.Title应该返回“运营经理”还是“Mr.”? 在构图下,这种模糊性得到更好的处理:

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

管理器对象由员工和人员组成。 标题行为取自员工。 这种明确的组合消除了其他内容中的歧义,并且您会遇到更少的错误。


就我个人而言,我学会了总是比继承更喜欢构图。 没有程序上的问题,你可以通过继承来解决,你不能用组合来解决; 尽管在某些情况下您可能不得不使用接口(Java)或协议(Obj-C)。 由于C ++不知道任何这样的事情,你必须使用抽象基类,这意味着你不能完全摆脱C ++中的继承。

组合通常更合乎逻辑,它提供了更好的抽象,更好的封装,更好的代码重用(特别是在非常大的项目中),并且不太可能因为您在代码中的任何地方进行了单独的更改而在远处破坏任何东西。 它还使维护“ 单一责任原则 ”变得更加容易,这个原则经常被概括为“ 一个阶级改变不应该有一个以上的原因 ”,这意味着每一个阶级都有特定的目的,它应该只有与其目的直接相关的方法。 此外,使用非常浅的继承树,即使您的项目开始变得非常庞大,也可以更轻松地保持概览。 许多人认为继承代表我们的现实世界相当好,但事实并非如此。 现实世界比继承使用更多的构图。 几乎每个可以握在手中的真实世界对象都是由其他较小的真实世界对象组成的。

不过,有构成的缺点。 如果完全忽略继承,只关注构图,那么您会注意到,如果您使用了继承,则通常必须编写一些不必要的额外代码行。 你有时也被迫重复自己,这违反了DRY原则 (DRY =不要重复自己)。 另外组合通常需要委托,并且一个方法只是调用另一个对象的另一个方法,而没有其他代码围绕此调用。 这种“双方法调用”(可能很容易扩展到三重或四重方法调用,甚至比这更远)的性能比继承性能差得多,您只需继承父级方法即可。 调用继承的方法可能与调用非继承的方法的速度相同,也可能稍微慢一点,但通常比两个连续的方法调用仍快。

您可能已经注意到大多数OO语言不允许多重继承。 虽然有几种情况下多重继承可以真正为您购买某些东西,但这些都不是规则的例外。 当你遇到一种情况时,你认为“多重继承将是一个非常酷的功能来解决这个问题”,通常你应该重新考虑继承,因为即使它可能需要一些额外的代码行,基于构图的解决方案通常会变得更加优雅,灵活和面向未来。

继承是一个很酷的功能,但是恐怕过去几年它已经被滥用了。 人们把继承看作是可以钉住一切的一把锤子,不管它是钉子,螺丝钉,还是完全不同的东西。


我的一般经验法则: 在使用继承之前,请考虑组合是否更有意义。

原因: 子类化通常意味着更多的复杂性和连通性,即难以改变,维护和扩展而不会犯错误。

Sun的Tim Boudreau提供了更加完整和具体的答案

我所看到的使用继承的常见问题是:

  • 无辜的行为可能会产生意想不到的结果 - 这是一个典型的例子,在子类实例字段被初始化之前,调用来自超类构造函数的可重写方法。 在一个完美的世界里,没有人会这样做。 这不是一个完美的世界。
  • 它为子分类器提供了对方法调用顺序的假设的不正当诱惑 - 如果超类随着时间推移可能演变,这种假设往往不稳定。 另见我的烤面包机和咖啡壶比喻
  • 类越来越重 - 你不一定知道你的超类在构造函数中做了什么,或者它将使用多少内存。 因此,构建一些无辜的轻量级对象可能比您想象的要昂贵得多,而且如果超类发展,这可能会随着时间而改变
  • 它鼓励子类的爆炸 。 课堂上课需要花费时间,更多班级需要记忆。 在您处理NetBeans规模的应用程序之前,这可能不是问题,但在那里,我们遇到了实际问题,例如菜单速度较慢,因为菜单的第一次显示会触发大量的类加载。 我们通过转向更多的声明性语法和其他技术来解决这个问题,但这也需要花费时间来解决。
  • 这会让事情变得更加困难 - 如果你公开创建了一个类,那么交换超类将会破坏子类 - 这是一种选择,一旦你公开代码,你就结婚了。 所以如果你没有改变你的超类的真正功能,那么如果你使用它,你就有更多的自由来改变它,而不是扩展你需要的东西。 举个例子,继承JPanel - 这通常是错误的; 如果子类在某个地方是公开的,那么你永远不会有机会重新审视这个决定。 如果以JComponent getThePanel()方式访问它,您仍然可以执行此操作(提示:将组件的模型作为API展示给您)。
  • 对象层次结构不能缩放(或者比以后进行缩放要困难得多) - 这是经典的“太多层”问题。 我将在下面介绍这一点,以及AskTheOracle模式如何解决它(尽管它可能会冒犯OOP纯粹主义者)。

...

如果你确实允许继承,那么你可能会用一粒盐来承担如下事情:

  • 除常量外,不会公开任何字段
  • 方法应是抽象的或最终的
  • 不要调用超类构造函数中的方法

...

所有这些对小项目的适用比对大项目适用的程度要低,对私人课程的适用程度低于对公共方面的适用


所有现有的答案都很好的解释了为什么组合往往比继承更可取。 但是,确实存在继承更合适的情况,所以有一个很好的“测试”来知道我们是否处于这种情况是很重要的。

Circle-Ellipse问题很好地说明了为什么“is-a”实际上不是检测何时使用继承的好测试 。 事实上, 即使一个圆是一个椭圆 ,但让Circle类继承Ellipse类是一个坏主意,因为有些事情可以用椭圆形,但圆形不能。 例如,椭圆可以伸展,但圆圈不能。 所以你可以使用ellipse.stretch() ,但不能使用circle.stretch()

我认为一个更好的测试是“是 - 一个”并且“具有多功能性” 。 换句话说,只要子类可以多态使用,最好使用继承。 “is-a”关系是多态使用的必要条件,但这是不够的。 由超类提供的每个功能也可以由子类提供也是非常重要的。 所以当你有一个“是某种”另一个类的类,但是增加了一些约束时,你就不应该使用继承,因为它不能被多态使用。

换句话说, 继承不是关于共享属性,而是关于共享功能 。 通常情况下,“is-a”作为一个测试,测试超类的所有getter是否在子类中有意义,而“has-as-much-functional-as”用作测试超类的所有setter是否在子类中有意义。 获取者和设置者共同定义一个类的“功能”。 吸气人员只能定义一个班级的“属性”。 通过“setter”,我的意思是任何改变对象状态的函数,而不仅仅是微不足道的函数。

其他答案/评论几乎都说了同样的事情,但我觉得我的观点增加了一些清晰度。


由于继承提供了所有不可否认的好处,下面是它的一些缺点。

继承的缺点:

  1. 您不能在运行时更改从超类继承的实现(显然是因为在编译时定义了继承)。
  2. 继承公开了一个子类的父类的实现细节,这就是为什么经常说继承破坏封装的原因(从某种意义上说,你确实需要专注于接口而不是实现,所以通过子类再次使用并不总是首选)。
  3. 继承提供的紧密耦合使得子类的实现与超类的实现紧密相关,父类实现中的任何更改都会强制子类发生更改。
  4. 通过子分类过度重复使用可能会使继承堆栈变得非常深和非常混乱。

另一方面, 对象组合是在运行时通过获取对其他对象的引用的对象来定义的。 在这种情况下,这些对象将永远无法访问彼此的受保护数据(无封装中断),并且将被迫相互尊重对方的接口。 而且在这种情况下,实现依赖关系比继承的情况要少很多。


简而言之,我会同意“比继承更喜欢构图”,对我来说,这听起来像“比可口可乐更喜欢马铃薯”。 有继承的地方和组成的地方。 你需要了解差异,那么这个问题就会消失。 对我来说真正意义的是“如果你打算使用继承 - 再想一想,你很可能需要组合”。

当你想要吃东西时,你应该更喜欢土豆比可口可乐,而当你想要喝时,你应该更喜欢马铃薯可口可乐。

创建子类应该不仅仅是一种调用超类方法的简便方法。 当子类“is-a”在结构上和功能上都是超类的时候,你应该使用继承,当它可以作为超类使用时,你将使用它。 如果不是这种情况 - 这不是继承,而是另一回事。 构图是当你的对象由另一个构成时,或者与它们有某种关系。

所以对我而言,如果有人不知道他是否需要继承或组合,真正的问题是他不知道他是否想喝酒或吃东西。 更多地考虑你的问题领域,更好地理解它。


继承是非常强大的,但是你不能强制它(见: 圆椭圆问题 )。 如果你真的不能完全确定一个真正的“是 - 一个”亚型关系,那么最好还是写作。


继承是非常诱人的,特别是来自程序性的土地,它看起来往往看起来很优雅。 我的意思是我所需要做的就是将这一点功能添加到其他类中,对吧? 那么,其中一个问题就是这样

继承可能是你可以拥有的最糟糕的耦合形式

您的基类通过将实现细节以受保护成员的形式公开给子类来打破封装。 这会让你的系统变得僵硬脆弱。 然而,更悲惨的缺陷是新的子类带来了继承链的所有包袱和意见。

文章“ 继承是邪恶的:DataAnnotationsModelBinder史诗般的失败” ,在C#中演示了这个例子。 它显示了在使用组合时以及如何重构时继承的用法。







aggregation