为什么喜欢构图而不是继承呢?每种方法都有哪些权衡取舍?什么时候应该选择继承而不是作文?
首选组合而不是继承,因为它更具有延展性/易于修改,但不使用compose-always方法。通过组合,可以使用依赖注入/设置器轻松更改行为。继承更加严格,因为大多数语言不允许您从多个类型派生。因此,一旦你从TypeA派生,鹅就会或多或少地煮熟。
我对上述的酸测试是:
例如如果不是更多的话,塞斯纳双翼飞机将暴露飞机的完整界面。因此,它适合从飞机派生。
例如鸟可能只需要飞机的飞行行为。在这种情况下,将其作为接口/类/两者提取出来并使其成为两个类的成员是有意义的。
更新:刚回到我的答案,现在看来,如果不特别提及Barbara Liskov的Liskov Substitution Principle作为'我是否应该从这种类型继承?'的测试,它是不完整的。
这里没有找到满意的答案,所以我写了一个新的。
要理解为什么“更喜欢构图而不是继承”,我们首先要回到这个缩短的习语中省略的假设。
继承有两个好处:subtyping and subclassing
这两个好处有两个不同的目的:继承:面向子类型和面向代码重用。
如果代码重用是唯一的目的,那么子类化可能比他需要的更多,即父类的一些公共方法对子类没有多大意义。在这种情况下,不需要使组合优于继承,而是要求组合。这也是“is-a”与“has-a”概念的来源。
因此,只有在有意使用子类型时,即稍后以多态方式使用新类时,我们才会面临选择继承或组合的问题。这是在讨论中缩短的习语中省略的假设。
要使子类型符合类型签名,这意味着组合总是暴露出同样数量的API类型。现在,权衡取舍:
this
,即在另一个成员函数中调用重写方法(甚至type),无论是公共的还是私有的(尽管discouraged)。开放递归可以是simulated via composition,但它需要额外的努力,可能并不总是可行(?)。这个answer到一个重复的问题谈论类似的东西。考虑到上述权衡,我们因此更喜欢构成而不是继承。然而,对于紧密相关的类,即当隐式代码重用确实带来好处,或者需要开放递归的神奇力量时,继承应该是选择。
我的一般经验法则:在使用继承之前,请考虑组合是否更有意义。
原因:子类化通常意味着更多的复杂性和连通性,即更难以改变,维护和扩展而不会出错。
一个更加完整和具体的太阳answer from Tim Boudreau:
我认为使用继承的常见问题是:
- 无辜的行为可能会产生意想不到的结果 - 这个经典的例子是在初始化子类实例字段之前调用超类构造函数中的可覆盖方法。在一个完美的世界里,没有人会这样做。这不是一个完美的世界。
- 它为子类提供了有关方法调用顺序的假设的反常诱惑 - 如果超类可能随着时间的推移而演变,这种假设往往不稳定。另见my toaster and coffee pot analogy。
- 类越来越重 - 你不一定知道你的超类在它的构造函数中做了什么工作,或者它将要使用多少内存。所以构建一些无辜的可能轻量级对象可能比你想象的要贵得多,如果超类演变,这可能会随着时间的推移而改变
- 它鼓励子类的爆炸。类加载成本时间,更多类成本内存。在您处理NetBeans规模的应用程序之前,这可能不是问题,但在那里,我们遇到了一些实际问题,例如菜单很慢,因为菜单的第一次显示触发了大量的类加载。我们通过转向更具声明性的语法和其他技术来解决这个问题,但这也需要花费时间来修复。
- 这使得以后更改内容变得更加困难 - 如果你已经公开了一个类,那么交换超类就会破坏子类 - 这是一个选择,一旦你公开了代码,你就会嫁给它。因此,如果您没有改变超类的真实功能,那么如果您使用,您可以更自由地更改内容,而不是扩展您需要的内容。举例来说,继承JPanel - 这通常是错误的;如果子类在某个地方公开,你永远不会有机会重新审视这个决定。如果它作为JComponent getThePanel()访问,您仍然可以这样做(提示:在API中公开组件的模型)。
- 对象层次结构不能扩展(或者使它们后期扩展比计划更难) - 这是典型的“太多层”问题。我将在下面讨论,以及AskTheOracle模式如何解决它(虽然它可能冒犯OOP纯粹主义者)。
...
如果你允许遗传,我可以采取的做法是:
- 除了常数之外,永远不会暴露任何字段
- 方法应该是抽象的或最终的
- 从超类构造函数中调用no方法
...
所有这些对小型项目的适用性要小于大型项目,对私人类别的应用要少于公共项目
看到其他答案。
人们常说,当下面的句子为真时,一个类Bar
可以继承一个类Foo
:
- 酒吧是一个foo
不幸的是,仅凭上述测试并不可靠。请改用以下内容:
- 酒吧是foo,AND
- 酒吧可以做食物可以做的一切。
第一个测试确保Foo
的所有getter在Bar
(=共享属性)中有意义,而第二个测试确保所有Foo
的setter在Bar
(=共享功能)中有意义。
例1:
狗是动物,狗可以做动物可以做的一切(如呼吸,死亡等)。因此,类Dog
可以继承类Animal
。
例2:
圆是一个椭圆但是椭圆可以做的事情和圆不能(例如拉伸)。因此,类Circle
不能继承类Ellipse
。
这被称为Circle-Ellipse problem,这确实不是问题,只是一个明确的证据,单独的第一个测试不足以得出继承是可能的结论。特别是,此示例强调派生类应扩展基类的功能,而不是限制它。否则,基类不能以多态方式使用。
即使你可以使用继承并不意味着你应该:使用组合总是一个选项。继承是一个强大的工具,允许隐式代码重用和动态调度,但它确实带来一些缺点,这就是为什么组合通常是首选。继承与构成之间的权衡并不明显,我认为最好在lcn's answer中解释。
根据经验,当多态使用预计非常普遍时,我倾向于选择继承而不是组合,在这种情况下,动态调度的强大功能可以带来更加可读和优雅的API。例如,在GUI框架中具有多态类Widget
,或在XML库中具有多态类Node
允许使用比纯粹基于组合的解决方案更具可读性和直观性的API。
您知道,用于确定继承是否可行的另一种方法称为Liskov Substitution Principle:
使用指针或对基类的引用的函数必须能够在不知道它的情况下使用派生类的对象
从本质上讲,这意味着如果基类可以多态使用,那么继承就是可能的,我认为这相当于我们的测试“条形图是一个foo,条形图可以完成foos可以做的所有事情”。
继承是非常强大的,但你不能强迫它(参见:circle-ellipse problem)。如果你真的不能完全确定一个真正的“is-a”子类型关系,那么最好选择合成。
继承在子类和超类之间创建了一种强大的关系;子类必须知道超类的实现细节。当你必须考虑如何扩展它时,创建超级类会更加困难。您必须仔细记录类不变量,并说明其他方法可覆盖的方法在内部使用的内容。
如果层次结构实际上表示is-a-relationship,则继承有时很有用。它涉及开放 - 封闭原则,它规定类应该被关闭以进行修改但是可以扩展。这样你就可以拥有多态性;有一个处理超类型及其方法的泛型方法,但是通过动态调度,可以调用子类的方法。这是灵活的,并且有助于创建间接性,这在软件中是必不可少的(更少了解实现细节)。
但是,继承很容易被过度使用,并且会产生额外的复杂性,并且类之间存在硬依赖关系。由于层和动态选择方法调用,理解在执行程序期间发生的事情变得相当困难。
我建议使用composing作为默认值。它更模块化,并提供后期绑定的好处(您可以动态更改组件)。此外,单独测试这些东西也更容易。如果您需要使用类中的方法,则不必强制使用某种形式(Liskov Substitution Principle)。
假设飞机只有两个部分:发动机和机翼。 然后有两种方法来设计飞机类。
Class Aircraft extends Engine{
var wings;
}
现在你的飞机可以从固定的机翼开始 并在飞行中将它们改为旋转翅膀。它本质上是 带翅膀的发动机。但如果我想改变怎么办? 飞机上的引擎也是?
基类Engine
暴露一个mutator来改变它
属性,或者我重新设计Aircraft
为:
Class Aircraft {
var wings;
var engine;
}
现在,我也可以动态更换我的发动机。
你需要看看鲍勃叔叔的The Liskov Substitution Principle课堂设计原则中的SOLID。 :)
当您想要“复制”/公开基类的API时,您可以使用继承。如果您只想“复制”功能,请使用委托。
例如:您想要从列表中创建堆栈。堆栈只有pop,push和peek。鉴于您不希望在堆栈中使用push_back,push_front,removeAt等等功能,因此不应使用继承。
这两种方式可以很好地共存,实际上相互支持。
组合就是模块化:你创建类似于父类的接口,创建新对象并委托对它的调用。如果这些对象不需要彼此了解,那么使用组合物是非常安全和容易的。这里有很多可能性。
但是,如果父类由于某种原因需要访问“子类”为没有经验的程序员提供的功能,那么它可能看起来像是一个使用继承的好地方。父类可以只调用它自己的抽象“foo()”,它被子类覆盖,然后它可以将值赋给抽象基。
它看起来是个好主意,但在很多情况下,最好只给类一个实现foo()的对象(或者甚至设置手动提供foo()的值,而不是从一些基类继承新类,这需要要指定的函数foo()。
为什么?
因为继承是一种移动信息的不良方式。
该组合在这里有一个真正的优势:关系可以颠倒:“父类”或“抽象工作者”可以聚合任何特定的“子”对象实现某些接口+任何子类都可以设置在任何其他类型的父类中,它接受它的类型。并且可以有任意数量的对象,例如MergeSort或QuickSort可以对实现抽象比较接口的任何对象列表进行排序。或者换句话说:实现“foo()”的任何一组对象和可以使用具有“foo()”的对象的其他对象组可以一起玩。
我可以想到使用继承的三个真正原因:
如果这些都是真的,则可能需要使用继承。
使用原因1没有什么不好,在对象上有一个可靠的界面是非常好的。这可以使用组合或继承来完成,没问题 - 如果这个界面很简单而且不会改变。通常,继承在这里非常有效。
如果原因是2号,那就有点棘手了。你真的只需要使用相同的基类吗?通常,仅使用相同的基类是不够好的,但它可能是您的框架的要求,这是一个无法避免的设计考虑因素。
但是,如果你想使用私有变量,案例3,那么你可能会遇到麻烦。如果您认为全局变量不安全,那么您应该考虑使用继承来访问私有变量也是不安全的。请注意,全局变量并非都不好 - 数据库本质上是一组全局变量。但如果你能处理它,那就很好了。
除了考虑因素之外,还必须考虑对象必须经历的继承“深度”。任何超过五或六级继承的深度都可能导致意外的转换和装箱/拆箱问题,在这些情况下,构建对象可能是明智之举。
将遏制视为一种关系。汽车“有一个”引擎,一个人“有一个”的名字等。
将继承视为一种关系。汽车“是”汽车,人是“哺乳动物”等。
我不相信这种方法。我从Second Edition of Code Complete那里直接从Steve McConnell那里直接拿到了6.3节。
理解这一点的一个简单方法是,当您需要类的对象具有与其父类相同的接口时,应该使用继承,以便可以将其视为父类的对象(向上转换) 。此外,对派生类对象的函数调用在代码中的任何地方都将保持相同,但调用的具体方法将在运行时确定(即,低级实现不同,高级接口保持不变)。
当您不需要新类具有相同的接口时,应该使用组合,即您希望隐藏该类的实现的某些方面,该类的用户不需要知道。因此,组合更多地支持封装(即隐藏实现),而继承意味着支持抽象(即,提供某事物的简化表示,在这种情况下,对于具有不同内部的一系列类型的相同接口)。
如果你有一个两个类之间的is-a关系(例如dog是一个犬),你就去继承。
另一方面,当你在两个班级(学生有课程)或(教师学习课程)之间有一个或一些形容词关系时,你选择了作文。
在invariants can be enumerated中,子类型适当且更强大,否则使用函数组合来实现可扩展性。
我同意@Pavel,当他说,有组成的地方,有继承的地方。
如果您的回答对任何这些问题都是肯定的,我认为应该使用继承。
但是,如果您的意图纯粹是代码重用,那么组合很可能是更好的设计选择。
从新程序员的不同角度解决这个问题:
当我们学习面向对象编程时,通常会尽早传授继承,因此它被视为解决常见问题的简单方法。
我有三个类,都需要一些共同的功能。因此,如果我编写一个基类并让它们都从它继承,那么它们都将具有该功能,我只需要将它保存在一次。
这听起来很棒,但在实践中,它几乎永远不会有效,原因如下:
最后,我们将代码绑在一些困难的结上并从中得不到任何好处,除了我们说:“很酷,我学会了继承,现在我用它了。”这并不意味着居高临下,因为我们都做到了。但我们都这样做是因为没有人告诉我们不要。
一旦有人向我解释“赞成组合而非继承”,我每次尝试使用继承在类之间共享功能时都会重新思考,并意识到大多数时候它并没有真正起作用。
解毒剂是Single Responsibility Principle。把它想象成一种约束。我的班级必须做一件事。我必须能够给我的班级一个名字,以某种方式描述它做的一件事。 (一切都有例外,但是当我们学习时,绝对规则有时会更好。)因此我不能写一个名为ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses
的基类。我需要的任何不同功能必须在它自己的类中,然后需要该功能的其他类可以依赖于该类,而不是从它继承。
冒着过度简化的风险,即组合 - 组成多个类来协同工作。一旦我们形成这种习惯,我们发现它比使用继承更灵活,可维护和可测试。
我听过的经验法则是,当它是“a-a”关系时,应该使用继承,当它是“has-a”时,应该使用它。即便如此,我觉得你应该总是倾向于构图,因为它消除了很多复杂性。
继承是代码重用的一个非常强大的机制。但需要正确使用。我会说如果子类也是父类的子类型,则正确使用继承。如上所述,Liskov替代原则是这里的关键点。
子类与子类型不同。您可以创建不是子类型的子类(这是您应该使用合成的时候)。要了解子类型是什么,让我们开始解释类型是什么。
当我们说数字5是整数类型时,我们声明5属于一组可能的值(作为示例,请参阅Java原始类型的可能值)。我们还指出,我可以对加值和减法等值执行一组有效的方法。最后我们说明有一组总是满足的属性,例如,如果我添加值3和5,我将得到8作为结果。
再举一个例子,考虑抽象数据类型,整数集和整数列表,它们可以容纳的值仅限于整数。它们都支持一组方法,如add(newValue)和size()。并且它们都具有不同的属性(类不变),集合不允许重复,而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);
}
}
}
我们的整数类集是整数列表的子类,但不是子类型,因为它不满足List类的所有功能。满足方法的值和签名,但属性不满足。 add(Integer)方法的行为已经明确更改,而不是保留父类型的属性。从您的课程客户的角度思考。它们可能会收到一组整数,其中需要一个整数列表。客户端可能希望添加值并将该值添加到List中,即使该值已存在于List中。但如果价值存在,她就不会得到那种行为。对她来说太大了!
这是不正确使用继承的典型示例。在这种情况下使用组合。
(片段来自:use inheritance properly)。
组合v / s继承是一个广泛的主题。对于什么是更好的没有真正的答案,因为我认为这一切都取决于系统的设计。
通常,对象之间的关系类型提供了更好的信息来选择其中之一。
如果关系类型是“IS-A”关系,那么继承是更好的方法。否则关系类型是“HAS-A”关系,那么组合将更好地接近。
它完全依赖于实体关系。
正如许多人所说,我将首先从检查开始 - 是否存在“是 - ”关系。如果它存在,我通常检查以下内容:
是否可以实例化基类。也就是说,基类是否可以是非抽象的。如果它可以是非抽象的,我通常更喜欢作文
例如1.会计是员工。但我不会使用继承,因为可以实例化Employee对象。
例如,Book是一个SellingItem。 SellingItem无法实例化 - 它是抽象概念。因此我将使用inheritacne。 SellingItem是一个抽象基类(或C#中的接口)
您如何看待这种方法?
另外,我在Why use inheritance at all?支持@anon回答
使用继承的主要原因不是作为一种组合形式 - 它可以让你获得多态行为。如果您不需要多态,则可能不应该使用继承。
@MatthieuM。在https://softwareengineering.stackexchange.com/questions/12439/code-smell-inheritance-abuse/12448#comment303759_12448说
继承问题是它可以用于两个正交目的:
接口(用于多态)
实现(用于代码重用)
参考
尽管撰写是首选,但我想强调继承的优点和组合的缺点。
继承的优点:
构成要点:
例如如果Car包含Vehicle并且您必须获得已在Vehicle中定义的Car的价格,您的代码将是这样的
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将返回Employee的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的原因。
现在假设您要创建一个Manager类型,最终得到:
class Manager : Person, Employee {
...
}
这个例子可以正常工作,但是,如果Person和Employee都声明了Title
呢? Manager.Title应该返回“运营经理”还是“先生”?在构图下,这种歧义可以更好地处理:
Class Manager {
public string Title;
public Manager(Person p, Employee e)
{
this.Title = e.Title;
}
}
Manager对象由Employee和Person组成。标题行为取自员工。这种明确的组合消除了其他事物的歧义,你会遇到更少的错误。
你想强迫自己(或其他程序员)坚持什么,以及何时你想让自己(或其他程序员)更自由。有人认为,当你想强迫某人处理/解决某个特定问题时,继承是有帮助的,这样他们就不会朝错误的方向前进。
Is-a
和Has-a
是一个有用的经验法则。
由于继承提供了所有无可否认的好处,这里有一些缺点。
继承的缺点:
另一方面,对象组合在运行时通过获取对其他对象的引用的对象来定义。在这种情况下,这些对象将永远无法访问彼此的受保护数据(没有封装中断),并且将被迫尊重彼此的接口。在这种情况下,实现依赖性将比继承的情况少得多。
另一个非常务实的原因是,优先考虑组合而不是继承,这与域模型有关,并将其映射到关系数据库。将继承映射到SQL模型真的很难(最终会遇到各种各样的hacky变通方法,比如创建不常用的列,使用视图等)。一些ORML试图解决这个问题,但它总是很快变得复杂。可以通过两个表之间的外键关系轻松地对组合进行建模,但继承更加困难。
虽然简而言之,我同意“首选组合而非继承”,对我来说,这听起来像“喜欢土豆而不是可口可乐”。有继承的地方和组成的地方。你需要了解差异,然后这个问题就会消失。它对我来说真正意味着“如果你要继承 - 再想想,你可能需要作文”。
当你想吃的时候,你应该更喜欢土豆而不是可口可乐,而当你想喝时,你可以选择土豆可乐。
创建子类不仅仅意味着调用超类方法的方便方法。当子类“is-a”超类在结构和功能上都应该使用继承,当它可以用作超类时,你将使用它。如果不是这种情况 - 它不是继承,而是其他东西。组合是指您的对象由另一个组成,或与它们有某种关系。
所以对我来说,看起来如果有人不知道他是否需要继承或组成,真正的问题是他不知道他是想喝酒还是吃饭。更多地考虑您的问题域,更好地理解它。
继承是非常诱人的,特别是来自程序性的土地,它往往看起来很优雅。我的意思是我需要做的就是将这一点功能添加到其他类中,对吧?嗯,其中一个问题是
您的基类通过以受保护成员的形式将实现细节暴露给子类来打破封装。这使您的系统变得僵硬和脆弱。然而,更具悲剧性的缺陷是新的子类带来了继承链的所有包袱和意见。
文章Inheritance is Evil: The Epic Fail of the DataAnnotationsModelBinder在C#中介绍了这个例子。它显示了在应该使用组合时如何使用继承以及如何重构它。
就个人而言,我学会了总是喜欢构图而不是继承。你可以用继承来解决没有程序问题,你无法用组合来解决这个问题。虽然在某些情况下您可能必须使用接口(Java)或协议(Obj-C)。由于C ++不知道任何这样的东西,你将不得不使用抽象基类,这意味着你不能完全摆脱C ++中的继承。
组合通常更符合逻辑,它提供更好的抽象,更好的封装,更好的代码重用(特别是在非常大的项目中),并且不太可能在远处破坏任何东西只是因为您在代码中的任何位置进行了单独的更改。它还使得更容易坚持“单一责任原则”,这通常被概括为“一个类不应该有一个以上的理由来改变。”,这意味着每个类都存在于特定目的,它应该只有与其目的直接相关的方法。即使项目开始变得非常庞大,使用非常浅的继承树也可以更容易地保持概览。许多人认为继承代表了我们的现实世界,但事实并非如此。现实世界使用的组合比继承更多。你可以握在手中的每一个真实物体都是由其他较小的现实世界物体组成的。
但是,构图有缺点。如果你完全跳过继承而只关注组合,你会注意到你经常需要编写一些额外的代码行,如果你使用了继承则不需要这些代码行。你有时也被迫重复自己,这违反了DRY原则(DRY =不要重复自己)。组合通常也需要委托,而方法只是调用另一个对象的另一个方法而没有围绕此调用的其他代码。这种“双方法调用”(可能很容易扩展到三次或四次方法调用,甚至比这更远)的性能比继承要差得多,在继承中只需继承父级的方法。调用一个继承的方法可能与调用一个非继承的方法同样快,或者它可能稍慢,但通常仍然比两个连续的方法调用快。
您可能已经注意到大多数OO语言不允许多重继承。虽然有几种情况下多重继承可以真正为你买点东西,但这些只是规则的例外。每当你遇到你认为“多重继承将是解决这个问题的一个非常酷的功能”的情况时,你通常都应该重新考虑继承,因为即使它可能需要一些额外的代码行基于构图的解决方案通常会变得更加优雅,灵活和面向未来。
继承真的是一个很酷的功能,但我担心它在过去几年里被滥用了。人们将遗传视为可以钉住所有东西的一把锤子,无论它实际上是钉子,螺钉还是完全不同的东西。