探索门面、适配器、单例、原型、构造器、代理和工厂这 7 种现代软件设计模式。
设计模式被用来解决日常软件开发中所遇到的设计问题。
这些问题可能包括:
维持数据库连接
创建和管理对象
向订阅了特定实体的一组用户发送通知
针对这些问题,如果你试图独自思考并设计出最优的解决方案,可能需要花费大量的精力。
但,其实你完全不必这样做!
设计模式正是为了解决这些反复出现的问题而产生的。因此,你所要做的就是根据你的框架和语言实现特定的模式就可以了!
那么,让我们来看一下在 Node.js 中,你可能需要实现的最常见的设计模式。
顺便说一下,如果您想跳过文章直接阅读代码,请查看我的 Bit Scope。
首先,重要的是要理解门面模式(Facade Pattern),因为它在 Node.js 应用中非常重要。
简单来说,门面模式就是通过提供统一的接口来简化复杂子系统的设计。
作为单一入口,它隐藏了所有的内部实现细节,简化了调用者与底层功能的交互。它就像一个网关,将客户端与复杂的细节隔离开来。
例如,使用 Google 账户登录网站的过程就可以视为门面模式的一个现实的例子。你只需要点击“使用 Google 登录” 按钮,而这个按钮就是一个统一的登录选项。
你无需再为输入邮箱、密码等其他个人信息而烦恼。
优势:
简化接口: 减少开发人员的认知负荷,使其与复杂系统的交互变得简单。
降低耦合性: 将客户端代码与内部实现细节解耦,提高代码的可维护性和灵活性。
提高可读性: 将复杂的逻辑封装在门面中,使代码更有条理且更易于理解。
受控访问: 可在访问底层功能之前通过设定特定的规则或校验实现访问控制。
思考如下代码:
// 复杂模块
class ComplexModule {
initialize() {
// 复杂的初始化逻辑...
}
operation1() {
// 复杂操作 1
}
operation2() {
// 复杂操作 2
}
}
// 客户端代码
const complexModule = new ComplexModule();
complexModule.initialize();
complexModule.operation1();
complexModule.operation2();
上述代码片段展示了客户端如何在模块外部与其子系统进行交互。你不仅需要手动执行所有操作,并且在维护代码时很可能会遇到问题。
再来看看下面这段代码:
// 在复杂模块中使用门面模式
class ModuleFacade {
constructor() {
this.complexModule = new ComplexModule();
}
performOperations() {
this.complexModule.initialize();
this.complexModule.operation1();
this.complexModule.operation2();
}
}
// 客户端代码
const moduleFacade = new ModuleFacade();
moduleFacade.performOperations();
现在,如你所见,我们不需要再在模块外部对子模块进行初始化,而是将其封装在 performOperations 函数中,它会负责处理所有与复杂内部子系统的通信。
这样,你就能以一种简洁的方法来处理复杂的通信树。
点击这里查看完整代码实现。
接下来,是你在退休之前可能每天都要使用的模式之一。有时候,你需要确保某些东西只能有一个实例。
例如,考虑一下数据库连接。在特定时间内,应用程序是否需要一个以上的数据库连接?能否重用现有连接?
这就是单例模式的作用所在。它确保你的类只有一个全局实例,且可以通过静态方法进行访问。
优势:
全局访问: 是一种在应用程序中任何位置访问共享数据和功能的便捷方式。
资源管理: 通过单一实例来确保诸如数据库连接、日志记录器或文件句柄等资源的高效使用。
一致性: 使更改仅影响单个实例,确保行为执行的一致性。
受控状态: 通过集中管理数据操作来简化状态管理。
下面是在 Node.js 中实现单例的一个示例:
class ConfigManager {
constructor() {
this.databaseConfig = { /* 数据库配置 */ };
this.apiKey = "your_api_key";
// 其他应用配置
}
static getInstance() {
if (!this.instance) {
this.instance = new ConfigManager();
}
return this.instance;
}
getDatabaseConfig() {
return this.databaseConfig;
}
getApiKey() {
return this.apiKey;
}
// 其他获取配置的方法
}
// 客户端
const configManager = ConfigManager.getInstance();
// 访问配置
const databaseConfig = configManager.getDatabaseConfig();
const apiKey = configManager.getApiKey();
你可能有一个或多个与外部服务交互的 Node.js 应用程序,每个服务都需要特定的配置参数。使用单例模式,你可以通过创建一个 ConfigManager 类来负责集中处理这些配置。
点击这里查看完整代码实现。
接下来,你需要设想一个场景,即你正在使用的 API 和你正在开发的客户端之间存在 API 不兼容的问题。
例如,你可能有一个需要两个 props 的 React 组件:
名字
姓氏
但你的 API 只会返回一个变量
全名
因此,如果你没有调整 API 返回体的权限,就必须利用现有的资源使应用能够正常运行。
这就是适配器模式的作用所在。
适配器模式可以在不兼容的接口之间架起桥梁,从而使它们能够无缝地协同工作。
优势:
互操作性: 使具有不同接口的组件之间能够通信,促进系统集成和重用。
松散耦合: 将客户端代码与适配组件的具体实现解耦,提高灵活性和可维护性。
灵活性: 允许通过创建新的适配器来适应新组件,而无需修改现有代码。
可重用性: 适配器实现可以针对类似的兼容性需求重复使用,减少代码重复。
示例:
下面是适配器设计模式的一个简单的代码示例。
点击这里查看完整代码实现。
老系统
class OldSystem {
request() {
return "Old System Request";
}
}
新系统和适配器
class NewSystem {
newRequest() {
return "New System Request";
}
}
class Adapter {
constructor(newSystem) {
this.newSystem = newSystem;
}
request() {
return this.newSystem.newRequest();
}
}
客户端使用
// 老系统的使用
const oldSystem = new OldSystem();
console.log(oldSystem.request()); // 输出:Old System Request
// 通过适配器使用新系统
const newSystem = new NewSystem();
const adapter = new Adapter(newSystem);
console.log(adapter.request()); // 输出: New System Request
接下来,我们将介绍的模式可以用来构建对象,从而使对象的管理变得更加容易。
构建器模式将一个复杂对象的构建与它的表示分离。
这就像组装一台定制电脑——单独选择组件并构建最终产品。在 Node.js 中,构造器模式有助于构建具有复杂配置的对象,并保证这个过程可以分步进行且可定制。
在这种设计模式中,你可以为对象的每个可选属性创建单独的方法(“构造器”),而不是创建一个带有大量参数的构造函数。这些方法通常会返回类的当前实例(this),将它们串联起来就可以逐步构建出对象。
优势:
提高可读性: 使用有意义的方法名显式设置每个属性,使代码更加清晰。
灵活性: 仅使用必要的属性来构建对象,避免未使用的字段出现意料之外的值。
不可变性:build() 方法通常会创建一个新实例而不是修改构造器,这增强了不可变性,简化了推理过程。
错误处理: 在构造器方法中验证属性值并抛出错误比在复杂的构造函数中更容易。
示例:
下面是构建器设计模式的一个简单的代码示例。
点击这里查看完整代码实现。
class UserBuilder {
constructor(name) {
this.name = name;
this.email = null;
this.address = null;
}
withEmail(email) {
this.email = email;
return this; // 通过返回 this 实现方法的链式调用
}
withAddress(address) {
this.address = address;
return this;
}
build() {
// 验证和构造用户对象
const user = new User({
name: this.name,
email: this.email,
address: this.address,
});
return user;
}
}
// 客户端代码
const user1 = new UserBuilder('John')
.withEmail('john@example.com')
.withAddress('123 Main St.')
.build();
console.log(user1); // 打印完整的用户对象的值
工厂模式为对象创建提供了一个接口,但允许子类改变所创建对象的类型。
把它想象成一个制造工厂,不同的装配线生产不同的产品。在 Node.js 中,工厂模式在创建对象时无需指定其具体类,提高了灵活性和可扩展性。
优势:
解耦: 客户端代码与特定对象的创建逻辑解耦,提高了代码的灵活性和可维护性。
集中控制: 开发者可以轻松地添加新对象类型或修改现有的对象类型,只需在工厂中处理更改,而不会影响客户端代码。
灵活性: 工厂可根据运行时条件或配置选择合适的对象,使代码更具适应性。
封装性: 对象创建的细节被隐藏在工厂内部,提高了代码的可读性和可维护性。
示例:
下面是工厂设计模式的一个简单的代码示例。
点击这里查看完整代码实现。
图形接口
// Shape 接口
class Shape {
draw() {}
}
多种图形
// Shape 接口的具体实现
class Circle extends Shape {
draw() {
console.log("Drawing Circle");
}
}
class Square extends Shape {
draw() {
console.log("Drawing Square");
}
}
class Triangle extends Shape {
draw() {
console.log("Drawing Triangle");
}
}
图形工厂
// ShapeFactory 类负责创 Shape 的实例
class ShapeFactory {
createShape(type) {
switch (type) {
case 'circle':
return new Circle();
case 'square':
return new Square();
case 'triangle':
return new Triangle();
default:
throw new Error('Invalid shape type');
}
}
}
客户端代码
// 客户端代码使用 ShapeFactory 来创建 Shape
const shapeFactory = new ShapeFactory();
const circle = shapeFactory.createShape('circle');
circle.draw(); // 输出:画圆
const square = shapeFactory.createShape('square');
square.draw(); // 输出:画正方形
const triangle = shapeFactory.createShape('triangle');
triangle.draw(); // 输出:画三角形
原型模式通过复制一个已存在的对象(称为原型)来创建新对象。
通过该模式可以创建主对象的副本。当创建对象的成本比复制该对象的成本高时,它就非常有用。
概念:
原型: 定义一个具有所需属性和方法的基准对象。该对象将作为后续对象的蓝图。
克隆: 通过复制原型来创建新对象,通常使用如 Object.create 之类的内置方法或自定义克隆逻辑。
定制: 新创建的对象可以修改各自的属性,但不会影响原始原型。
优势:
性能: 克隆现有对象通常比从头开始构建新对象更快,复杂对象尤为明显。
内存效率: 通过原型共享属性和方法,可以避免冗余存储,减少内存使用。
动态修改: 开发者可以轻松地扩展原型,为当前和未来的所有实例添加新功能。
下面是原型模式的一个简单的代码示例。
点击这里查看完整代码实现。
原型对象
// 原型对象
const animalPrototype = {
type: 'unknown',
makeSound: function () {
console.log('Some generic sound');
},
clone: function () {
return Object.create(this); // 使用 Object.create() 进行克隆
},
};
自定义实现
// 基于原型的自定义实例
const dog = animalPrototype.clone();
dog.type = 'Dog';
dog.makeSound = function () {
console.log('Woof!');
};
const cat = animalPrototype.clone();
cat.type = 'Cat';
cat.makeSound = function () {
console.log('Meow!');
};
客户端代码
// 使用自定义实例的客户端代码
dog.makeSound(); // 输出:Woof!
cat.makeSound(); // 输出:Meow!
代理模式是通过充当另一个对象的代理或占位符,以实现对该对象的访问控制。
该模式就是在客户端和真实对象之间创建一个中介对象(“代理”)。这个代理控制着对真实对象的访问,可以在操作到达目标对象之前或之后实施拦截和修改。
这样,你就可以在不更改真实对象实现的前提下,添加额外的功能。代理模式对于懒加载、访问控制、添加日志以及调试功能等都非常有用。
为了更好地理解代理设计模式(Proxy Design Pattern)及其在 React 环境中的用例,可以看下这篇文章:React 代理设计模式实战。
优势
控制访问: 在与真实对象交互之前强制执行权限或验证。
附加功能: 添加日志记录、缓存或安全等特性,而无需更改对象本身。
抽象: 通过隐藏真实对象的实现细节来简化客户端代码。
灵活性: 在运行时动态更改目标对象或处理器的行为。
示例:
下面是该模式的一个简单示例,点击这里查看完整实现。
在所有这些示例中,我都通过 JavaScript Proxy 对象来为其他对象创建代理。要更深入地了解 JavaScript 内置代理,请访问此处。
const target = {
name: 'Alice',
sayHello() {
console.log(Hello, my name is ${this.name} );
},
};
const handler = {
get(target, prop, receiver) {
console.log(Property ${prop} accessed );
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(Property ${prop} set to ${value} );
return Reflect.set(target, prop, value, receiver);
},
};
const proxy = new Proxy(target, handler);
proxy.name; // 输出:访问属性名
proxy.sayHello(); // 输出:访问属性方法 sayHello
// Hello, my name is Alice
proxy.name = 'Bob'; // 输出:属性名设置为 Bob
对于每个开发者来说,设计模式都是重要的学习内容。无论你是初学者还是专家,理解设计模式及其在现代软件开发中的应用都很重要,因为它能让你更快地构建出更好的软件。
如果你想查看本文涉及的代码,请查看我在 Bit Scope 中的实现。
感谢阅读!
原文地址:
https://blog.bitsrc.io/nodejs-design-patterns-must-know-8ef0a73b3339
声明:本文由 InfoQ 翻译,未经许可禁止转载。
8 月 16-17 日,FCon 全球金融科技大会将在上海举办。本届大会由中国信通院铸基计划作为官方合作机构,来自工银科技、北京银行、平安银行、广发银行、中信银行、度小满、蚂蚁集团等金融机构及金融科技公司的资深专家将现身说法分享其在金融科技应用实践中的经验与深入洞察。