很多人学完封装、继承、多态、组合,仍会写出"有类无设计"的代码:一个类什么都干、改需求就改老代码、子类不能替父类、接口又肥又乱、高层模块死盯底层实现。根因不是没背过SOLID缩写,而是没把五大原则当成排查坏味道、约束协作关系、落地设计模式的架构标尺。
本文将逐条拆解单一职责、开闭、里氏替换、接口隔离、依赖倒置。每条原则给出核心定义、业务场景、反例与正例,并讲清原则之间的联动。所有设计模式的本质,都是对SOLID的合理落地与极致优化。
一、为什么先吃透SOLID,再谈设计模式
设计模式解决的是"怎么组织对象协作",SOLID解决的是"协作该不该这么组织"。没有SOLID,工厂、策略、装饰器往往变成生搬硬套的"模式表演"。有了SOLID,模式才有判断依据,该不该拆、能不能扩、能不能替、接口该不该肥、依赖该指向谁。
五大原则可记作一条架构主线:
| 原则 | 英文 | 核心目标 | 一句话口诀 |
|---|---|---|---|
| 单一职责 | SRP | 变更原因唯一 | 一类一事 |
| 开闭 | OCP | 扩展开、修改关 | 加新不改旧 |
| 里氏替换 | LSP | 子类可替换父类 | 能替才叫继承 |
| 接口隔离 | ISP | 接口小而专 | 不强迫用不了的 |
| 依赖倒置 | DIP | 依赖抽象不依赖细节 | 高层不认底层 |
它们不是彼此割裂的教条,SRP拆得好,OCP才扩得开;LSP/ISP把继承与接口守住边界;DIP把协作重心翻到抽象上。策略、工厂、适配器、模板方法等模式,本质上都是在强化这条链路。

