SOLID 是面向对象设计的五大原则,Robert C. Martin 提出。每个字母代表一个原则,合起来教你写"容易扩展、易于维护、不容易出 bug"的代码。这篇文章用具体代码示例讲透 SOLID,把"纸上原则"变成"工程肌肉记忆"。
S - 单一职责原则(Single Responsibility)
"一个类只应该有一个引起它变化的原因"。说人话:一个类只做一件事。
// 违反 SRP:一个类做了 3 件事
class UserManager {
save(user) { /* 数据库操作 */ }
sendWelcomeEmail(user) { /* 邮件发送 */ }
generateReport(users) { /* PDF 报表 */ }
}
// 数据库变了要改、邮件模板变了要改、报表格式变了要改 —— 三个不同原因都让这个类变化
// 遵循 SRP:拆开
class UserRepository {
save(user) { ... }
}
class EmailService {
sendWelcomeEmail(user) { ... }
}
class ReportGenerator {
generateUserReport(users) { ... }
}
SRP 是 SOLID 里最简单也最重要的原则。其他四个原则在某种意义上都是 SRP 的延伸。
O - 开闭原则(Open/Closed)
"对扩展开放,对修改关闭"。新功能通过新增代码实现,而不是改老代码。
// 违反 OCP:加新支付方式要改 calculate
class PaymentProcessor {
calculate(order, type) {
if (type === 'credit') return order.total * 1.03;
if (type === 'paypal') return order.total * 1.025;
if (type === 'alipay') return order.total * 1.01;
// 每加一种,这个 if 链都要改
}
}
// 遵循 OCP:用策略模式
interface PaymentStrategy {
calculate(order): number;
}
class CreditCardStrategy implements PaymentStrategy {
calculate(order) { return order.total * 1.03; }
}
class PayPalStrategy implements PaymentStrategy {
calculate(order) { return order.total * 1.025; }
}
// 加新方式:新写一个类,旧代码完全不动
class PaymentProcessor {
constructor(private strategy: PaymentStrategy) {}
calculate(order) { return this.strategy.calculate(order); }
}
OCP 是面向对象的核心价值 —— 通过抽象让代码"能扩展但不容易改坏"。设计模式(策略、工厂、模板方法)很大程度都是为 OCP 服务的。
L - 里氏替换原则(Liskov Substitution)
"子类能替换父类,且不破坏程序的正确性"。Barbara Liskov 1987 年提出。
// 违反 LSP:经典反例
class Rectangle {
setWidth(w) { this.width = w; }
setHeight(h) { this.height = h; }
area() { return this.width * this.height; }
}
class Square extends Rectangle {
setWidth(w) { this.width = w; this.height = w; }
setHeight(h) { this.width = h; this.height = h; }
}
// 客户代码:
function test(r: Rectangle) {
r.setWidth(5);
r.setHeight(4);
assert(r.area() === 20); // Rectangle 通过,Square 失败(area=16)
}
// Square 不能正确替换 Rectangle
说明"正方形 is-a 长方形"在数学上对,但在 OO 设计上错。原因:Rectangle 的契约里"setWidth 只影响 width",Square 违反了这个契约。
违反 LSP 的常见信号
- 子类抛出父类不抛出的异常。
- 子类需要更严格的输入(比父类更窄)。
- 子类返回更宽松的输出(违反父类承诺)。
- 子类用
throw new UnsupportedOperationException()拒绝实现某些方法。
I - 接口隔离原则(Interface Segregation)
"不要强迫客户端依赖它不用的方法"。说人话:小接口比大接口好。
// 违反 ISP:Worker 接口太胖
interface Worker {
work(): void;
eat(): void;
sleep(): void;
}
// 机器人也是 Worker?
class Robot implements Worker {
work() { ... }
eat() { throw new Error("机器人不吃饭"); } // 被迫实现
sleep() { throw new Error("机器人不睡觉"); }
}
// 遵循 ISP:拆成小接口
interface Workable { work(): void; }
interface Feedable { eat(): void; }
interface Sleepable { sleep(): void; }
class Human implements Workable, Feedable, Sleepable { ... }
class Robot implements Workable { ... }
Go 语言的"小接口"哲学是 ISP 的极致体现 —— io.Reader 只有一个 Read 方法。Java 的"函数式接口"(Runnable / Supplier)也是这个思路。
D - 依赖倒置原则(Dependency Inversion)
"高层模块不应依赖低层模块,两者都应该依赖抽象"。
// 违反 DIP:高层 OrderService 直接 new 低层 MySQLOrderRepo
class OrderService {
private repo = new MySQLOrderRepository(); // 硬编码依赖
placeOrder(order) {
this.repo.save(order);
}
}
// 换 PostgreSQL?重写 OrderService。测试?没法 mock。
// 遵循 DIP:依赖接口
interface OrderRepository {
save(order): void;
}
class OrderService {
constructor(private repo: OrderRepository) {}
placeOrder(order) {
this.repo.save(order);
}
}
class MySQLOrderRepository implements OrderRepository { ... }
class PostgreSQLOrderRepository implements OrderRepository { ... }
// 注入(IoC)
const service = new OrderService(new MySQLOrderRepository());
DIP 是 Spring / Guice / Angular 等 IoC 框架的理论基础。它让代码可以根据配置切换实现,且测试时能 mock 依赖。
SOLID 综合应用
把五个原则放一起的真实例子 —— 订单服务:
// 接口(抽象)— SRP + ISP
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
interface NotificationService {
notify(user: User, message: string): Promise<void>;
}
interface PaymentGateway {
charge(amount: number, method: PaymentMethod): Promise<PaymentResult>;
}
// 业务逻辑只依赖抽象 — DIP
class OrderService {
constructor(
private repo: OrderRepository,
private notify: NotificationService,
private payment: PaymentGateway,
) {}
async placeOrder(order: Order) { // SRP:只编排"下单"流程
const result = await this.payment.charge(order.total, order.paymentMethod);
if (!result.success) throw new PaymentError(result.error);
order.status = 'paid';
await this.repo.save(order);
await this.notify.notify(order.user, '订单已支付');
}
}
// 各种实现(可以替换 — LSP)
class MySQLOrderRepository implements OrderRepository { ... }
class MemoryOrderRepository implements OrderRepository { ... } // 测试用
class EmailNotification implements NotificationService { ... }
class SmsNotification implements NotificationService { ... }
class CompositeNotification implements NotificationService { // 装饰器,OCP
constructor(private notifiers: NotificationService[]) {}
async notify(user, message) {
await Promise.all(this.notifiers.map(n => n.notify(user, message)));
}
}
// 测试很容易 — 没有 DIP 测试是噩梦
test('placeOrder success', async () => {
const repo = new MemoryOrderRepository();
const notify = new MockNotificationService();
const payment = { charge: jest.fn().mockResolvedValue({ success: true }) };
const service = new OrderService(repo, notify, payment);
await service.placeOrder(testOrder);
expect(notify.calls).toHaveLength(1);
});
SOLID 不是教条
每个原则都有"过度"的风险:
- SRP 过度:一个 30 行的 class 拆成 5 个,逻辑分散难追踪。
- OCP 过度:为不存在的需求做抽象,YAGNI(You Aren't Gonna Need It)。
- DIP 过度:所有依赖都接口化,简单的工具类也要 mock,测试和代码都变复杂。
SOLID 的核心精神是"让代码容易改"。但代码的"容易改"和"容易读"是平衡 —— 过度抽象让代码难读。当代码真的需要改时再抽象,不要为了 SOLID 而 SOLID。
实战:何时该拆
问自己几个问题:
- 这个类是不是经常因为不同原因改?(违反 SRP 的信号)
- 加新功能需要改老代码吗?(违反 OCP 的信号)
- 子类能不能在任何使用父类的地方替换?(LSP 检查)
- 接口里有没有"多数实现都不用"的方法?(违反 ISP 的信号)
- 测试时需要真实数据库 / 网络 / 文件系统吗?(违反 DIP 的信号)
写在最后
SOLID 是写好面向对象代码的"常识"—— 不是 fancy 高级技巧,而是基本素养。背熟五个字母没用,要做到肌肉记忆:写一段代码顺手就符合 SOLID。这是经验积累的过程,不是看一篇文章就会。新人 review 老人代码、老人重构新人代码,都按 SOLID 来衡量,几个月后整个团队代码质量都会提升。
一图看懂
SOLID 之 DIP 一图看懂:
—— 别看了 · 2026