深入23种设计模式实战
创建型模式深度实践
高级创建型模式实战
在 TypeScript 项目中,创建对象似乎很简单:一个 new 关键字就够了。但随着应用程序(尤其是在 Next.js 这样的框架中)变得复杂,对象的创建过程本身也需要被精心设计。创建型模式的核心思想是:将对象的创建与使用分离,从而让系统更加灵活、可配置和可维护。本章将跳过基础,直接深入探讨如何在复杂的场景下运用这些模式。
抽象工厂:驾驭跨平台UI
想象一下,你正在用 Next.js 构建一个应用,它需要同时在 Web 和原生移动端(例如,通过 React Native)运行。按钮、输入框等基础组件在不同平台上有完全不同的实现。直接在业务组件里用条件语句判断平台(if (isWeb) { <button /> } else { <Button /> })会导致代码混乱且难以维护。
抽象工厂模式为此提供了优雅的解决方案。它定义了一个用于创建一系列相关或相互依赖对象的接口,而无需指定它们的具体类。我们可以定义一个 UIFactory 接口,然后为每个平台提供一个具体的工厂实现。
// 1. 定义抽象产品接口
interface Button {
render(): JSX.Element;
onClick(): void;
}
interface TextInput {
render(): JSX.Element;
}
// 2. 定义抽象工厂接口
interface UIFactory {
createButton(): Button;
createTextInput(): TextInput;
}
// 3. 为Web平台创建具体工厂和产品
class WebButton implements Button { /*...*/ }
class WebTextInput implements TextInput { /*...*/ }
class WebFactory implements UIFactory {
createButton(): Button { return new WebButton(); }
createTextInput(): TextInput { return new WebTextInput(); }
}
// 4. 为原生平台创建具体工厂和产品
class NativeButton implements Button { /*...*/ }
class NativeTextInput implements TextInput { /*...*/ }
class NativeFactory implements UIFactory {
createButton(): Button { return new NativeButton(); }
createTextInput(): TextInput { return new NativeTextInput(); }
}
在应用中,我们可以根据当前环境(例如,通过一个环境变量或平台检测函数)来决定实例化哪个工厂。之后,整个应用的UI创建都通过这个工厂进行,业务逻辑与具体平台的实现完全解耦。这种模式由著名的 (“四人帮”)在其著作《设计模式》中提出,是面向对象设计的基础之一。
抽象工厂的核心价值在于:提供一个统一的接口来创建“一族”产品,使得客户端代码从具体的产品实现中解耦。
单例模式:SSR环境的陷阱
单例模式确保一个类只有一个实例,并提供一个全局访问点。这对于管理全局状态、日志记录器或数据库连接池非常有用。在传统的客户端应用中,实现一个单例相对直接。但在 Next.js 这样的同构(Isomorphic)应用中,情况变得复杂起来。
Next.js 的代码可以在两个完全不同的环境中执行:服务端(用于 或SSR)和客户端(在用户的浏览器中)。如果在模块的顶层创建一个单例实例,那么在服务端,每个服务器请求都会创建一个新的实例。这意味着用户A的请求和用户B的请求会得到不同的单例对象,这完全违背了单例的初衷,并可能导致用户间数据泄露。
正确的做法是确保单例实例只在客户端被创建和共享。我们可以利用闭包和对 window 对象的检查来实现一个安全的、仅限客户端的单例。
// 客户端状态管理器
class ClientStateManager {
private static instance: ClientStateManager | null = null;
private state: Record<string, any> = {};
private constructor() {
// 私有构造函数,防止外部直接 new
}
public static getInstance(): ClientStateManager {
// 关键:只在浏览器环境中创建和缓存实例
if (typeof window !== 'undefined' && !ClientStateManager.instance) {
ClientStateManager.instance = new ClientStateManager();
}
// 在服务器端,或实例已创建,返回实例或null
return ClientStateManager.instance as ClientStateManager;
}
public setState(key: string, value: any): void {
this.state[key] = value;
}
public getState(key: string): any {
return this.state[key];
}
}
// 使用示例(在组件中)
const manager = ClientStateManager.getInstance();
if (manager) { // 必须检查,因为在SSR期间它会是null
manager.setState('user', { name: 'Alice' });
}
建造者模式:告别混乱的配置
当一个对象的构造过程非常复杂,需要多个参数或配置步骤时,直接使用构造函数或工厂模式会变得很笨拙。你可能会遇到一个拥有十几个参数的构造函数,其中大部分还是可选的,这被称为“伸缩构造函数”反模式。
建造者模式通过将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。它通过链式调用(chaining)的方式,一步步地设置对象的属性,最后通过一个 build() 方法生成最终的实例。这使得代码的可读性大大提高。
假设我们正在构建一个复杂的 API 请求配置对象,它可能包含 URL、方法、头部、查询参数、请求体等多个部分,其中许多是可选的。
class APIRequestBuilder {
private url: string = '';
private method: 'GET' | 'POST' | 'PUT' = 'GET';
private headers: Record<string, string> = {};
private queryParams: Record<string, string> = {};
private body: any = null;
constructor(baseUrl: string) {
this.url = baseUrl;
}
public setMethod(method: 'GET' | 'POST' | 'PUT'): this {
this.method = method;
return this; // 返回this以支持链式调用
}
public addHeader(key: string, value: string): this {
this.headers[key] = value;
return this;
}
public addQueryParam(key: string, value: string): this {
this.queryParams[key] = value;
return this;
}
public setBody(data: any): this {
this.body = data;
return this;
}
public build(): RequestConfig {
// RequestConfig 是最终的配置对象类型
// 这里可以添加最终的URL构建逻辑等
return {
url: this.url,
method: this.method,
headers: this.headers,
queryParams: this.queryParams,
body: this.body
};
}
}
// 使用起来非常清晰
const requestConfig = new APIRequestBuilder('/api/users')
.setMethod('POST')
.addHeader('Content-Type', 'application/json')
.addHeader('Authorization', 'Bearer token')
.setBody({ name: 'Bob', age: 30 })
.build();
在结束之前,我们快速回顾一下今天讨论的模式。
在一个需要同时支持 Web 和原生移动端的 Next.js 应用中,抽象工厂模式主要解决了什么问题?
在 Next.js 的同构(Isomorphic)环境中,一个简单的、在模块顶层初始化的单例实例可能会导致什么问题?
创建型设计模式提供了一套强大的工具集,用于在复杂系统中优雅地处理对象的实例化。理解并恰当地运用它们,是提升代码质量与架构水平的关键一步。