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


当前回答

在Java或C#中,对象一旦实例化就不能更改其类型。

因此,如果您的对象需要显示为不同的对象或根据对象的状态或条件表现出不同的行为,那么请使用组合:参考状态和策略设计模式。

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

其他回答

我同意@帕维尔的说法,他说,有地方可以组成,也有地方可以继承。

我认为如果你的答案是肯定的,那么应该使用继承。

您的类是从多态性中受益的结构的一部分吗?例如,如果您有一个Shape类,它声明了一个名为draw()的方法,那么我们显然需要Circle和Square类作为Shape的子类,这样它们的客户端类将依赖于Shape而不是特定的子类。您的类是否需要重用其他类中定义的任何高级交互?如果没有继承,模板方法设计模式将不可能实现。我相信所有可扩展框架都使用这种模式。

然而,如果您的意图纯粹是代码重用,那么组合很可能是更好的设计选择。

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

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

如果你想得到自OOP兴起以来人们一直给出的规范的教科书式答案(你可以看到很多人在这些答案中给出了答案),那么应用以下规则:“如果你有一个is-a关系,那么使用继承。如果你有个has-a关系,则使用组合”。

这是传统的建议,如果这让你满意,你可以停止阅读,继续你的快乐之路。对于其他人。。。

is-a/has-a比较存在问题

例如:

正方形是矩形,但如果矩形类具有setWidth()/setHeight()方法,那么没有合理的方法可以在不违反Liskov替换原则的情况下使正方形继承自矩形。is-a关系通常可以被改写为听起来像has-a关系。例如,员工是一个人,但一个人也具有“已就业”的就业状态。如果不小心,is-a关系可能会导致讨厌的多重继承层次结构。毕竟,英语中没有一条规则规定一个对象是一件事情。人们很快就把这条“规则”传递出去了,但有没有人试图支持它,或者解释为什么这是一个很好的启发式方法?当然,这很符合OOP应该模拟真实世界的想法,但这本身并不是采用原则的理由。

有关此主题的更多信息,请参阅StackOverflow问题。

要知道何时使用继承与组合,我们首先需要了解每种方法的利弊。

实现继承的问题

其他答案在解释继承问题方面做了很好的工作,所以我在这里尽量不深入太多细节。但是,这里有一个简短的列表:

很难遵循在基类和子类方法之间交织的逻辑。通过调用另一个可重写的方法在类中不小心地实现一个方法将导致您泄漏实现细节并破坏封装,因为最终用户可能会重写您的方法并检测您何时在内部调用它。(请参阅“有效Java”第18项)。脆弱的基础问题,简单地说,如果最终用户的代码在您尝试更改它们时碰巧依赖于实现细节的泄漏,那么它们就会损坏。更糟糕的是,大多数OOP语言默认允许继承——API设计者如果没有主动阻止人们从公共类继承,那么在重构基类时就需要格外谨慎。不幸的是,脆弱的基础问题经常被误解,导致许多人不理解维护任何人都可以继承的类需要什么。致命的死亡钻石

构图的问题

有时可能有点冗长。

就是这样,我是认真的。这仍然是一个真正的问题,有时会与DRY原则产生冲突,但一般来说,这并没有那么糟糕,至少与继承相关的无数陷阱相比。

什么时候应该使用继承?

下次当你为一个项目绘制你的UML图时(如果你这样做的话),并且你在考虑添加一些继承时,请遵循以下建议:不要。

至少目前还没有。

继承是作为实现多态性的工具出售的,但与它捆绑在一起的是这个强大的代码重用系统,坦率地说,大多数代码都不需要。问题是,一旦你公开了你的继承层次结构,你就被锁定在这种特定的代码重用风格中,即使这对于解决你的特定问题来说是过度的。

为了避免这种情况,我的两分钱是永远不要公开公开你的基类。

