理解 SOLID 原则:编写更简洁的 JavaScript 代码
编写简洁、可维护的代码是构建可扩展应用的关键。由罗伯特·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; }
❌ 问题所在 :
这个函数同时承担了三个职责:
- 验证输入合法性
- 保存数据到数据库
- 发送欢迎邮件
每个职责的修改理由都不同(比如业务规则变更、数据库逻辑调整、邮件服务升级),违背了"单一职责"的核心要求。
✅ 正面示例:遵循 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,需注意:
- 子类不应重写方法以抛出错误或大幅改变行为
- 用"鸭子类型"(Duck Typing)非正式地定义接口,确保行为一致性
- 按"能力"设计,而非按"类型"(如拆分 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,可遵循以下建议:
- 将庞大的接口(或对象)拆分为小型、用途单一的单元
- 不强迫组件、函数或类实现超出需求的功能
- 尽可能用"组合"替代"继承"
应用 ISP 后,代码会更简洁、聚焦,且随着项目增长,可维护性会显著提升。
✅ 5. 依赖倒置原则(Dependency Inversion Principle, DIP)
定义:SOLID 原则的最后一条,核心要求为:
- 高层模块不应依赖低层模块,两者都应依赖抽象;
- 抽象不应依赖细节,细节应依赖抽象。
🧠 通俗解释
核心业务逻辑(高层代码)不应与具体实现细节(如 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' });
🎯 优化点在哪里
- UserService 可适配任何遵循 Database 抽象的实现(MySQL、MongoDB 等)
- 替换数据库时,无需修改核心业务逻辑
- 测试更简单:用 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 原则,需能做到三点------解释原则定义、识别代码中的违反情况、演示重构优化方法。面试官通常关注这三方面的能力。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
WebRTC 入门指南:实时通信完全解析
🚀 简介 WebRTC(Web 实时通信)是一项强大的技术,支持浏览器和移动应用实时交换音视频与数据——无需中间服务器中转。它是现代视频通话、屏幕共享工具及实时协作平台的核心底层技术。 本文将完整覆盖 WebRTC 技术流程:从获取用户媒体到建立安全的点对点(P2P)连接,并提供基于 TypeScript 风格的 JavaScript 实战示例。 🎥 捕获媒体流 什么是媒体流(Media Stream)? 流(Stream)是连续的数据传输流——在 WebRTC 中,特指实时传输的音频或视频数据。 使用 getUserMedia 捕获音视频 通过 navigator.mediaDevices.getUserMedia() 方法可请求访问用户的麦克风和摄像头,示例如下: const constraints = { audio: true, video: true }; // 配置:同时捕获音频和视频 navigator.mediaDevices .getUserMedia(constraints) .then((mediaStream) => { console.log('成功...
- 下一篇
2025 HarmonyOS创新赛|对话刘子安:向外走,做鸿蒙开发的新生力量
8 月 23 日,开源中国主办的“鸿蒙一夏”开发者系列沙龙-创新赛专场在郑州圆满落幕,吸引了来自全国各地近 200 位鸿蒙开发者的莅临参与,热度更是席卷云端,线上直播吸引 10000+ 人次观看,盛况空前。 本次沙龙重点宣讲了 2025 HarmonyOS 创新赛 的相关内容。作为鸿蒙生态最大规模开发者的官方赛事,今年的 HarmonyOS 创新赛基于 HarmonyOS 6 开发者 Beta 版本能力,聚焦六大赛题方向: 全场景一体化:挑战“ 1+8+N ”,将手表、平板、PC 等多设备联动,打造突破性的应用解决方案; 软硬件协同:聚焦人与设备之间的交互体验升级,结合软硬件(如手写笔、手势识别、语音交互),在各种社交场景中探索增强互动的新玩法; 智能化创新:运用 AI 大模型能力进行应用智能化创新探索,展现应用场景与 AI 大模型的深度融合; 3D 空间化融合:将 3D 视觉技术融入创作之中,创造更加沉浸式的交互感受; 全新交互形态:探索元服务、Agent 等全新交互形态,为用户带来前所未有的体验; 社会影响:关注无障碍领域的特殊人群需求,让目标用户群体享受科技发展带来的便利。 本次...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS关闭SELinux安全模块
- 2048小游戏-低调大师作品
- CentOS7设置SWAP分区,小内存服务器的救世主
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Linux系统CentOS6、CentOS7手动修改IP地址
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- SpringBoot2配置默认Tomcat设置,开启更多高级功能