编写简洁、可维护的代码是构建可扩展应用的关键。由罗伯特·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 原则,需能做到三点——解释原则定义、识别代码中的违反情况、演示重构优化方法。面试官通常关注这三方面的能力。

扩展链接

SpreadJS如何支持JavaScript框架

理解 SOLID 原则:编写更简洁的 JavaScript 代码的更多相关文章

  1. 使用 Promises 编写更优雅的 JavaScript 代码

    你可能已经无意中听说过 Promises,很多人都在讨论它,使用它,但你不知道为什么它们如此特别.难道你不能使用回调么?有什么了特别的?在本文中,我们一起来看看 Promises 是什么以及如何使用它 ...

  2. [label][翻译][JavaScript-Translation]七个步骤让你写出更好的JavaScript代码

    7 steps to better JavaScript 原文链接: http://www.creativebloq.com/netmag/7-steps-better-javascript-5141 ...

  3. 编写可测试的JavaScript代码

    <编写可测试的JavaScript代码>基本信息作者: [美] Mark Ethan Trostler 托斯勒 著 译者: 徐涛出版社:人民邮电出版社ISBN:9787115373373上 ...

  4. 编写可维护的JavaScript代码(部分)

    平时使用的时VS来进行代码的书写,VS会自动的将代码格式化,所有写了这么久的JS代码,也没有注意到这些点.看了<编写可维护的javascript代码>之后,做了些笔记. var resul ...

  5. 新书《编写可测试的JavaScript代码 》出版,感谢支持

    本书介绍 JavaScript专业开发人员必须具备的一个技能是能够编写可测试的代码.不管是创建新应用程序,还是重写遗留代码,本书都将向你展示如何为客户端和服务器编写和维护可测试的JavaScript代 ...

  6. 编写更好的jQuery代码

    这是一篇关于jQuery的文章,写到这里给初学者一些建议. 现在已经有很多文章讨论jQuery和JavaScript的性能问题,然而,在这篇文章中我计划总结一些提升速度的技巧和一些我自己的建议来改善你 ...

  7. 编写更好的jQuery代码的建议

    讨论jQuery和javascript性能的文章并不罕见.然而,本文我计划总结一些速度方面的技巧和我本人的一些建议,来提升你的jQuery和javascript代码.好的代码会带来速度的提升.快速渲染 ...

  8. 编写更好的jQuery代码的建议(share)

    留个备份! 原文链接: Mathew Carella   翻译: 伯乐在线- yanhaijing译文链接: http://blog.jobbole.com/52770/ 讨论jQuery和javas ...

  9. 编写更好的jQuery代码(转)

    这是一篇关于jQuery的文章,写到这里给初学者一些建议. 原文地址:http://flippinawesome.org/2013/11/25/writing-better-jquery-code/ ...

  10. 编写可维护的javascript代码---开篇(介绍自动报错的插件)

    文章开篇主要推荐了2款检测编程风格的工具: JSLint和JSHint: jsLint是由Douglas Crockford创建的.这是一个通用的javascript代码质量检测工具,最开始JSLin ...

随机推荐

  1. mysql安全小结

    sql的注入是一个很困扰人的问题,一些恶意攻击者可以利用sql注入来获取甚至是修改数据库中的信息,尤其是一些比较敏感的密码一类的数据. sql注入主要利用mysql 的注释将后续应正常执行的语句注释掉 ...

  2. 「Note」图论方向 - 网络流

    1. 网络流 1.1. 定义 1.1.1. 网络 网络是指一个有向图 \(G=(V,E)\),每条边 \((u,v)\in E\) 有一个权值,\(c(u,v)\) 称为容量,当 \((u,v)\no ...

  3. Kubernetes中Service学习笔记

    我们知道 Pod 的生命周期是有限的.可以用 ReplicaSet 和Deployment 来动态的创建和销毁 Pod,每个 Pod 都有自己的 IP 地址,但是如果 Pod 重建了的话那么他的 IP ...

  4. bigdecimal去除末尾多余的0 ,stripTrailingZeros()科学计数法解决

    BigDecimal是处理高精度的浮点数运算的常用的一个类 当需要将BigDecimal中保存的浮点数值打印出来,特别是在页面上显示的时候,就有可能遇到预想之外的科学技术法表示的问题. 一般直接使用 ...

  5. 历时半年,我将一个大型asp.net的零代码快速开发平台转成了java

    老的博客园朋友应该清楚,我在10年前开发了一个基于asp.net的大型开发平台,其中作为开源项目的SilverLight流程设计器也获得了当年的微软开发大奖.时过境迁,当年的设计器早就在技术的更新换代 ...

  6. 文件操作&深浅拷贝&异常处理

    文件操作 [1]基本流程 (1)文件操作 操作 打开读文件内容 r with open('01.txt', 'r', encoding='utf-8') as f: data = f.read() p ...

  7. Collections工具类详解

     Java提供了一个操作Set List Map 的工具类Collections . 里面有大量方法对集合元素进行排序,查询修改等操作. 还能把集合设为不可变. 对集合对象实现线程同步控制.同步控制 ...

  8. 将图片地址转为二进制(博客自定义随机背景图API)

    背景 最近写博客. 觉得自己的博客毫无生机,想加一些图片. 于是在找了一些三方的随机图片链接,发现一些问题: 给的些链接不会直接返回图片, 要么是302重定向 要么是返回json 这导致,这个链接无法 ...

  9. Codeforces Round #710 (Div. 3) ABCDE 题解

    A. Strange Table 签到题,算出对应行列即可. view code #include<iostream> #include<string> #include< ...

  10. WD 笔试 反思 记录

    1.从  1 2 3  4 5 6 7 8 9从中选择至少一个,乘积的种类有多少种 转:解题思路http://www.nowcoder.com/questionTerminal/65c51812549 ...