SOLID 是五条面向对象设计原则的首字母缩写。它们分别是单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则。

这五条原则常被放在一起讲,却不是一套必须逐条打卡的编码规范。它们试图回答几个相近的问题。一个类为什么会变得难改,模块之间为什么互相牵连,替换一个实现为什么会破坏调用方,以及抽象什么时候真的有用。

这个缩写从哪里来

Robert C. Martin 在 2000 年发表的论文《Design Principles and Design Patterns》中讨论了面向对象设计和软件腐化问题,也整理了后来成为 SOLID 的前五条设计原则。SOLID 这个缩写据 Wikipedia 记录是在 2000 年代中期由 Michael Feathers 提出的。

所以要把两件事分开看。五条原则的内容早于 SOLID 这个词,缩写只是帮助人记住它们。它们也主要针对面向对象程序设计,并不等于所有软件架构都必须采用继承、接口和类层次结构。

S,单一职责原则

单一职责原则(Single Responsibility Principle,SRP)常被简化为“一个类只做一件事”。更准确的说法是,一个模块应该只有一个引起变化的原因。

这里的“原因”通常来自不同的业务角色或变化来源。一个 Invoice 类既负责计算账单,又负责生成 HTML,再负责把结果发送给客户。它会被三类变化同时牵动。账单规则、页面格式和邮件服务只要有一处变化,这个类都可能被修改。

把它拆成账单计算、文档渲染和通知发送,目的是把变化边界分开。这样做以后,修改页面格式不会让账单计算也跟着重新测试。

SRP 也容易被用得过度。一个只有几行代码的简单类,若为了追求“绝对单一”而拆成很多包装层,理解成本会先上升。判断一个模块是否承担了太多职责,要看它是否因为互不相关的变化反复修改。

O,开闭原则

开闭原则(Open-Closed Principle,OCP)要求软件实体对扩展开放,对修改关闭。这里的实体可以是类、模块或组件。新增一种行为时,理想情况是通过新增实现来完成,而不是反复改动已经稳定的核心逻辑。

例如,订单系统需要支持多种折扣规则。若每增加一种折扣都要在一个巨大 if 语句里加入分支,原有代码会不断变动,测试范围也随之扩大。把折扣规则表达成可替换的策略,新增规则时只需要添加一个实现,就更接近开闭原则。

这条原则并不要求提前为所有想象中的变化建立扩展点。过早设计一套插件系统,可能比修改一个简单函数付出更多成本。真正值得稳定下来的扩展边界,通常来自已经出现的第二种实现,或者来自明确而重要的变化方向。

L,里氏替换原则

里氏替换原则(Liskov Substitution Principle,LSP)来自 Barbara Liskov 1987 年关于数据抽象的研究。它的核心意思是,使用基类或抽象类型的代码,应该能够使用它的子类型,而不需要知道自己换了一个实现。

经典的反例是把正方形当成长方形的子类。长方形接口允许分别修改宽和高,正方形却要求两者始终相等。调用方按照长方形的契约修改其中一个值时,正方形会产生出乎意料的结果。类型关系在语法上成立,行为契约却没有成立。

里氏替换原则要看的重点是子类型是否遵守父类型对输入、输出和状态变化的承诺。子类型不能无故拒绝父类型允许的输入,也不能返回调用方无法处理的结果,更不能在调用方预期安全的操作中突然改变基本语义。

I,接口隔离原则

接口隔离原则(Interface Segregation Principle,ISP)认为,客户端不应该被迫依赖自己不会使用的方法。

假设一个 Worker 接口同时包含打印、扫描、传真和复印方法。只需要打印的客户端也必须依赖完整接口,这会带来不必要的耦合。把它拆成更小的能力接口,打印客户端只依赖打印能力,接口变化的影响范围就会更小。

接口隔离并不等于把每个方法都做成一个接口。接口太碎会制造大量名称、文件和间接调用。好的接口通常围绕客户端真正需要的能力组织,而不是围绕实现者拥有的全部功能组织。

D,依赖倒置原则

依赖倒置原则(Dependency Inversion Principle,DIP)包含两层意思。高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。

例如,订单服务直接创建 MySQL 客户端时,业务规则就和具体数据库绑定在一起。让订单服务依赖一个表达存取需求的接口,再由数据库适配器实现这个接口,业务规则就可以在不更换的情况下接受内存仓储、测试替身或另一种数据库实现。

依赖倒置常与依赖注入一起出现,但两者不是同一个概念。依赖倒置是设计方向,依赖注入是把具体实现传给对象的一种实现方式。一个项目可以通过构造函数注入实现依赖倒置,也可以在其他组合位置完成依赖组装。

五条原则怎样放在一起看

SRP 关注变化原因,OCP 关注变化方式,LSP 关注替换时的行为,ISP 关注接口大小,DIP 关注依赖方向。它们互相联系,却解决不同问题。

在一个具体模块里,先观察变化和依赖,再决定是否需要抽象,通常比从 SOLID 的字母开始设计更稳妥。一个可读的具体实现,往往比一组没有真实替换需求的接口更适合作为起点。需求出现以后,再通过测试和重构稳定边界。

SOLID 也不保证系统一定简单。接口、继承、工厂和依赖注入都可能增加间接层。Reddit 和 Hacker News 上关于这些原则的讨论经常提到同一个风险,开发者把 SOLID 变成检查清单以后,可能为了满足原则而制造单方法接口、空洞抽象和多余的包装类。

这反映了设计原则脱离问题使用的风险。原则的价值在于帮助人看见耦合和变化,最终判断仍然要回到代码的规模、需求的稳定程度和维护者的理解成本。

关联词

  • SRP,单一职责原则,关注模块是否被不同的变化来源同时牵动。
  • OCP,开闭原则,关注新增行为时能否减少对稳定代码的修改。
  • LSP,里氏替换原则,关注子类型是否遵守父类型的行为契约。
  • ISP,接口隔离原则,关注客户端是否被迫依赖不用的接口成员。
  • DIP,依赖倒置原则,关注高层策略和低层细节之间的依赖方向。
  • 依赖注入,把依赖对象从外部传入的一种实现技术,常用于落实依赖倒置。
  • 重构,在不改变外部行为的前提下改善内部结构,是逐步应用这些原则的重要手段。

小结

SOLID 是一组观察面向对象设计的概念工具。它提醒我们留意变化边界、行为契约、接口大小和依赖方向。学习它的重点是知道代码为什么难改,以及哪种边界能让下一次修改更局部。

参考资料