---
title: "组合和继承"
date: "2026-03"
category: "设计模式与架构"
tags: ["设计模式与架构"]
description: "组合优于继承的关键因素是去耦合，或者说，就像积木一样的模块，而状态则通过外部因素给入——大部分情况下采用静态方法就足够了，但少数情况下可能需要的是一个组合类，下面许多方法共享同样的状态——但是..."
source: "https://enriquemark.com/zh-hans/posts/%E7%B5%84%E5%90%88%E5%92%8C%E7%B9%BC%E6%89%BF"
---

组合优于继承的关键因素是去耦合，或者说，就像积木一样的模块，而状态则通过外部因素给入——大部分情况下采用静态方法就足够了，但少数情况下可能需要的是一个组合类，下面许多方法共享同样的状态——但是，我为什么不采用完全的DI，让状态从外部注入呢？也就是说，更精确的来讲：只有所有组合方法都需要共享同样的单一状态时，我才需要用类——哪怕这个状态本身也还是从类外面注入进来的，区别就是一个是DI方法，一个是DI类。对于一般的情况来说，DI方法就够了。

而组合本身也一直强调，什么时候用它？[组合](/zh-hans/posts/组合) 里面提到「有一个」和「是一个」的区别，类本身是字面意义上的「一类」，换言之，他们是一个物种——共享结构上固定的（就像上面所说的）、提前定义好的父类状态（属性）或方法。子类确实可以写，但那也是父类允许的情况，子类本身的范围和边界被父类限制。这里和接口不同的点是，接口虽然也是对结构的约束，但它更专注于结构本身的抽象，而父类是更底一层的抽象，它除了结构之外，还有具体的行为——有具体内容的方法。而这些具体正是保证里氏替换原则（LSP）的基础——任何子类都可以替代父类，因为他们本身就完全包含（继承）了父类的一切内容。多余的那部分只是没被使用而已。

但是，标准的LSP不是硬约束，假设底层以来了父类的「可覆写」方法，而子类完全覆写了，那就会炸。这也是为啥滥用继承反而容易耦合——不符合LSP原则的继承就不该出现——或者说，父类应该约定好，其可覆写方法是「扩展性」的——而且这些扩展部分，本身就可以作为一个约定而存在了，比如叫什么名字、输入输出是什么，只是逻辑内部可以让你随便写。从这里看，就会发下辖，哪怕是扩展，设计良好的父类也能保证LSP能运作，因为结构（输入-输出）是类型固定的，正常来说哪怕是扩展，用的时候都不会出问题。从一开始就应该保证子类再继承的时候不会破坏核心功能，这些就是保证LSP的核心骨架逻辑。但是，即便如此，这也是结构约束，万一子类就是胡搞，导致方法直接出错，返回null（返回类型系统默认不管这个），或者搞了预期的副作用，那还是会出问题。毕竟你不可能穷尽一切，只要开了扩展点，那什么牛鬼蛇神都可能出来。最大化保证LSP是可以做到的，但这也是继承滥用做不到的地方。

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

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

而且这么来说，组合本身也是可以和继承结合使用的——比如一个大型的工具模块——就算是乐高也分各种类型，你是砖块我是小人，小人之见可以随便拼，但它（一般而言）也不会和砖块结合。那这时，功能上的一系列的「有一个」，就要遵循「是一个」的模式来构建自身了。哪怕他们整体仍然是「有一个」的模块，但自成一脉。
