23 种设计模式全解析
单例与工厂模式
单例模式:确保全局唯一
在软件开发中,我们常常需要某个类只有一个实例。比如,全局配置对象、日志记录器或数据库连接池。如果每次使用时都创建一个新实例,不仅会浪费资源,还可能导致状态不一致。单例模式就是为了解决这个问题而生的:它保证一个类只有一个实例,并提供一个全局访问点来获取该实例。
在 TypeScript 中实现单例模式的关键是利用 private 构造函数和 static 方法。通过将构造函数设为私有,我们阻止了外部代码通过 new 关键字直接实例化该类。然后,我们提供一个公共的静态方法(通常命名为 getInstance),它负责创建和返回唯一的实例。
class GlobalConfig {
private static instance: GlobalConfig;
public readonly theme: string = 'dark';
// 构造函数是私有的,外部无法直接 new
private constructor() {
console.log('GlobalConfig instance created.');
}
// 公共的静态方法,用于获取唯一实例
public static getInstance(): GlobalConfig {
if (!GlobalConfig.instance) {
GlobalConfig.instance = new GlobalConfig();
}
return GlobalConfig.instance;
}
public getConfigDetails(): string {
return `Current theme is ${this.theme}`;
}
}
// 如何使用:
const config1 = GlobalConfig.getInstance();
const config2 = GlobalConfig.getInstance();
console.log(config1 === config2); // 输出: true
console.log(config1.getConfigDetails()); // 输出: Current theme is dark
上面的代码中,GlobalConfig 的构造函数是 private 的,所以你不能写 new GlobalConfig()。所有对该类的访问都必须通过 GlobalConfig.getInstance() 方法。这个方法会检查静态属性 instance 是否已经存在。如果不存在,它就创建一个新实例并保存起来;如果存在,它就直接返回已有的实例。这样,无论你调用多少次 getInstance(),得到的都是同一个对象。
单例模式的核心思想是:控制实例化过程,确保全局唯一性。
工厂模式:解耦对象创建
现在,我们来看看另一种控制对象创建的设计模式:工厂模式。与单例模式关注“唯一性”不同,工厂模式的核心目标是解耦——将对象的创建过程与使用过程分离开来。
解耦
verb
在软件工程中,指减少模块之间的相互依赖,使得一个模块的变化对其他模块的影响降到最低。高内聚、低耦合是良好软件设计的标志。
想象一下,你的应用需要根据用户的会员等级显示不同的 UI 组件,比如普通用户的按钮、VIP 用户的按钮和管理员的按钮。如果直接在业务代码里用 if/else 或 switch 来判断并 new 不同的按钮类,代码会变得非常臃肿且难以维护。每次新增一种用户类型,你都得修改所有创建按钮的地方。
工厂方法模式通过定义一个用于创建对象的接口,但让子类决定实例化哪一个类,从而解决了这个问题。工厂方法使一个类的实例化延迟到其子类。
让我们用 TypeScript 来实现这个按钮工厂的例子。首先定义产品接口和具体产品:
// 1. 产品接口
interface Button {
render(): void;
onClick(event: Event): void;
}
// 2. 具体产品
class RegularButton implements Button {
render() { console.log('Rendering a regular button.'); }
onClick(event: Event) { console.log('Regular button clicked.'); }
}
class VIPButton implements Button {
render() { console.log('Rendering a special VIP button!'); }
onClick(event: Event) { console.log('VIP button clicked.'); }
}
接下来,我们定义创建者(工厂)的抽象类和具体实现。这里,我们可以结合泛型来让工厂更加灵活。
// 3. 创建者(工厂)抽象类
abstract class ButtonFactory {
// 这是工厂方法
public abstract createButton(): Button;
public renderButton(): void {
const button = this.createButton();
button.render();
}
}
// 4. 具体创建者
class RegularUserDialog extends ButtonFactory {
public createButton(): Button {
return new RegularButton();
}
}
class VIPUserDialog extends ButtonFactory {
public createButton(): Button {
return new VIPButton();
}
}
// 使用
function initializeUI(factory: ButtonFactory) {
console.log('Initializing UI...');
factory.renderButton();
}
// 客户端代码根据需要选择具体的工厂
initializeUI(new RegularUserDialog()); // 输出: Initializing UI... Rendering a regular button.
initializeUI(new VIPUserDialog()); // 输出: Initializing UI... Rendering a special VIP button!
看到好处了吗?initializeUI 函数完全不知道它具体用的是哪种按钮。它只依赖于 ButtonFactory 抽象类。我们把创建对象的复杂逻辑封装在了具体的工厂子类(RegularUserDialog, VIPUserDialog)中。如果未来要增加“管理员按钮”,我们只需要创建一个新的 AdminButton 类和一个新的 AdminUserDialog 工厂,而完全不需要改动 initializeUI 函数。这就是的体现。
Next.js 中的陷阱
在 Next.js 这样的同构(Isomorphic)框架中使用单例模式时,需要特别小心。Next.js 的代码可以在服务器端(SSR/SSG)和客户端(浏览器)两种环境中运行。如果你不加注意,可能会在两个环境中各自创建一个单例实例,从而破坏了“全局唯一”的初衷。
一个常见的错误是在模块的顶层直接创建单例实例:
export const mySingleton = MySingleton.getInstance();
在 Node.js 环境中,模块在首次 require 时被缓存,所以这种方式在服务器端通常是安全的。但在客户端,每次页面刷新或在不同的 Server Component/Client Component 之间切换时,行为可能变得不可预测。特别是当代码被打包进不同的 JavaScript chunk 时,可能会导致多个实例被创建。
确保 Next.js 中单例唯一性的关键在于:理解代码的运行环境,并控制实例的生命周期。
一个更稳妥的策略是,不要在模块顶层实例化。而是将单例实例与一个稳定的生命周期绑定,比如 React 的 Context 或者一个全局状态管理库(如 Redux、Zustand)。
对于纯粹在服务器端使用的单例(例如数据库连接),可以利用 Node.js 的模块缓存机制,但最好是用一个函数来惰性初始化和导出它,而不是直接导出一个实例。对于需要在客户端和服务器之间共享状态的情况,通常不推荐使用传统的单例模式,而应该依赖 Next.js 提供的状态管理或数据获取机制,如 React.cache 或 useSWR。
| 特性 | 单例模式 | 工厂模式 |
|---|---|---|
| 核心目标 | 保证全局只有一个实例 | 解耦对象的创建与使用 |
| 关注点 | 实例的数量(唯一) | 实例化的过程(封装) |
| 实现方式 | 私有构造函数 + 静态方法 | 抽象工厂 + 具体工厂子类 |
| 适用场景 | 全局配置、日志、数据库连接池 | 创建复杂对象、根据条件创建不同对象 |
| 灵活性 | 较低,类与其实例紧密绑定 | 较高,易于扩展新产品和工厂 |
理解这些设计模式的本质和适用场景,能帮助你在复杂的 TypeScript 和 Next.js 项目中写出更清晰、更可维护的代码。它们不是银弹,但却是你工具箱里非常有用的工具。
TypeScript 提供了强大的工具来构建可扩展和健壮的应用程序。
准备好测试一下你对这些模式的理解了吗?
在TypeScript中实现单例模式时,通常使用哪两个关键字来确保一个类只有一个实例?
根据提供的文本,工厂模式的核心目标是什么?
设计模式是前人智慧的结晶,合理运用它们,能让你的代码质量提升一个台阶。