您现在的位置是:首页 > 文章详情

理解 SOLID 原则:编写更简洁的 JavaScript 代码

日期:2025-08-26点击:3

编写简洁、可维护的代码是构建可扩展应用的关键。由罗伯特·C·马丁(Bob 大叔)提出的 SOLID 原则,是五条核心设计准则,能帮助开发者更好地组织代码、减少漏洞,并降低后续修改的难度。

本文将逐一拆解每条原则,用简单的 JavaScript 示例演示,并解释其重要性。

🧱 SOLID 分别代表什么?

SOLID 是五条面向对象设计原则的首字母缩写:

  • S --- 单一职责原则(Single Responsibility Principle, SRP)
  • O --- 开闭原则(Open/Closed Principle, OCP)
  • L --- 里氏替换原则(Liskov Substitution Principle, LSP)
  • I --- 接口隔离原则(Interface Segregation Principle, ISP)
  • D --- 依赖倒置原则(Dependency Inversion Principle, DIP)

下面我们逐一展开讲解。

✅ 1. 单一职责原则(Single Responsibility Principle, SRP)

定义:一个模块、类或函数,只应有一个修改的理由。

通俗理解:每个函数或类只做一件事。这能让代码更易测试、复用性更高,且更易维护。

我们先看一个违反 SRP 的反面示例,再对比遵循原则的重构版本。

🚫 反面示例:违反 SRP