二、S:单一职责原则(SRP)
2.1 核心定义:一个类只有一个引起它变化的原因
Robert C. Martin的原意是:一个模块应对一个参与者(actor)负责,即引起该类修改的业务动机应当唯一。类里方法可以很多,但这些方法必须服务于同一职责边界。
架构价值是高内聚,把不同变化源拆开,改结算不影响登录,改短信通道不影响订单落库。这是后续OCP、DIP能成立的前提------职责混杂时,任何"扩展"都会牵一发而动全身。
2.2 业务场景
电商订单模块同时承担:创建订单、计算优惠、发短信通知、写操作日志、导出Excel。需求上线后,运营改短信文案就要动订单类,财务改导出格式也要动订单类,这是典型的多变化源撞车。
2.3 反例:一个类扛全场
反例
java
public class OrderService {
public void createOrder(Order order) {
// 1. 校验库存
// 2. 算价落库
// 3. 发短信
// 4. 写审计日志
// 5. 同步导出报表字段
}
}
表面"一个入口很方便",实则通知、审计、报表任意一方变更,都要打开核心交易类------耦合的是变化源,不是方法个数。
2.4 正例:按变化源拆分,再组合协作
java
public class OrderRepository {
public void save(Order order) { /* 持久化 */ }
}
public class PriceCalculator {
public Money calc(Order order) { /* 计价 */ return null; }
}
public class OrderNotifier {
public void notifyCreated(Order order) { /* 短信/站内信 */ }
}
public class AuditLogger {
public void log(Order order) { /* 审计 */ }
}
public class OrderAppService {
private final OrderRepository repo;
private final PriceCalculator calculator;
private final OrderNotifier notifier;
private final AuditLogger auditLogger;
public OrderAppService(OrderRepository repo, PriceCalculator calculator,
OrderNotifier notifier, AuditLogger auditLogger) {
this.repo = repo;
this.calculator = calculator;
this.notifier = notifier;
this.auditLogger = auditLogger;
}
public void createOrder(Order order) {
order.setPayable(calculator.calc(order));
repo.save(order);
notifier.notifyCreated(order);
auditLogger.log(order);
}
}
OrderAppService只编排流程,各细节类各自应对自己的变化。这既是SRP,也是上一章强调的组合复用:用has-a拼职责,而不是用继承硬塞能力。排查的时候可以问自己,例如改短信文案要不要动这个类?改计价规则要不要动?改存储引擎要不要动?若答案里有两个"要",就开始拆。
三、O:开闭原则(OCP)
3.1 核心定义:对扩展开放,对修改关闭
开闭原则指的是:软件实体(类、模块、函数)应该允许通过新增代码扩展行为,而尽量避免为了新业务去改已经稳定的旧代码。它不是禁止一切修改,而是要求你把"会变的"抽成抽象,让稳定流程只依赖抽象。策略模式、工厂模式、模板方法,几乎都是OCP的工程化表达。
3.2 业务场景
支付渠道从支付宝、微信扩展到云闪付:若每次加渠道都在if-else里插入分支,旧逻辑会被反复打开------回归成本陡增。正确做法是:统一支付抽象,新渠道=新实现类。
3.3 反例:用修改堆需求
例如:
python
class PayService:
def pay(self, channel: str, amount: float):
if channel == "alipay":
print(f"支付宝支付 {amount}")
elif channel == "wechat":
print(f"微信支付 {amount}")
elif channel == "unionpay": # 每次加渠道都改这里
print(f"云闪付支付 {amount}")
else:
raise ValueError("unknown channel")
PayService永远不会稳定:每个新渠道都是一次对核心类的侵入修改。
3.4 正例:抽象入口+新增实现
python
from abc import ABC, abstractmethod
class Payment(ABC):
@abstractmethod
def pay(self, amount: float) -> None: ...
class AliPay(Payment):
def pay(self, amount: float) -> None:
print(f"支付宝支付:{amount}")
class WeChatPay(Payment):
def pay(self, amount: float) -> None:
print(f"微信支付:{amount}")
class UnionPay(Payment):
def pay(self, amount: float) -> None:
print(f"云闪付支付:{amount}")
class PayService:
def do_pay(self, payment: Payment, amount: float) -> None:
payment.pay(amount)
# 扩展:只新增类,不改 PayService
PayService().do_pay(UnionPay(), 99.0)
多态提供"统一调用、差异实现";OCP要求你把这种差异固化在扩展点上。没有抽象边界,多态只是写法。有了扩展点,多态才是架构能力。
3.5 与封装的关系
上一章讲封装是"隔离变化"。OCP是隔离变化的工程化目标。变化被封在新类里,旧调用方接口不变。若属性、行为、依赖都裸奔在外,根本谈不上"对修改关闭"。
四、L:里氏替换原则(LSP)
4.1 核心定义:子类型必须能够替换其基类型
里氏替换强调,凡是使用父类型的地方,换成其子类型后,程序行为语义仍应成立,不能悄悄破坏前置条件、后置条件或不变式。
LSP不是"能编译/能跑就行",而是"替换后约定仍成立"。违反LSP的继承,是上一章说的伪is-a:看起来像继承,实际是披着继承外衣的功能缝合。
4.2 业务场景与经典陷阱
"正方形是矩形"在数学上成立,在可变对象模型里常不成。矩形允许独立改宽高,正方形改宽必须联动改高。若业务代码依赖"改宽不影响高",用正方形替换矩形会行为错乱,这是继承复用压过语义正确的典型坏味道。
4.3 反例:伪替换
python
class Bird {
public void fly() {
System.out.println("飞翔");
}
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("企鹅不会飞");
}
}
// 调用方假定所有Bird都能fly
void migrate(Bird bird) {
bird.fly(); // 传入Penguin直接炸裂
}
python
class Rectangle:
def __init__(self, w, h):
self.w, self.h = w, h
def set_width(self, w):
self.w = w
def area(self):
return self.w * self.h
class Square(Rectangle):
def set_width(self, w):
self.w = self.h = w # 破坏了"只改宽"的约定
def stretch(rect: Rectangle):
rect.set_width(rect.w + 10)
# 调用方以为面积只按宽度变化;正方形会连带改高
4.4 正例:按真实能力建模,不强行继承
python
from abc import ABC, abstractmethod
class Flyer(ABC):
@abstractmethod
def fly(self): ...
class Walker(ABC):
@abstractmethod
def walk(self): ...
class Sparrow(Flyer, Walker):
def fly(self): print("麻雀飞翔")
def walk(self): print("麻雀行走")
class Penguin(Walker):
def walk(self): print("企鹅行走")
def migrate(flyer: Flyer):
flyer.fly() # 只接受真正会飞的类型
或用组合表达能力,而不是把所有鸟塞进一个会飞的父类。能替换,才叫正当继承。不能替换,就该拆接口或改组合。
4.5 排查步骤
- 子类是否削弱了父类承诺?
- 子类是否收紧了输入、放宽了错误输出?
- 文档/测试里对基类的断言,子类是否仍成立? 有一条"否",LSP就不能用。
五、I:接口隔离原则(ISP)
5.1 核心定义:不应强迫客户端依赖它不需要的接口
ISP针对的是"胖接口",一个巨大接口塞满所有方法,实现者不得不空实现或乱抛异常,调用者也被无用方法污染。正确做法是按客户端需要拆成多个小而专的接口。
注意ISP拆的是"角色接口",不是机械地一个方法一个接口。拆到客户端刚好够用、实现者刚好能兑现。
5.2 业务场景
打印设备体系:有的机器只会打印,有的能扫描传真。若统一强制实现print/scan/fax,简易打印机只得写下假实现,这既违反ISP,也会连带违反LSP(替换后行为失真)。
5.3 反例:胖接口逼空实现
python
interface MultiFunctionDevice {
void print(String doc);
void scan(String doc);
void fax(String doc);
}
class SimplePrinter implements MultiFunctionDevice {
public void print(String doc) { System.out.println("打印:" + doc); }
public void scan(String doc) { throw new UnsupportedOperationException(); }
public void fax(String doc) { throw new UnsupportedOperationException(); }
}
5.4 正例:按角色拆分
python
from typing import Protocol
class Printer(Protocol):
def print(self, doc: str) -> None: ...
class Scanner(Protocol):
def scan(self, doc: str) -> None: ...
class SimplePrinter:
def print(self, doc: str) -> None:
print(f"打印:{doc}")
class OfficeAllInOne:
def print(self, doc: str) -> None: ...
def scan(self, doc: str) -> None: ...
def fax(self, doc: str) -> None: ...
def do_print(device: Printer, doc: str) -> None:
device.print(doc) # 调用方只依赖自己需要的能力
Python的Protocol/鸭子类型很灵活,但也更容易写出"同名方法乱共享"。ISP要求在设计上先划清角色,再谈灵活。
5.5 记忆表
| 症状 | 往往违反 | 改法 |
|---|---|---|
实现类大量UnsupportedOperationException |
ISP+LSP | 拆接口 |
| 调用方import了一堆用不到的方法 | ISP | 收窄依赖接口 |
| 改一个客户端无关功能却牵动多方编译 | ISP / SRP | 按角色与变化源拆分 |
六、D:依赖倒置原则(DIP)
6.1 核心定义:高层模块不应依赖低层模块,二者都应依赖抽象
依赖倒置把常识里的依赖方向翻过来,业务编排(高层)不要直接new具体短信网关、具体数据库驱动(低层)。两边都依赖抽象(接口/协议)。控制反转(IoC)与依赖注入(DI)是DIP的常见落地手段,但DIP本身是设计方向,不是框架专属。
上一章的依赖、关联、聚合、组合,描述的是耦合强度。DIP进一步规定:稳定的抽象应处在依赖箭头的中心。
6.2 业务场景
通知中心:订单完成后发短信。若OrderService直接依赖AliyunSmsClient,换供应商就改核心业务;若双方都依赖Notifier抽象,替换通道只需换实现与装配。
6.3 反例:高层钉死低层细节
python
class AliyunSmsClient:
def send(self, phone: str, content: str):
print(f"[Aliyun] {phone}: {content}")
class OrderService:
def __init__(self):
self.sms = AliyunSmsClient() # 高层直接依赖细节
def complete(self, order):
# ...完成订单
self.sms.send(order.phone, "订单已完成")
6.4 正例:依赖抽象,细节可替换
python
from typing import Protocol
class Notifier(Protocol):
def send(self, phone: str, content: str) -> None: ...
class AliyunSmsNotifier:
def send(self, phone: str, content: str) -> None:
print(f"[Aliyun] {phone}: {content}")
class OrderService:
def __init__(self, notifier: Notifier):
self.notifier = notifier
def complete(self, order):
self.notifier.send(order.phone, "订单已完成")
# 装配点决定具体实现,业务核心不感知供应商
svc = OrderService(AliyunSmsNotifier())
DIP让OCP真正可执行,扩展点(新Notifier实现)能插进去,是因为高层从来没绑死某个低层类。
6.5 与四种对象关系的对照
| 关系 | 常见误用 | DIP 建议 |
|---|---|---|
| 依赖 | 方法临时new具体类 |
参数接收抽象,或从工厂/容器取抽象 |
| 关联/聚合 | 成员变量直接持有具体实现 | 成员类型声明为接口/协议 |
| 组合 | 内部写死具体部分且无法替换 | 对可变部分组合抽象,或在边界注入 |
十、总结
SOLID 的核心从来不是五条英文字母,而是一条可执行的架构纪律:
- 单一职责把变化源拆开,形成高内聚单元。
- 开闭要求用新增实现承接变化,稳住已验证逻辑。
- 里氏替换守住继承与多态的契约底线。
- 接口隔离让抽象小而专,避免胖接口污染。
- 依赖倒置把依赖方向扭向抽象,让高层业务摆脱低层细节。