写出可维护的 JS 代码:SOLID 从入门到进阶

1 阅读8分钟

SOLID 原则完整指南

—— 从初级到中高级的面向对象设计进阶之路

使用 JavaScript(ES6+)实现 · 面向可维护、可扩展系统的构建


🔖 目录

  1. 引言:为什么需要 SOLID?
  2. SOLID 概述
  3. 单一职责原则(SRP)
  4. 开闭原则(OCP)
  5. 里氏替换原则(LSP)
  6. 接口隔离原则(ISP)
  7. 依赖倒置原则(DIP)
  8. 综合案例:用户注册系统
  9. 常见误区与反模式
  10. 如何在项目中逐步引入 SOLID
  11. 总结与进阶建议

1. 引言:为什么需要 SOLID?

随着项目规模增长,代码逐渐变得“难以修改”、“一改就崩”、“新人看不懂”,这是典型的 软件腐化(Code Rot) 现象。

SOLID 是一组由 Robert C. Martin 提出的设计原则,旨在帮助开发者写出:

  • ✅ 更易维护的代码
  • ✅ 更易测试的模块
  • ✅ 更易扩展的功能
  • ✅ 更低耦合的结构

💡 目标不是“完美代码”,而是“可持续演进的系统”

即使你使用的是动态语言如 JavaScript,这些原则依然适用,并能显著提升工程质量和协作效率。


2. SOLID 概述

缩写名称中文含义核心思想
SSingle Responsibility Principle单一职责原则一个类只做一件事
OOpen/Closed Principle开闭原则对扩展开放,对修改关闭
LLiskov Substitution Principle里氏替换原则子类应能安全替换父类
IInterface Segregation Principle接口隔离原则客户端不应依赖不需要的方法
DDependency Inversion Principle依赖倒置原则高层和底层都依赖抽象

⚠️ 注意:JavaScript 没有原生接口和强类型,但可通过约定、继承、多态和依赖注入模拟。


3. 单一职责原则(SRP)

✅ 定义

一个模块(类、函数、文件)应该只有一个引起它变化的原因。

📌 初级理解

  • “这个类太长了” → 可能违反 SRP
  • “改一个功能影响另一个功能” → 职责混杂

❌ 反例:全能型“上帝类”

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  // 职责1:业务逻辑
  isValid() {
    return this.email.includes("@");
  }

  // 职责2:持久化
  save() {
    db.save("users", this);
  }

  // 职责3:通知
  sendWelcomeEmail() {
    emailService.send(this.email, "Welcome!", "Hello...");
  }

  // 职责4:日志
  logAction(action) {
    console.log(`[${new Date()}] ${this.name}: ${action}`);
  }
}

一旦数据库换 ORM、邮件换服务、日志要写入文件——都要改这个类!

✅ 正确做法:拆分职责

// 实体类(仅数据)
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  isValid() {
    return this.email.includes("@");
  }
}

// 数据访问层
class UserRepository {
  save(user) {
    db.save("users", user);
  }
}

// 邮件服务
class EmailService {
  sendWelcomeEmail(email) {
    this.send(email, "Welcome!", "Hello...");
  }

  send(to, subject, body) {
    // 发送逻辑
  }
}

// 日志服务
class Logger {
  log(user, action) {
    console.log(`[${new Date()}] ${user.name}: ${action}`);
  }
}

✅ 使用方式(解耦后)

const user = new User("Alice", "alice@example.com");

if (user.isValid()) {
  new UserRepository().save(user);
  new EmailService().sendWelcomeEmail(user.email);
  new Logger().log(user, "registered");
}

✅ 收益

  • 各模块独立测试
  • 修改不影响其他部分
  • 易于复用(如 EmailService 可用于订单通知)

4. 开闭原则(OCP)

✅ 定义

软件实体应对扩展开放,对修改关闭

📌 初级理解

  • 添加新功能时,不要去改旧代码
  • 应该通过“新增”而不是“修改”来实现

❌ 反例:条件判断堆叠

function calculateShipping(order) {
  if (order.country === "US") {
    return order.amount * 0.1;
  } else if (order.country === "CA") {
    return order.amount * 0.15;
  } else if (order.country === "EU") {
    return order.amount * 0.2;
  }
  // 新增国家?必须改这里!→ 违反 OCP
}

✅ 正确做法:策略模式 + 多态

class ShippingStrategy {
  calculate(order) {
    throw new Error("Must override");
  }
}

class USShipping extends ShippingStrategy {
  calculate(order) {
    return order.amount * 0.1;
  }
}

class CAShipping extends ShippingStrategy {
  calculate(order) {
    return order.amount * 0.15;
  }
}

class EUShipping extends ShippingStrategy {
  calculate(order) {
    return order.amount * 0.2;
  }
}

// 扩展时不需修改原有逻辑
class AsiaShipping extends ShippingStrategy {
  calculate(order) {
    return order.amount * 0.08;
  }
}

