SOLID 原则完全指南:从五个字母到工程肌肉记忆

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。

实战:何时该拆

问自己几个问题:

  1. 这个类是不是经常因为不同原因改?(违反 SRP 的信号)
  2. 加新功能需要改老代码吗?(违反 OCP 的信号)
  3. 子类能不能在任何使用父类的地方替换?(LSP 检查)
  4. 接口里有没有"多数实现都不用"的方法?(违反 ISP 的信号)
  5. 测试时需要真实数据库 / 网络 / 文件系统吗?(违反 DIP 的信号)

写在最后

SOLID 是写好面向对象代码的"常识"—— 不是 fancy 高级技巧,而是基本素养。背熟五个字母没用,要做到肌肉记忆:写一段代码顺手就符合 SOLID。这是经验积累的过程,不是看一篇文章就会。新人 review 老人代码、老人重构新人代码,都按 SOLID 来衡量,几个月后整个团队代码质量都会提升。

一图看懂

SOLID 之 DIP 一图看懂:

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

微前端完全指南:从 qiankun 到 Module Federation 的实战

2026-5-15 17:38:32

技术教程

DDD 领域驱动设计完全指南:从限界上下文到聚合的工程实践

2026-5-15 17:43:37

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索