讲解

如果说设计模式是「招式」,SOLID 原则就是「内功心法」。它是五条面向对象设计原则的首字母缩写,由 Robert C. Martin 归纳推广。装修队里有句行话「活儿好不好,看隐蔽工程」——代码也一样,表面上功能都对,内部结构是不是经得起修改,要靠这些原则来度量。

单一职责原则(SRP):一个类只应该有一个引起它变化的原因。把「计算订单金额」和「把订单存进数据库」揉在一个类里,改任何一边都可能碰坏另一边;拆开之后,每个类的变化原因单一而清晰。判断标准很朴素:用一句话描述这个类是干什么的,如果句子里出现了「和」「以及」,多半职责超了。

开闭原则(OCP):对扩展开放,对修改关闭。新增需求时应该通过「新增代码」实现,而不是修改已有的、已被测试覆盖的代码。实现手段就是面向抽象编程——调用方依赖接口,新需求实现新类。

里氏替换原则(LSP):子类必须能无缝替换父类出现的位置。经典反例是「正方形继承长方形」——把长方形的长和宽设成不同值对长方形合法,正方形却会崩溃,说明这个继承关系违反了 LSP。通俗地说:子类不能削弱父类的承诺。

接口隔离原则(ISP):客户端不应该被迫依赖它用不到的方法。一个又宽又全的「胖接口」强迫所有实现者写一堆空方法;拆成多个窄接口,每个实现者只挑自己真正支持的。TypeScript 的接口系统让这条原则落实起来几乎零成本。

依赖倒置原则(DIP):高层模块不应依赖低层模块,双方都应依赖抽象;抽象不应依赖细节,细节应依赖抽象。翻译成人话:业务逻辑不要直接 new 一个具体的数据库、具体的支付通道,而是依赖一个接口,由外部把具体实现「注入」进来。这是后面工厂、依赖注入等一整族实践的理论基础。

示例

用一个订单结算的例子同时演示开闭原则和依赖倒置:Order 只依赖 DiscountPolicy 抽象,新增折扣策略时 Order 一行都不用改:

import assert from 'node:assert/strict';

// 抽象:订单只依赖它(依赖倒置)
interface DiscountPolicy {
  discount(price: number): number;
}

class NoDiscount implements DiscountPolicy {
  discount(price: number): number {
    return price;
  }
}

class VipDiscount implements DiscountPolicy {
  constructor(private readonly rate: number) {}
  discount(price: number): number {
    return Math.round(price * this.rate * 100) / 100;
  }
}

class Order {
  constructor(private readonly policy: DiscountPolicy) {}

  checkout(items: number[]): number {
    const total = items.reduce((sum, p) => sum + p, 0);
    return this.policy.discount(total);
  }
}

assert.equal(new Order(new NoDiscount()).checkout([100, 50]), 150);
assert.equal(new Order(new VipDiscount(0.8)).checkout([100, 50]), 120);

// 新增「满减」策略:只新增类,不修改 Order(开闭原则)
class FullReduction implements DiscountPolicy {
  discount(price: number): number {
    return price >= 200 ? price - 40 : price;
  }
}

assert.equal(new Order(new FullReduction()).checkout([120, 100]), 180);
console.log('普通 150,VIP 120,满减 180 —— 新增策略无需改动 Order');

注意两个细节:一是 Order 的构造函数接收的是 DiscountPolicy 接口而不是某个具体类(依赖倒置),具体用哪个策略由调用方决定;二是「满减」需求出现时,我们新增的 FullReduction 类没有触碰 Order 和既有策略(开闭原则),老代码的测试继续有效。五条原则经常这样联手出现:DIP 告诉你依赖接口,OCP 是这个做法换来的结果。

常见坑

  • 把单一职责理解成「一个类只做一件事」的极端化:拆到过细会制造几十个小类,判断标准是「变化原因」而不是方法个数。
  • 为了开闭原则到处抽象:对永远不会变化的简单逻辑也建接口,是无谓的间接层;先明确哪里真的会变,再在那里留扩展点。
  • 继承时只复用代码、不管语义:LSP 检查的是行为承诺能否替换,不是字段像不像。子类抛出父类不会抛的异常,就是典型的破坏。
  • 接口隔离做成「一个方法一个接口」:接口应该按使用者的内聚需求划分,过碎和过胖一样难用。
  • 把依赖倒置和依赖注入框架划等号:DIP 是设计原则,手工通过构造函数传参就已经在实践它;框架只是规模化后的工具。

小结

SOLID 是评价面向对象设计的五把尺子:单一职责、开闭、里氏替换、接口隔离、依赖倒置。后面每讲一个模式,都可以回头问一句「它体现了哪条原则」。下一章进入第一个创建型模式:工厂方法。