对象组合优于继承
为何偏爱对象组合而非继承
引言
从面向对象编程的早期开始,就存在着使用(甚至滥用)继承的做法。许多开发人员喜欢定义一个抽象类,其中常见的代码在基类中实现为虚拟方法。这样做的主要原因是可以使用重写功能来修改行为,并支持代码重用。但这样做会随着时间的推移使代码变得脆弱,因为基类会拥有越来越多的虚拟方法,而这些方法可能并不是其所有派生类都需要。
在下面的文章中,我们将首先探讨在类设计中进行组合的含义。我们将看到组合如何避免“对象污染问题”,这是不正确继承的副产物。本文解释了组合在 WPF 应用程序中的使用,但也可用于其他应用程序。
问题
让我们尝试创建一个包含不同类型用户的系统(Manager
、Coder
和 Leader
)。所有用户都应具有打印机相关的操作。System
应支持以下功能:打印、传真、扫描、电子邮件。
用户与打印机功能之间的映射应如下所示:
Manager
:打印、传真、扫描、电子邮件Leader
:打印、扫描Coder
:打印
组合解决方案
让我们尝试通过组合来解决这个问题。根据上述问题定义,我们可以想象一些可以用于实现的软件实体和接口。
- 接口:
IPrinter
、IEmployee
- 类:
SuperPrinter
、AvergePrinter
、BasicPrinter
、Manager
、Coder
、Leader
所有上述推断都只是功能规范中定义的参与者和动作的简单表示。在功能规范中找不到将打印机作为用户操作插件的引用。但如果您仔细阅读规范,就可以设想一个 IPrinterEnabled
接口,它将公开 Printer
接口作为属性,允许用户在构造时插入具体的打印机对象。
这种即插即用的方式使系统能够灵活应对未来的变化,因为用户可以重新排列用户和打印机之间的关系(例如,允许 Leader
使用 SuperPrinter
)。
请参阅下面的类图解释这一点。
您还可以参考附带的 WPF 示例,其中演示了其实现。
继承解决方案
我想解释一下我们如何使用继承方法来解决上述问题。请注意,应用程序对最终用户来说行为将完全相同,但系统在改变时会变得僵化。我不想深入描述实现,您可以查看类图和附带的代码来理解。

参考上述图表和代码,您会发现具体的用户类被附加的打印机功能知识所污染。虽然它们可能在内部使用打印机对象来将调用转接到适当的执行,但这强制开发人员要么创建一个基类来包含这些打印机签名并将其设为虚拟以便派生类可以重写,要么在所有具体类上实现接口。
开发人员通常会选择基类方法,认为如果 IPrinterCommand
接口随时间变化,他们只需要在基类中创建适当的虚拟方法。这是一个愚蠢的错误,散发着设计上的腐朽气息,因为它需要修改现有的(且工作的)基类源代码,并更新所有依赖于特定更改的派生类。
结论
正如您所见,这两种方法都有一些共同的类,但用户类使用打印机功能的方式存在细微差别。在继承方法中,打印机功能被继承到派生的用户对象中,显示了用户与打印功能之间的紧密耦合。而在组合方法中,用户类通过允许它们继承自 IPrinterEnabled
接口来启用打印机功能,使开发人员能够将打印机功能作为插件分配给用户对象。我们可以很肯定地说,组合方法中的用户对象并没有被打印功能所污染。
注意:我使用了上述示例作为随机场景。这可能不是一个实际的场景。考虑到所有开发人员都习惯于听到疯狂的需求,这个例子不会让你发疯。