为什么 “优先考虑组合而非继承”?#
在最近的一篇文章中,我分享了我对项目代码库复杂性的沮丧。基本上,它太复杂且交织在一起。尽管我感到不适,但我必须找到一个解决方案,这让我接触到了设计模式。
我听到的一个原则是 “优先考虑组合而非继承”。起初,我对这两者之间的区别只有模糊的理解,更不用说如何在我的项目中实现这个概念了。然而,这段来自 ArjanCodes 的视频帮助我理解了其背后的理由,我现在将对此进行解释。
耦合造成复杂性#
在设计项目时,特别是在面向对象编程中,由于不同对象之间的交互和依赖,复杂性可能迅速升级。这种复杂性源于耦合。
复杂的耦合可能导致几个问题。它可能使修改和添加新功能变得艰巨,因为你必须考虑不同对象之间错综复杂的相互作用。更改可能在整个代码库中产生不可预见的后果。此外,单元测试在高度耦合的代码中变得更加困难,因为你需要测试所有场景以实现足够的覆盖率。
下面的图表说明了系统耦合的不同级别。高度耦合的系统需要测试所有可能的输入组合。然而,通过将计算解耦为步骤,每个函数可以通过检查单个输入及其对应的输出来验证,这样测试起来要容易得多。
复杂的继承结构引入耦合#
借鉴 Arjan 的例子,在纯粹基于继承的方法中,一个Employee
类可能封装与员工相关的所有操作,例如 ID、年龄、性别和支付方式。对于每种员工类型,你创建一个从基类派生的子类。这种方法通过继承解决问题。
然而,如果你将所有责任分配给一个单一的Employee
类,随着项目的增长及其不断变化的需求,最终将导致一个庞大的层次结构树。每个部分都依赖于其父类,导致共享同一父类的子类之间产生间接耦合。修改任何基类可能会导致混乱。如果一个新变体对你现有实现有甚至是微小的不兼容要求,你可能不得不从一个非常基础的类继承并重新实现所有功能。简而言之,继承造成耦合,过度使用它会使你的工作变得复杂而不是简单。
优先考虑接口#
问题并不是说我们不应该创建复杂的系统,而是确保这些系统内组件的逻辑分离。如果我们检查之前的例子,Employee
类由于被分配了多个责任而显得问题重重。它细分了不同的员工类型,并管理各种支付和佣金,这些任务可以由单独的实例处理。
为了解决这个问题,我们可以为不同的合同类型创建一个Contract
类,为各种佣金创建一个Commission
类。然后可以将这些实例传递给Employee
类。只要这些保持统一的接口,我们的任务就变得简单得多。
结论#
设计模式对我来说是相当新的,但我可以大致理解它们的目标 —— 降低系统的复杂性,并尽可能使其模块化。
“组合优于继承” 的理念在抽象层面上与依赖注入非常相似。它们都是表达同一目标的不同术语,旨在分离和保持局部的简单性,从而增强代码的可维护性和灵活性。