No history yet

单例与工厂模式

单例模式:确保全局唯一

在软件开发中,我们常常需要某个类只有一个实例。比如,全局配置对象、日志记录器或数据库连接池。如果每次使用时都创建一个新实例,不仅会浪费资源,还可能导致状态不一致。单例模式就是为了解决这个问题而生的:它保证一个类只有一个实例,并提供一个全局访问点来获取该实例。

在 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/elseswitch 来判断并 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.cacheuseSWR

特性单例模式工厂模式
核心目标保证全局只有一个实例解耦对象的创建与使用
关注点实例的数量(唯一)实例化的过程(封装)
实现方式私有构造函数 + 静态方法抽象工厂 + 具体工厂子类
适用场景全局配置、日志、数据库连接池创建复杂对象、根据条件创建不同对象
灵活性较低,类与其实例紧密绑定较高,易于扩展新产品和工厂

理解这些设计模式的本质和适用场景,能帮助你在复杂的 TypeScript 和 Next.js 项目中写出更清晰、更可维护的代码。它们不是银弹,但却是你工具箱里非常有用的工具。

TypeScript 提供了强大的工具来构建可扩展和健壮的应用程序。

准备好测试一下你对这些模式的理解了吗?

Quiz Questions 1/5

在TypeScript中实现单例模式时,通常使用哪两个关键字来确保一个类只有一个实例?

Quiz Questions 2/5

根据提供的文本,工厂模式的核心目标是什么?

设计模式是前人智慧的结晶,合理运用它们,能让你的代码质量提升一个台阶。