如果需要多态性,请使用接口。如果你需要允许人们自定义你的类的行为,通过策略模式提供显式的连接点,这是一种更易读的方式来实现这一点,另外,保持这种API的稳定性更容易,因为你完全可以控制他们可以改变和不能改变的行为。如果您试图通过使用继承来遵循开放-封闭原则,以避免向类添加急需的更新,那么就不要这样做。更新类。如果你真的拥有被雇佣来维护的代码,而不是试图在代码中添加一些东西,那么你的代码库就会更干净。如果你害怕引入bug,那么就对现有代码进行测试。如果需要重用代码,首先尝试使用组合或帮助函数。

最后,如果您决定没有其他好的选择,并且必须使用继承来实现所需的代码重用,那么您可以使用它,但是,请遵循P.A.I.L.限制继承的四条规则,以保持其正常。

将继承用作私有实现细节。不要公开您的基类,请为此使用接口。这使您可以根据自己的需要自由添加或删除继承,而无需进行重大更改。保持基类抽象。这使得将需要共享的逻辑与不共享的逻辑区分开来更加容易。隔离基础类和子类。不要让子类覆盖基类方法(使用策略模式),避免让它们期望财产/方法相互存在,使用其他形式的代码共享来实现这一点。使用适当的语言特性强制基类上的所有方法不可重写(在Java中为“final”,在C#中为非虚拟)。继承是最后的手段。

特别是“隔离”规则可能听起来有点难理解,但如果你自律,你会得到一些很好的好处。特别是,它让您可以自由地避免上面提到的与继承相关的所有主要令人讨厌的陷阱。

遵循代码要容易得多,因为它不会在基类/子类之间来回穿梭。如果您从未使任何方法可重写,则当您的方法在内部调用其他可重写方法时,不会意外泄漏。换句话说,您不会意外地破坏封装。脆弱的基类问题源于依赖意外泄漏的实现细节的能力。由于基类现在是孤立的,所以它不会比依赖于另一个via组合的类更脆弱。致命的死亡钻石不再是一个问题,因为根本不需要有多层继承。如果你有抽象基类B和C,它们都共享很多功能,只需将该功能从B和C中移出,并移入一个新的抽象基类类D。从B继承的任何人都应该更新为从B和D继承,从C继承的任何人均应该从C和D继承。由于你的基类都是私有实现细节,要弄清楚谁继承了什么,做出这些改变应该不会太难。

结论

我的主要建议是在这件事上动动脑筋。比什么时候使用继承更重要的是对继承及其相关利弊的直观理解,以及对其他可以用来代替继承的工具的良好理解(组合并不是唯一的选择。例如,策略模式是一个令人惊叹的工具,它经常被遗忘)。也许当您对所有这些工具都有了很好的、扎实的理解后,您会选择比我推荐的更频繁地使用继承,这是完全可以的。至少,您正在做出明智的决定,而不仅仅是使用继承,因为这是您知道如何做到这一点的唯一方法。

进一步阅读:

我写了一篇关于这一主题的文章,深入探讨并提供了示例。一个网页,讲述了继承所做的三种不同的工作,以及如何通过Go语言中的其他方式完成这些工作。将类声明为不可继承(例如,Java中的“final”)是很好的原因列表。乔舒亚·布洛赫(Joshua Bloch)的《有效的Java》(Effective Java)一书,第18项,讨论了组合而非继承,以及继承的一些危险。

理解这一点的一个简单方法是,当您需要类的对象与其父类具有相同的接口时,应该使用继承,这样它就可以被视为父类的对象(上广播)。此外,派生类对象上的函数调用在代码中的任何地方都保持不变,但要调用的特定方法将在运行时确定(即,低级实现不同,高级接口保持不变)。

当您不需要新类具有相同的接口时,即您希望隐藏该类的用户不需要知道的类实现的某些方面时,应使用组合。因此,组合更多的是支持封装(即隐藏实现),而继承意味着支持抽象(即提供某种东西的简化表示,在这种情况下,相同的接口用于具有不同内部结构的一系列类型)。

另一个非常实用的原因是,倾向于组合而不是继承,这与域模型以及将其映射到关系数据库有关。将继承映射到SQL模型真的很难(你最终会遇到各种棘手的变通方法,比如创建不经常使用的列、使用视图等)。一些ORML试图处理这个问题,但它总是很快变得复杂。通过两个表之间的外键关系可以很容易地对组合进行建模,但继承要困难得多。