// 统一入口
function calculateShipping(strategy, order) {
  return strategy.calculate(order); // 多态调用
}

✅ 使用

const order = { amount: 100 };
const usStrategy = new USShipping();
const asiaStrategy = new AsiaShipping();

console.log(calculateShipping(usStrategy, order));     // 10
console.log(calculateShipping(asiaStrategy, order));   // 8

✅ 收益

  • 新增策略无需修改 calculateShipping
  • 易于单元测试每个策略
  • 支持运行时切换

5. 里氏替换原则(LSP)

✅ 定义

子类对象应当可以在任何使用父类的地方被替换,而不改变程序的正确性。

📌 初级理解

  • 不要让子类“破坏”父类的行为契约
  • “看起来像鸭子,叫起来却是猫” → 危险!

❌ 反例:Square 继承 Rectangle 导致行为异常

class Rectangle {
  setWidth(w) { this.width = w; }
  setHeight(h) { this.height = h; }
  getArea() { 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 testArea(rect) {
  rect.setWidth(5);
  rect.setHeight(4); // 用户期望面积是 20
  console.log(rect.getArea()); // 实际输出 16!
}

testArea(new Rectangle()); // 20 ✅
testArea(new Square());    // 16 ❌ 行为不一致

虽然语法上可以替换,但语义上已破坏预期。

✅ 解决方案

  • 避免错误的 IS-A 关系
  • 使用组合优于继承
  • 或基于共同抽象派生
class Shape {
  getArea() { throw new Error("Not implemented"); }
}

class Rectangle extends Shape {
  constructor(w, h) { this.width = w; this.height = h; }
  getArea() { return this.width * this.height; }
}

class Square extends Shape {
  constructor(s) { this.side = s; }
  getArea() { return this.side ** 2; }
}

现在两者都实现 Shape 接口,可被统一处理且行为可靠。


6. 接口隔离原则(ISP)

✅ 定义

客户端不应该被迫依赖它不需要的接口方法。

📌 初级理解

  • 不要强迫一个类实现一堆空方法
  • 小而专的接口 > 大而全的接口

❌ 反例:机器人被迫“吃”

class Worker {
  work() {}
  eat() {}
}

class HumanWorker extends Worker {
  work() { console.log("Human working"); }
  eat() { console.log("Human eating"); }
}

class RobotWorker extends Worker {
  work() { console.log("Robot working"); }
  eat() {
    throw new Error("Robots don't eat!"); // 被迫实现无意义方法
  }
}

✅ 正确做法:拆分接口

class Workable {
  work() { throw new Error("Not implemented"); }
}

class Eatable {
  eat() { throw new Error("Not implemented"); }
}

class HumanWorker extends Workable {
  work() { /* ... */ }
  eat() { /* ... */ } // 实现两个
}

class RobotWorker extends Workable {
  work() { /* ... */ }
  // 不实现 eat → 干净简洁
}

或者使用 Mixin 模式(JS 特色):

const CanWork = (superClass) =>
  class extends superClass {
    work() { console.log(`${this.name} is working`); }
  };

const CanEat = (superClass) =>
  class extends superClass {
    eat() { console.log(`${this.name} is eating`); }
  };

class Base {}
class Human extends CanEat(CanWork(Base)) {}
class Robot extends CanWork(Base) {}

new Human().work(); // OK
new Robot().work(); // OK
// new Robot().eat(); → TypeError ✅ 安全

7. 依赖倒置原则(DIP)

✅ 定义

高层模块不应依赖低层模块,二者都应依赖抽象。

📌 初级理解

  • 不要硬编码依赖具体类
  • 用“接口”或“抽象类”作为中间层
  • 支持依赖注入(DI)

❌ 反例:开关直接依赖灯泡

class LightBulb {
  turnOn() { console.log("💡 On"); }
}

class Switch {
  constructor() {
    this.device = new LightBulb(); // 硬编码依赖
  }

  toggle() {
    this.device.turnOn();
  }
}

想换成风扇?必须改 Switch 类 → 紧耦合!

✅ 正确做法:依赖抽象 + 依赖注入

// 抽象:所有可开关设备
class SwitchableDevice {
  turnOn() { throw new Error("Not implemented"); }
}

// 具体实现
class LightBulb extends SwitchableDevice {
  turnOn() { console.log("💡 Light on"); }
}

class Fan extends SwitchableDevice {
  turnOn() { console.log("🌀 Fan on"); }
}

// 高层模块依赖抽象
class Switch {
  constructor(device) { // 依赖注入
    if (!(device instanceof SwitchableDevice)) {
      throw new Error("Device must be Switchable");
    }
    this.device = device;
  }

  toggle() {
    this.device.turnOn();
  }
}

✅ 使用

const bulb = new LightBulb();
const fan = new Fan();

const switch1 = new Switch(bulb);
const switch2 = new Switch(fan);

switch1.toggle(); // 💡 Light on
switch2.toggle(); // 🌀 Fan on

✅ 完全解耦,支持任意 SwitchableDevice

💡 提示:可用容器管理依赖(如 Awilix、InversifyJS),进一步自动化 DI。


8. 综合案例:用户注册系统

我们来构建一个符合所有 SOLID 原则的小型注册系统。

功能需求

  • 用户注册
  • 验证邮箱格式
  • 保存到数据库
  • 发送欢迎邮件
  • 记录日志

✅ 架构设计(遵循 SOLID)

src/
├── entities/
   └── User.js            SRP: 仅定义数据
├── services/
   ├── UserService.js     SRP: 控制流程
   ├── EmailService.js    SRP + ISP: 仅发送邮件
   └── Logger.js          SRP: 日志记录
├── repositories/
   └── UserRepository.js  SRP: 数据访问
└── strategies/
    └── ValidationStrategy.js  OCP: 可扩展验证规则

代码实现

// entities/User.js
export class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  isValid() {
    return this.email.includes("@");
  }
}
// services/EmailService.js
export class EmailService {
  send(to, subject, body) {
    console.log(`📧 Sending to ${to}: ${subject}`);
  }

