---
title: "组合和继承"
date: "2026-03"
updated: "2026-09"
category: "设计模式与架构"
tags: ["设计模式与架构"]
description: "组合优于继承的关键因素是去耦合，或者说，就像积木一样的模块，且应当尽可能的消除外部的状态和副作用。理想的组合，彼此应当是原子的。只对自身的内部负责，而不关心外部..."
source: "https://enriquemark.com/zh-hans/posts/composition-and-inheritance/"
---

组合优于继承的关键因素是去耦合，或者说，就像积木一样的模块，且应当尽可能的消除外部的状态和副作用。理想的组合，彼此应当是原子的。只对自身的内部负责，而不关心外部。但类就不一样了，继承本身就暗示了相反的东西，它的关键点在于「状态和行为的共享」——这会造成无形间的耦合，这是代价，但在某些时候确实是需要的。这里我们可以从何时用组合看出来。什么时候用它？[组合](/zh-hans/posts/composition/)里面提到「有一个」和「是一个」的区别，类本身是字面意义上的「一类」，换言之，他们是一个物种——共享结构上固定的（就像上面所说的）、提前定义好的父类状态（属性）和方法（实现的具体行为）。

而我们所说的耦合，指的就是这种状态共享上带来的隐藏关联。父类和子类共享一个可变的状态，但它对下面的继承者扩展了什么是无知的，换言之它无法预料子类到底依赖了自己的哪个部分，也许你更改了一个无关紧要的东西，但远在天边的另一个功能坏了；或者反过来，任何一个子类改了祖父类的状态，使用这个状态的他自己的父类就可能出问题，等等诸如此类——这就是隐式依赖，顺著整个链条传导的后果。

正常来说哪怕是扩展，用的时候都不该出问题，如果约定良好，确保继承时的纪律和测试覆盖，的确可以让继承链出问题时不至于出生产事故。但是，我为什么非要搞得这么耦合呢？万一子类就是胡搞，导致方法直接出错，或者搞了非预期的副作用，只要你是关联的，那就还是会出问题。毕竟在团队开发中，每个人都只能看到自己的一个部分，甚至人员流动之下，前后都是彼此不共享信息的。撰写父类的人不可能穷尽一切，只要开了扩展点，那什么牛鬼蛇神都可能出来。

如果我需要专注于状态和行为的具体实现，且希望后续的依赖方「继承」（字面意义上）这些内容，那这种情况下用基类和继承的模式是标准做法，没什么冗余的。毕竟我不能让人类当一个佛兰肯斯坦，用一大堆不相干的「积木」去拼凑，就算这样最终能跑起来，它也会像科学怪人那样是让人困惑的——虽然就像上述说的一样，能动，但我们的印象就是人类有其固有的状态，这些状态必须是继承的而不是拼凑的，人类必须得是「是一个」，所以根本不是标准做法了，而是成了「反模式」。

但有些情况下我们就需要组合：积木、去耦合化的模块。它面向的是「有一个」。我只是想要一个复用的功能，就跟乐高一样，可以在任何地方拼，也不依赖什么必然的固定状态，那组合就再合适不过了。你拼成房子还是直升机我不感兴趣，只要你能拼，那你就能用。

而对组合来说，至关重要的一个东西就是「接口」。何为接口？如果说类是专注于状态和行为的具体实现，那么接口就是专注于结构本身的约束——它不关心实现，而只关心这个实现应该是什么形状的。

从另外一个方面说，组合并不与类排斥。因为组合的灵活性，它的拼装场所可以发生在任何地方——例如类的里面。即便是类，我也没必要把各种工具方法都作为这个类的固有行为塞进去，那只会造成过于臃肿的方法膨胀。需要的时候直接使用组合工具，为什么不呢？只要组合能够确保自己的输入和输出是不变的，那么它对任何使用方也都是明确的。
