在软件工程中,设计模式(Design Patterns)是针对特定场景的具象解决方案,而设计原则(Design Principles)则是指导我们构建可维护、可扩展、低耦合、高内聚代码体系的底层哲学与架构心智模型。
SOLID 面向对象五大原则
SOLID 是由 Robert C. Martin(Uncle Bob)总结提炼的面向对象设计五大核心原则,是构建健壮 OO 系统的基石。
单一职责原则
- 英文简称:SRP
- 英文全称:Single Responsibility Principle
- 核心定义:
“A class should have one, and only one, reason to change.”(一个类应该且仅有一个引起它变化的原因。)
- 核心思想:每个类、模块或函数只负责系统中的单一功能或单一维度的业务职责。
- 解决痛点:避免“上帝类(God Class)”,降低修改某一部分逻辑时对其他无关逻辑造成连带破坏的风险。
- 遵循带来的好处:
- 降低类复杂度:每个类代码量可控,逻辑聚焦,阅读和理解成本大幅降低。
- 提高可测试性:单一职责的类依赖少、边界清晰,易于编写细粒度、高覆盖率的单元测试(Mock 成本极低)。
- 减少变更冲突:多人协同开发时,不同业务需求的修改会分散到不同类中,极大减少 Git 合并冲突。
- 提升代码复用率:细粒度职责的模块更容易在其他业务场景中被直接复用。
- 实践要点:
- 职责划分的粒度取决于业务变化的方向。
- 将数据结构(如
User实体)、数据持久化(如UserRepository)与业务动作(如UserNotificationService)分离开来。
开闭原则
- 英文简称:OCP
- 英文全称:Open-Closed Principle
- 核心定义:
“Software entities should be open for extension, but closed for modification.”(软件实体应当对扩展开放,对修改关闭。)
- 核心思想:当需求发生变化或需要增加新功能时,应当通过添加新的代码(扩展)来实现,而不是通过修改已有且稳定运行的代码。
- 解决痛点:消除“改一处影响全局”的连锁反应,降低对旧有代码的重复回归测试成本。
- 遵循带来的好处:
- 保障系统稳定性:已通过严格测试的既有逻辑无需被修改,最大程度保证已有功能的稳定性和零回归风险。
- 灵活支持新需求:通过新增实现类即可热插拔接入新能力,系统具有极高的新增吞吐效率。
- 架构生命周期更长:基于抽象建立的骨架不易腐化,能够平滑应对业务快速迭代。
- 实践要点:
- 核心手段是抽象(Abstraction)与多态(Polymorphism)。
- 结合策略模式(Strategy)、工厂模式(Factory)或插件化架构,将“变化的部分”封装在接口之后。
里氏替换原则
- 英文简称:LSP
- 英文全称:Liskov Substitution Principle
- 核心定义:
“Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.”(子类对象必须能够完全替换掉所有使用其基类的地方,且程序行为不受影响。)
- 核心思想:子类继承父类时,必须保证父类约定的契约(行为、前后置条件、不变量)不被破坏。
- 解决痛点:避免多态调用时发生意外的行为偏差、隐式副作用或运行时异常(如
UnsupportedOperationException)。 - 遵循带来的好处:
- 保证多态的健壮性:调用方可以完全信赖抽象基类的行为契约,无需写
instanceof或向下转型判断。 - 提升继承体系的可维护性:防止滥用继承带来的代码逻辑扭曲,保持类层级树的整洁与语义一致性。
- 增强系统可插拔性:任何新的子类都可以透明地无缝替换老实现,上层业务无需做任何适配或防御性补丁。
- 保证多态的健壮性:调用方可以完全信赖抽象基类的行为契约,无需写
- 经典反例:“正方形不是长方形”——若
Square继承自Rectangle,重写setWidth时会隐式修改height,破坏长方形的行为假设。 - 实践要点:
- 子类方法的前置条件(参数范围)不能比父类更严格。
- 子类方法的后置条件(返回值范围)不能比父类更宽松。
- 避免子类用“空实现”或抛出异常来覆盖父类方法。
接口隔离原则
- 英文简称:ISP
- 英文全称:Interface Segregation Principle
- 核心定义:
“Clients should not be forced to depend upon interfaces that they do not use.”(客户端不应该被强迫依赖它不使用的接口。)
- 核心思想:提供多个专用、细粒度的小接口,而不是一个臃肿庞大的胖接口(Fat Interface)。
- 解决痛点:避免实现类为了实现一个接口而不得不提供大量无意义的空实现;避免胖接口变动导致无关调用方被动重新编译或部署。
- 遵循带来的好处:
- 极低的模块依赖耦合:客户端仅依赖其真正关心的方法,接口变动的影响被严格限制在最小范围。
- 杜绝接口污染:实现类不需要为无关功能承担空实现或抛错样板代码,结构更加清晰。
- 高度灵活的接口组合:细粒度接口可如乐高积木般自由组合(例如 Go 语言标准库中的
io.Reader、io.Writer、io.ReadCloser)。
- 实践要点:
- 角色接口模式(Role Interface):站在调用方视角来定义接口。
- 通过多重继承或接口组合支持多重能力。
依赖倒置原则
- 英文简称:DIP
- 英文全称:Dependency Inversion Principle
- 核心定义:
“High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.”(高层模块不应依赖低层模块,二者都应依赖抽象;抽象不应依赖细节,细节应依赖抽象。)
- 核心思想:面向接口编程,解耦高层业务策略与底层技术实现(如存储、网络、消息中间件)。
- 解决痛点:打破“底层技术选型绑架上层业务”的硬编码耦合链条,消除级联修改。
- 遵循带来的好处:
- 实现业务与技术的彻底解耦:核心业务逻辑不再与具体的数据库驱动、第三方 SDK 等技术实现绑定。
- 大幅提升可替换性与扩展性:例如在持久层从 MySQL 切换为 PostgreSQL / Mongo 时,业务层代码一行不用改。
- 极大简化自动化测试:通过依赖注入,可在单元测试中随心所欲替换为 Mock / Stub 对象进行快速单测。
- 衍生落地机制:
- 控制反转(Inversion of Control, IoC):将对象的创建与依赖生命周期管理控制权由程序代码自身反转给外部容器。
- 依赖注入(Dependency Injection, DI):通过构造函数、Setter 或接口将依赖对象直接注入,而非在类内部直接
new具体实现。
面向对象经典核心法则
除了 SOLID,面向对象领域还有两条极为关键的互补设计原则:
迪米特法则 / 最少知识原则
- 英文简称:LoD / PLK
- 英文全称:Law of Demeter / Principle of Least Knowledge
- 核心定义:
“Only talk to your immediate friends, don’t talk to strangers.”(只与直接的朋友通信,不和陌生人说话。)
- 核心思想:一个对象应当对其他对象有尽可能少的了解。一个方法内应该只调用:
- 该对象自身的方法
- 方法参数传入的对象
- 该方法内部实例化的对象
- 该类的成员变量对象
- 解决痛点:避免深层嵌套的对象导航(火车车厢调用链)导致调用方对整条依赖路径上的数据结构产生强耦合与脆弱性。
- 遵循带来的好处:
- 局部隔离性强:中间对象的内部结构变动(如重构字段、拆分类)不会波及远端调用方。
- 提高代码健壮性:消除过长的链式
get带来的连续空指针(NPE)隐患。 - 封装更彻底:促使对象主动暴露具有语义的行为方法,而非单纯暴露 getter 沦为贫血模型。
- 典型反例:
order.getCustomer().getAddress().getCity()。 - 改进方式:在
Order上提供封装委托方法,如order.getDeliveryCity(),隐藏内部对象图细节。
组合复用原则 / 合成复用原则
- 英文简称:CRP / CARP
- 英文全称:Composite Reuse Principle / Composite/Aggregate Reuse Principle
- 核心定义:
“Favor ‘object composition’ over ‘class inheritance’.”(优先使用对象组合/聚合,而非类继承。)
- 核心思想:
- 继承(Is-A)属于白箱复用,父类内部细节对子类暴露,破坏了封装性,且编译期静态绑定难以在运行时动态切换。
- 组合(Has-A)属于黑箱复用,只依赖被组合对象的公共接口,耦合度极低,支持运行时动态替换能力。
- 解决痛点:避免继承体系层级过深导致的类爆炸(Class Explosion)、难以扩展以及脆弱基类问题(Fragile Base Class Problem)。
- 遵循带来的好处:
- 运行时动态可变:可以在运行时动态为对象注入或切换不同的组件实现(如策略切换)。
- 强封装保护:被组合组件的内部实现对外部完全黑箱,组件之间相互独立、隔离度高。
- 系统架构更扁平:避免构建庞杂臃肿的多层继承树,代码心智负担低。
- 实践建议:除非存在明确的、满足里氏替换的多态多特化关系,否则默认优先选择组合。
软件工程通用心智原则
这些原则不仅适用于面向对象,同样广泛适用于函数式编程、分布式架构及日常工程开发。
不要重复自己
- 英文简称:DRY
- 英文全称:Don’t Repeat Yourself
- 核心思想:系统的每一项业务知识与逻辑规则,在系统中都必须有单一、无歧义且权威的表达形式(Single Source of Truth, SSOT)。
- 解决痛点:多处重复代码在需求变更时容易出现“改了这里漏了那里”的重大一致性 Bug。
- 遵循带来的好处:
- 变更维护成本极低:一处修改,全局生效,消除重复维护与测试成本。
- 知识权威唯一:避免不同代码块由于维护不一致而导致的业务规则分歧。
- 注意:DRY 针对的是“业务知识与逻辑的重复”,而不是单纯代码结构偶然相似的重复。避免把偶发相似但演进方向完全不同的代码过早抽取而造成错误耦合。
保持简单直白
- 英文简称:KISS
- 英文全称:Keep It Simple, Stupid
- 核心思想:绝大多数系统在保持简单时运行得最好。代码应尽量保持直观、清晰、可读,避免为了炫技而引入不必要的抽象层或冷门语言特性。
- 解决痛点:过度设计(Over-engineering)导致团队认知负荷过大、新人上手困难、调试排错成本陡增。
- 遵循带来的好处:
- 大幅降低认知与上手成本:任何团队成员都能快速理解意图,易读、易审、易交接。
- 排错效率高:逻辑直接直白,线上排障时能够一目了然定位问题,减少隐蔽深层次 Bug。
你用不着它
- 英文简称:YAGNI
- 英文全称:You Aren’t Gonna Need It
- 核心思想:极限编程(XP)核心原则之一。只实现当下确定的需求,不要为了“未来可能用得上”而提前开发额外功能。
- 解决痛点:过早为尚未验证的伪需求编写大量冗余代码,最终沦为无人使用的死代码与维护负债。
- 遵循带来的好处:
- 缩短项目交付周期:将宝贵的开发和测试精力完全聚焦在当前最高价值的核心业务上。
- 保持代码库轻盈:没有冗余的预设逻辑,后续演进时不会被陈旧的伪需求束缚手脚。
关注点分离
- 英文简称:SoC
- 英文全称:Separation of Concerns
- 核心思想:将软件系统根据功能或关注维度划分为互不重叠的独立子域。
- 解决痛点:业务核心代码与日志、安全、事务等通用基础设施代码交织纠缠(Spaghetti Code)。
- 遵循带来的好处:
- 各层专注于本职能力:业务开发人员专注业务流程,基础架构人员专注底层框架与横切治理。
- 各模块独立演进与替换:基础设施升级(如日志框架迁移)完全不会干扰业务逻辑。
- 典型体现:
- 架构分层:展现层(Presentation)、业务逻辑层(Business Logic)、数据访问层(Data Access)。
- 面向切面编程(Aspect-Oriented Programming, AOP):将日志、权限、事务、监控等横切关注点(Cross-cutting Concerns)从核心业务逻辑中解耦出来单独管理。
命令查询分离 / 命令查询职责分离
- 英文简称:CQS / CQRS
- 英文全称:Command-Query Separation / Command-Query Responsibility Segregation
- 核心思想:
- CQS:一个方法要么是一个执行动作的命令(Command)(修改状态,无返回值/返回状态),要么是一个获取信息的查询(Query)(返回数据,无副作用),不应二者兼顾。
- CQRS:架构层面上将读模型(Query Model)与写模型(Command Model)进行数据链路与存储的物理或逻辑解耦。
- 解决痛点:查询操作中夹带修改状态的副作用(Side Effects),导致调试时行为不可预测;高并发下读写混合模型相互争抢资源。
- 遵循带来的好处:
- 纯粹可预测的读操作:查询方法安全无副作用,可任意并发调用或安全缓存。
- 架构读写针对性调优:针对高频读采用缓存/ES/只读库优化,针对写模型专注事务与一致性保障。
契约式设计
- 英文简称:DbC
- 英文全称:Design by Contract
- 核心定义:
“如果调用方(甲方)保证满足约定的前置条件,被调用方(乙方)就承诺必然交付满足约定的结果与状态。”
- 核心思想:软件模块之间的协作应当如同商业合同一样明确,每个函数/类都显式界定三大约束:
- 前置条件(Preconditions -
requires):调用方的责任。调用该方法前必须保证为真的条件(如参数合法性、对象前置状态)。若违反,100% 是调用方的 Bug。 - 后置条件(Postconditions -
ensures):被调用方的承诺。方法执行结束后保证交付的结果或系统状态。若前置条件满足但后置条件失败,100% 是被调用方实现的 Bug。 - 类不变量(Class Invariants -
invariant):类生命周期的恒定底线。在对象创建后及任何公开方法执行前后,对象内部必须始终维系的健康状态(如balance >= 0)。
- 前置条件(Preconditions -
- 解决痛点:
- 防御式代码泛滥与冗余:调用方怕出错查一遍,被调用方不信任外部又查一遍,底层工具类再查一遍,代码臃肿且充斥大量无意义的判空。
- 责任边界模糊:线上出现异常数据时,无法第一时间界定是传参方的错误还是实现方的逻辑缺陷。
- 遵循带来的好处:
- 责任界定清晰,告别扯皮:入口参数非法必是调用方的责任;计算结果偏差必是被调用方的责任。
- 错误快速暴露(Fail-fast):在系统入口与契约边界立即阻断非法调用,防止脏数据渗透到系统深处引发难以追踪的次生灾害。
- 免除多余防御开销:在契约建立后,内部方法可大胆信任输入,省去深层冗余的防御性分支判断。
- 里氏替换原则(LSP)的形式化根基:子类重写父类方法时,“前置条件只能更宽松(不能要求更多),后置条件只能更严格(承诺更多)”,这是保证多态替换安全性的数学基础。
- 典型代码示例:
public class BankAccount {
private double balance; // 类不变量:balance 必须始终 >= 0
public void withdraw(double amount) {
// ========== 1. 检验前置条件 (Precondition) ==========
// 违约属于调用方 Bug,入口立即 Fail-fast
if (amount <= 0) {
throw new IllegalArgumentException("取款金额必须大于 0");
}
if (amount > this.balance) {
throw new IllegalStateException("账户余额不足");
}
double oldBalance = this.balance;
// ========== 核心业务逻辑 ==========
this.balance -= amount;
// ========== 2. 断言后置条件 (Postcondition) ==========
// 违约属于当前方法自身逻辑 Bug
assert this.balance == oldBalance - amount : "扣款后余额计算异常";
// ========== 3. 断言类不变量 (Class Invariant) ==========
assert this.balance >= 0 : "账户发生透支,破坏类不变量";
}
}
- 现代工程与语言实践:
- Kotlin:内置
require(param > 0)校验前置条件(失败抛IllegalArgumentException);check(state)校验状态契约(失败抛IllegalStateException)。 - Java / Spring:Google Guava
Preconditions.checkArgument(...)以及 JSR-380 Bean Validation 注解(@NotNull,@Min,@Size)即声明式前置契约。 - 单元测试与断言:测试用例中的
assert本质上就是对方法后置条件与不变量的自动化契约验收。
- Kotlin:内置
原则速查矩阵
| 中文名称 | 英文全称 | 英文简称 | 解决的核心问题 | 核心收益 / 好处 |
|---|---|---|---|---|
| 单一职责原则 | Single Responsibility Principle | SRP | 职责过多导致的高耦合与易碎性 | 降低复杂度、极易单测、减少代码合并冲突 |
| 开闭原则 | Open-Closed Principle | OCP | 修改旧代码带来的回归风险 | 零回归风险、插件化扩展、架构生命周期长 |
| 里氏替换原则 | Liskov Substitution Principle | LSP | 不当继承破坏多态契约 | 保证多态健壮性、可透明替换、无需类型强转 |
| 接口隔离原则 | Interface Segregation Principle | ISP | 胖接口导致的无用依赖与污染 | 依赖极低、杜绝空实现样板代码、积木式自由组合 |
| 依赖倒置原则 | Dependency Inversion Principle | DIP | 高层依赖低层细节导致的紧耦合 | 业务与技术实现解耦、极易 Mock 测试、方便技术选型替换 |
| 迪米特法则 | Law of Demeter | LoD / PLK | 对象间调用链过深 | 隔离内部变动、消除 NPE 隐患、高内聚封装 |
| 组合复用原则 | Composite Reuse Principle | CRP / CARP | 继承层次过深、白箱破坏封装 | 运行时动态插拔、黑箱强封装、架构扁平轻量 |
| 不要重复自己 | Don’t Repeat Yourself | DRY | 逻辑分散导致修改遗漏 | 一处修改全局生效、规则一致性高、维护成本极低 |
| 保持简单直白 | Keep It Simple, Stupid | KISS | 过度设计与认知负荷过重 | 极低认知成本、易读易审易交接、排障一目了然 |
| 你用不着它 | You Aren’t Gonna Need It | YAGNI | 臆想需求带来的多余复杂度 | 缩短交付周期、聚焦核心价值、代码库轻盈无死代码 |
| 关注点分离 | Separation of Concerns | SoC | 业务与基础能力混杂 | 业务专注业务/架构专注底座、横切关注点独立演进 |
| 命令查询分离 | Command-Query Separation | CQS / CQRS | 读写混杂导致的副作用隐患 | 纯函数无副作用读、读写针对性独立扩展与缓存调优 |
| 契约式设计 | Design by Contract | DbC | 接口边界与状态假设模糊 | 责任边界分明、Fail-fast 尽早拦截错误、免除冗余防御代码 |