  sendWelcomeEmail(user) {
    this.send(user.email, "Welcome!", `Hi ${user.name}`);
  }
}
// repositories/UserRepository.js
export class UserRepository {
  save(user) {
    console.log(`💾 Saved user: ${user.name}`);
  }
}
// services/Logger.js
export class Logger {
  log(message) {
    console.log(`📝 [LOG] ${message}`);
  }
}
// services/UserService.js
export class UserService {
  constructor({ userRepository, emailService, logger }) {
    this.userRepository = userRepository;
    this.emailService = emailService;
    this.logger = logger;
  }

  registerUser(name, email) {
    const user = new User(name, email);

    if (!user.isValid()) {
      throw new Error("Invalid email");
    }

    this.userRepository.save(user);
    this.emailService.sendWelcomeEmail(user);
    this.logger.log(`${name} registered`);

    return user;
  }
}

✅ 使用(依赖注入)

const userService = new UserService({
  userRepository: new UserRepository(),
  emailService: new EmailService(),
  logger: new Logger()
});

userService.registerUser("Bob", "bob@example.com");

✅ 完全符合:

  • SRP:每个类职责单一
  • OCP:可扩展验证、存储方式等
  • LSP:所有服务实现统一接口
  • ISP:无冗余方法
  • DIP:高层模块依赖抽象构造参数

9. 常见误区与反模式

误区正确做法
把所有方法塞进一个工具类拆分为领域相关模块
用继承强行复用代码优先使用组合
忽视类型检查导致运行时错误使用 TypeScript 或运行时断言
过度设计早期项目在重构阶段逐步引入 SOLID
认为“只有 Java/C# 才需要 SOLID”所有 OO 设计语言都受益

10. 如何在项目中逐步引入 SOLID

🔄 渐进式改进策略

阶段行动
初级识别“上帝类”,拆分函数和文件
中级引入服务层、仓库层,分离关注点
高级使用依赖注入容器,抽象核心逻辑
团队级制定架构规范,代码评审中检查职责划分

✅ 推荐步骤

  1. 从最混乱的模块开始重构
  2. 写单元测试保护现有功能
  3. 拆分职责 → 提取类 → 引入接口抽象
  4. 使用构造函数注入依赖
  5. 回归测试,确保行为不变

11. 总结与进阶建议

✅ SOLID 的终极价值

原则带来的收益
SRP更高的内聚性,更低的维护成本
OCP系统更灵活,易于功能迭代
LSP多态安全,避免隐藏 bug
ISP接口清晰,客户端轻量
DIP解耦模块,支持测试与替换

🚀 进阶建议

  • 学习 设计模式(工厂、策略、观察者、装饰器)配合 SOLID 使用
  • 使用 TypeScript 强化接口与类型约束
  • 引入 依赖注入框架(如 InversifyJS)
  • 结合 Clean Architecture / Hexagonal Architecture 构建企业级应用
  • 编写 单元测试 + Mock 验证模块独立性

📎 附录:推荐资源

  • 📘 《Agile Software Development, Principles, Patterns, and Practices》– Robert C. Martin
  • 🎥 YouTube: “SOLID Principles Explained in JavaScript” – Web Dev Simplified
  • 🧪 工具:Awilix(DI Container for Node.js)
  • 💻 框架:NestJS(基于 TypeScript 和 DIP 的后端框架)

🙌 结语

“优秀的程序员写代码,伟大的程序员设计系统。”

SOLID 不是银弹,但它是指南针——指引我们从“能跑就行”走向“长期可维护”。

无论你是前端、Node.js 后端还是全栈开发者,掌握 SOLID 都将让你在职业道路上走得更远、更稳。

📌 记住一句话:

“写让人容易修改的代码,比写让人看懂的代码更重要。”