function processUserRegistration(userData) { // 1. 验证输入 if (!userData.email.includes('@')) { throw new Error('Invalid email'); } // 2. 保存用户到数据库(模拟操作) const userId = Math.floor(Math.random() * 1000); // 3. 发送欢迎邮件(模拟操作) console.log(`Sending welcome email to ${userData.email}`); return userId; } 

问题所在

这个函数同时承担了三个职责:

  1. 验证输入合法性
  2. 保存数据到数据库
  3. 发送欢迎邮件

每个职责的修改理由都不同(比如业务规则变更、数据库逻辑调整、邮件服务升级),违背了"单一职责"的核心要求。

✅ 正面示例:遵循 SRP

将不同职责拆分到独立函数中:

// 职责1:仅验证用户输入 function validateUser(userData) { if (!userData.email.includes('@')) { throw new Error('Invalid email'); } } // 职责2:仅负责数据库存储 function saveUserToDatabase(userData) { const userId = Math.floor(Math.random() * 1000); // 模拟数据库调用 console.log(`User saved with ID ${userId}`); return userId; } // 职责3:仅处理邮件发送 function sendWelcomeEmail(email) { console.log(`Sending welcome email to ${email}`); } // 协调函数:整合流程,不承担具体职责 function registerUser(userData) { validateUser(userData); const userId = saveUserToDatabase(userData); sendWelcomeEmail(userData.email); return userId; } 

优势

  • 每个函数目标明确,职责单一
  • 可独立测试(如单独测试输入验证逻辑)
  • 若邮件逻辑变更,只需修改 sendWelcomeEmail,不影响其他功能

🧪 使用示例

const user = { email: 'alice@example.com' }; const userId = registerUser(user); console.log(`New user ID: ${userId}`); 

遵循 SRP 能让代码:

  • 更易阅读和重构
  • 模块化程度更高,复用性更强
  • 需求变更时,引入漏洞的风险更低

即使在小型 JavaScript 项目中,SRP 也能培养良好的编码习惯,提升长期可维护性。编写代码时,不妨多问自己:"这个函数是不是做了不止一件事?"如果答案是肯定的,就拆分它。

✅ 2. 开闭原则(Open/Closed Principle, OCP)

定义 :由伯特兰·迈耶提出,是 SOLID 原则的第二条,核心要求为:

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

通俗理解:添加新功能时,无需修改已有代码。这种方式能减少引入漏洞的风险,同时提升代码复用性和灵活性。

下面通过 JavaScript 示例,对比违反和遵循 OCP 的实现方式。

❌ 反面示例(违反 OCP)

function getDiscountedPrice(customerType, price) { if (customerType === 'regular') { return price * 0.9; // 普通用户 9 折 } else if (customerType === 'vip') { return price * 0.8; // VIP 用户 8 折 } else if (customerType === 'platinum') { return price * 0.7; // 铂金用户 7 折 } else { return price; // 无折扣 } } 

问题所在

  • 新增用户类型(如"黄金用户")时,必须修改 getDiscountedPrice 函数
  • 违反"对修改关闭"的要求,修改过程可能破坏已有逻辑
  • 逻辑高度耦合,扩展性差

✅ 正面示例(遵循 OCP)

通过"策略模式"重构,用类的继承实现扩展:

// 抽象基类:定义折扣策略接口 class DiscountStrategy { getDiscount(price) { return price; // 默认无折扣 } } // 普通用户折扣策略(扩展) class RegularCustomerDiscount extends DiscountStrategy { getDiscount(price) { return price * 0.9; } } // VIP 用户折扣策略(扩展) class VIPCustomerDiscount extends DiscountStrategy { getDiscount(price) { return price * 0.8; } } // 铂金用户折扣策略(扩展) class PlatinumCustomerDiscount extends DiscountStrategy { getDiscount(price) { return price * 0.7; } } // 使用入口:对修改关闭,仅依赖抽象基类 function getDiscountedPrice(discountStrategy, price) { return discountStrategy.getDiscount(price); } // 实际使用 const customer = new VIPCustomerDiscount(); console.log(getDiscountedPrice(customer, 100)); // 输出 80(8 折) 

优化点在哪里

  • 新增折扣策略时,只需创建新的子类继承 DiscountStrategy,无需修改已有代码
  • 符合 OCP 核心:getDiscountedPrice 函数对修改关闭,对扩展开放(通过多态实现)
  • 逻辑解耦,易测试、易扩展

🚀 OCP 在 JavaScript 中的实际应用

  • 中间件系统(如 Express.js):添加新中间件时,无需修改框架核心逻辑
  • 插件架构(如 Webpack、ESLint):通过插件扩展功能,不改动工具内部代码
  • 表单验证库:新增验证规则时,只需注册规则,无需重写验证器核心

✅ 3. 里氏替换原则(Liskov Substitution Principle, LSP)

定义 :由芭芭拉·里氏提出,是 SOLID 原则的第三条,核心要求为:

子类对象应能替换父类对象,且不影响程序的正确性

通俗理解:子类的行为应与父类一致。如果需要检查对象类型,或重写方法时破坏了预期行为,就可能违反 LSP。

下面用 JavaScript 示例演示 LSP 的应用。

❌ 反面示例(违反 LSP)

// 父类:定义"鸟"的行为 class Bird { fly() { console.log('Flying'); } } // 子类:企鹅(继承自鸟,但无法飞行) class Penguin extends Bird { fly() { throw new Error("Penguins can't fly!"); // 重写方法但破坏预期行为 } } // 通用函数:假设所有"鸟"都能飞行 function makeBirdFly(bird) { bird.fly(); } // 测试 const genericBird = new Bird(); const penguin = new Penguin(); makeBirdFly(genericBird); // ✅ 输出 "Flying" makeBirdFly(penguin); // ❌ 抛出错误 

问题所在

  • Penguin 继承自 Bird,但重写的 fly 方法与父类预期行为冲突(父类默认"能飞")
  • makeBirdFly 函数依赖"鸟能飞"的假设,但 Penguin 无法满足,导致程序出错
  • 违反 LSP:子类不能安全替换父类

✅ 正面示例(遵循 LSP)

按"行为"设计继承结构,而非单纯按"类型":

// 父类:定义"鸟"的通用行为(所有鸟都会下蛋) class Bird { layEgg() { console.log('Laying an egg'); } } // 子类:会飞的鸟(拆分"飞行"行为) class FlyingBird extends Bird { fly() { console.log('Flying'); } } // 子类:企鹅(不会飞,仅继承鸟的通用行为) class Penguin extends Bird { swim() { console.log('Swimming'); } } // 子类:麻雀(会飞,继承 FlyingBird) class Sparrow extends FlyingBird {} // 通用函数:仅接收"会飞的鸟" function letBirdFly(bird) { bird.fly(); } // 测试 const sparrow = new Sparrow(); letBirdFly(sparrow); // ✅ 输出 "Flying" const penguin = new Penguin(); // letBirdFly(penguin); ❌ 若调用会报错,但设计上已避免这种用法 

优化点在哪里

  • 拆分 Bird 和 FlyingBird,确保只有"会飞的鸟"才会被传入 letBirdFly
  • Penguin 仍属于 Bird,但不承担"飞行"职责,符合实际行为
  • 子类未破坏父类的行为预期,可安全替换父类使用

🚀 LSP 在 JavaScript 中的实际应用

  • React 组件:组件继承基类或使用 Hooks 时,不应破坏复用或组合的预期行为
  • Promise 链:返回值需符合预期类型(如不随意混合同步/异步逻辑)
  • 事件处理器/中间件:需遵守约定(如 Express 中间件需调用 next())

✅ 核心要点

在 JavaScript 中遵循 LSP,需注意:

  1. 子类不应重写方法以抛出错误或大幅改变行为
  2. 用"鸭子类型"(Duck Typing)非正式地定义接口,确保行为一致性
  3. 按"能力"设计,而非按"类型"(如拆分 FlyingBird 和 Bird)

即使没有静态类型检查,JavaScript 开发者也能通过合理设计类层级、明确行为约定和可替换性,从 LSP 中获益。

✅ 4. 接口隔离原则(Interface Segregation Principle, ISP)

定义 :SOLID 原则的第四条,核心要求为:

客户端不应被迫依赖它不需要的接口

JavaScript 场景理解:不要让函数、类或对象实现无用的功能。应将庞大、通用的接口拆分为小型、针对性的接口。

这种设计能提升可维护性、避免代码臃肿,并让单个行为的扩展和测试更简单。

❌ 反面示例(违反 ISP)

// 庞大的"机器"接口:包含打印、扫描、传真功能 class Machine { print() { throw new Error('Not implemented'); } scan() { throw new Error('Not implemented'); } fax() { throw new Error('Not implemented'); } } // 老式打印机:仅支持打印,但被迫继承所有方法 class OldPrinter extends Machine { print() { console.log('Printing...'); } // scan() 和 fax() 未实现,却必须继承 } 

问题所在

  • OldPrinter 仅支持打印,却被迫继承 scan 和 fax 方法
  • 无用方法需保留空实现或抛出错误,易导致运行时混乱
  • 违反 ISP:客户端被迫依赖不需要的接口

✅ 正面示例(遵循 ISP)

按职责拆分接口,用"组合"替代"继承":

// 小型接口1:仅处理打印 class Printer { print() { console.log('Printing...'); } } // 小型接口2:仅处理扫描 class Scanner { scan() { console.log('Scanning...'); } } // 小型接口3:仅处理传真 class FaxMachine { fax() { console.log('Faxing...'); } } // 现代打印机:组合多个接口,拥有完整功能 class ModernPrinter { constructor() { this.printer = new Printer(); this.scanner = new Scanner(); this.faxMachine = new FaxMachine(); } print() { this.printer.print(); } scan() { this.scanner.scan(); } fax() { this.faxMachine.fax(); } } // 基础打印机:仅组合"打印"接口 class BasicPrinter { constructor() { this.printer = new Printer(); } print() { this.printer.print(); } } 

优化点在哪里

  • 功能模块化:每个接口小型且目标明确
  • BasicPrinter 仅依赖所需的"打印"功能,无冗余
  • ModernPrinter 通过组合扩展功能,无需继承无用方法
  • 符合 ISP:没有类被迫实现不需要的功能

🚀 ISP 在 JavaScript 中的实际应用

  • React 组件:避免传递庞大的 props 对象,只传组件必需的属性
  • 模块化服务:拆分服务职责(如 StorageService 不应包含 sendEmail 方法)
  • Node.js 模块:按用途拆分工具函数(如 mathUtils.js 不应包含 parseQueryString)

✂️ 保持接口精简且目标明确

在 JavaScript 中遵循 ISP,可遵循以下建议:

  1. 将庞大的接口(或对象)拆分为小型、用途单一的单元
  2. 不强迫组件、函数或类实现超出需求的功能
  3. 尽可能用"组合"替代"继承"

应用 ISP 后,代码会更简洁、聚焦,且随着项目增长,可维护性会显著提升。

✅ 5. 依赖倒置原则(Dependency Inversion Principle, DIP)

定义:SOLID 原则的最后一条,核心要求为:

  1. 高层模块不应依赖低层模块,两者都应依赖抽象;
  2. 抽象不应依赖细节,细节应依赖抽象。

🧠 通俗解释

核心业务逻辑(高层代码)不应与具体实现细节(如 API、数据库)强耦合。相反,两者都应依赖统一的抽象(如接口、基类)。

这种设计能提升灵活性、可测试性,并实现关注点分离。

❌ 反面示例(违反 DIP)

// 低层模块:具体的 MySQL 数据库实现 class MySQLDatabase { save(data) { console.log('Saving data to MySQL:', data); } } // 高层模块:用户服务(强耦合 MySQL 实现) class UserService { constructor() { this.db = new MySQLDatabase(); // 硬编码依赖低层模块 } registerUser(user) { this.db.save(user); } } 

问题所在

  • UserService 与 MySQLDatabase 强耦合,无法替换数据库(如切换到 MongoDB)
  • 测试困难:模拟 MySQLDatabase 需修改核心逻辑
  • 违反 DIP:高层模块直接依赖低层模块的具体实现

✅ 正面示例(遵循 DIP)

通过"抽象基类"解耦,让高层和低层都依赖抽象:

// 抽象基类(抽象):定义数据库接口 class Database { save(data) { throw new Error('Not implemented'); // 抽象方法,由子类实现 } } // 低层实现1:MySQL 数据库(依赖抽象) class MySQLDatabase extends Database { save(data) { console.log('Saving data to MySQL:', data); } } // 低层实现2:内存数据库(依赖抽象,用于测试) class InMemoryDatabase extends Database { constructor() { super(); this.data = []; } save(data) { this.data.push(data); console.log('Saved in memory:', data); } } // 高层模块:用户服务(依赖抽象,不依赖具体实现) class UserService { constructor(database) { this.db = database; // 通过构造函数注入依赖 } registerUser(user) { this.db.save(user); } } 

使用示例

// 可灵活切换数据库实现,无需修改 UserService const db = new MySQLDatabase(); // 或 new InMemoryDatabase() const userService = new UserService(db); userService.registerUser({ name: 'Eve' }); 

🎯 优化点在哪里

  1. UserService 可适配任何遵循 Database 抽象的实现(MySQL、MongoDB 等)
  2. 替换数据库时,无需修改核心业务逻辑
  3. 测试更简单:用 InMemoryDatabase 模拟数据库,无需真实环境

🧭 依赖倒置原则总结

依赖倒置原则通过以下方式提升代码灵活性和可维护性:

  • 优先依赖抽象类/接口,而非具体类
  • 降低层间耦合(高层与低层不直接关联)
  • 便于单元测试(可轻松模拟依赖)
  • 支持依赖替换(实际场景中灵活切换实现)

通过围绕抽象设计,能构建组件可替换、代码易演进的系统。

📦 SOLID 原则最终总结

SOLID 原则并非纯理论,而是经过验证的、实用的面向对象代码设计准则。遵循这些原则,你将获得:

  • 更简洁、模块化的代码
  • 更易测试和调试的逻辑
  • 更低的漏洞引入风险
  • 更高的扩展性和灵活性

SOLID 原则核心要点速查表

原则 核心思想
SRP(单一职责) 一个函数/类只负责一件事
OCP(开闭) 扩展功能无需修改已有代码
LSP(里氏替换) 子类可替换父类,且不破坏程序正确性
ISP(接口隔离) 不强迫客户端依赖无用接口
DIP(依赖倒置) 依赖抽象,而非具体实现

这五条原则共同构成了可维护、可适配、可扩展 JavaScript 应用的基础------即使在小型项目中,也能发挥重要作用。

💼 关于 SOLID 的常见面试题

若你正在准备面试,或想深化对 SOLID 的理解,以下是常见的相关问题及解答思路:

1. SOLID 原则是什么?

SOLID 是五条面向对象设计原则的首字母缩写,包括:

  • S:单一职责原则(SRP)
  • O:开闭原则(OCP)
  • L:里氏替换原则(LSP)
  • I:接口隔离原则(ISP)
  • D:依赖倒置原则(DIP)

它们的核心目标是帮助开发者编写可扩展、可维护、低耦合的代码。

2. 为什么单一职责原则很重要?

SRP 确保模块/类/函数只有一个修改理由,能降低耦合度、提升可维护性。

在 JavaScript 中,常见应用场景是拆分验证、数据存储、通信等逻辑(如用户注册时,分别处理输入校验、数据库保存、邮件发送)。

3. 如何在 JavaScript 中实现开闭原则?

通过多态或高阶函数实现,例如"策略模式":

定义抽象基类/接口,新增功能时创建子类/新策略,而非修改已有代码。

示例:不同用户的折扣计算(新增"黄金用户"时,只需添加新的折扣策略类)。

4. 里氏替换原则在实际应用中是什么意思?

子类应能替代父类使用,且不改变程序行为。

在 JavaScript 中,继承类时需确保重写的方法符合父类约定(如返回类型、参数格式、行为预期)。例如,Penguin 不应继承 Bird 的 fly 方法后抛出错误。

5. 没有正式接口的 JavaScript,如何应用接口隔离原则?

即使没有静态接口,仍可通过"小型、聚焦的抽象"遵循 ISP:

  • 避免设计包含冗余功能的大对象/类
  • 用组合替代继承,按需整合功能
  • 传递 props 或参数时,只传必需的内容(如 React 组件不接收无用 props)

6. 依赖倒置原则是什么?如何在 JavaScript 中应用?

DIP 要求高层模块不依赖低层模块,两者都依赖抽象。

在 JavaScript 中,可通过"依赖注入"实现:将低层模块(如数据库、邮件服务)作为参数传入高层模块,而非硬编码。例如,UserService 接收 Database 实例,而非直接创建 MySQLDatabase。

7. 能否举一个 JavaScript 中应用 SOLID 原则的实际例子?

以 Express.js 应用为例:

  • SRP:路由处理、参数验证、业务逻辑拆分到不同模块
  • OCP:新增接口时,通过添加中间件扩展功能,不修改核心逻辑
  • LSP:不同认证策略(如 JWT、OAuth)的子类,可替换使用
  • ISP:服务接口聚焦(如 EmailService 只处理邮件,不包含存储逻辑)
  • DIP:控制器通过依赖注入接收数据库服务,而非直接导入

面试技巧:深入理解 SOLID 原则,需能做到三点------解释原则定义、识别代码中的违反情况、演示重构优化方法。面试官通常关注这三方面的能力。

原文链接:https://my.oschina.net/powertoolsteam/blog/18689539
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章