TypeScript设计模式:单例模式

单例模式(Singleton Pattern)是一种创建型设计模式,用于确保一个类只有一个实例,并提供全局访问点以访问该实例。这在需要控制资源、避免重复实例化或确保状态一致的场景中非常有用。

什么是单例模式?

单例模式的核心是通过限制类的实例化来保证全局唯一性,使用私有构造函数和静态方法(如 getInstance())提供访问入口。其关键特性包括:

  • 唯一实例:整个应用中只有一个实例,避免资源浪费或状态冲突。
  • 全局访问:通过静态方法提供统一的访问点,简化调用。
  • 懒加载:实例在首次需要时创建,优化性能。

在 TypeScript 中,单例模式通过私有构造函数和模块系统实现,结合类型系统确保安全性和一致性。

单例模式的结构

单例模式通常包含以下元素:

  1. 单例类(Singleton) :包含私有构造函数、静态实例变量和 getInstance() 方法。
  2. 客户端 :通过 getInstance() 获取单例实例并使用其功能。

优点

  • 资源控制:限制实例数量,适合管理共享资源(如文件句柄或网络连接)。
  • 全局一致性:提供单一访问点,确保状态一致。
  • 懒加载:延迟实例化,减少启动开销。
  • 类型安全 :TypeScript 的访问修饰符(如 private)防止外部直接实例化。

适用场景

  • 需要全局唯一实例的场景,如桌面应用单例启动、日志记录器或配置管理。
  • 管理共享资源,如数据库连接池或缓存。
  • 确保全局状态一致,如应用窗口管理或设置存储。

简单的模拟代码介绍

以下是一个简单的 TypeScript 示例,模拟一个全局配置管理器单例,确保应用中只有一个配置实例,用于存储和访问全局设置。

typescript 复制代码
// 单例类:配置管理器
class ConfigManager {
  private static instance: ConfigManager | null = null;
  private settings: Map<string, string> = new Map();

  // 私有构造函数
  private constructor() {
    console.log("配置管理器实例已创建");
    // 初始化默认配置
    this.settings.set("主题", "暗色");
    this.settings.set("语言", "中文");
  }

  // 静态方法:获取单例实例
  public static getInstance(): ConfigManager {
    if (!ConfigManager.instance) {
      ConfigManager.instance = new ConfigManager();
    }
    return ConfigManager.instance;
  }

  // 公共方法:获取配置
  public getSetting(key: string): string | undefined {
    return this.settings.get(key);
  }

  // 公共方法:设置配置
  public setSetting(key: string, value: string): void {
    this.settings.set(key, value);
    console.log(`将 ${key} 设置为 ${value}`);
  }
}

// 客户端代码
function main() {
  // 第一次获取实例
  const config1 = ConfigManager.getInstance();
  console.log("主题:", config1.getSetting("主题"));

  // 修改配置
  config1.setSetting("主题", "亮色");

  // 第二次获取,应返回相同实例
  const config2 = ConfigManager.getInstance();
  console.log("第二次获取的主题:", config2.getSetting("主题"));
  console.log("是否为同一实例?", config1 === config2); // true
}

main();

运行结果

运行以上代码,将输出:

makefile 复制代码
配置管理器实例已创建
主题: 暗色
将 主题 设置为 亮色
第二次获取的主题: 亮色
是否为同一实例? true

这个简单示例展示了单例模式的核心:一个唯一的 ConfigManager 实例维护全局配置,确保应用内一致性。

现实世界示例:桌面程序单例启动

在桌面应用程序中,一个常见需求是确保同一时间只有一个应用实例运行。例如,像 VS Code 或 Notepad++ 这样的应用会防止多个实例运行,以避免资源冲突(如文件锁)或提供一致的用户体验(如聚焦已有窗口)。单例模式非常适合这一场景,因为它可以确保一个应用管理器实例,并协调启动行为。

以下是一个使用 TypeScript 和 Node.js 的示例,模拟桌面应用的单实例启动机制。我们通过一个锁文件来检测是否已有实例运行(这是桌面应用中常用的技术)。在真实的桌面应用(如基于 Electron 的应用)中,通常会使用操作系统特定的机制(如命名管道或套接字),但本例使用文件锁来简化展示。

实现示例

我们将创建一个 SingleInstanceManager 单例类,检查锁文件以判断是否有已有实例运行,并决定启动新实例或通知已有实例。

typescript 复制代码
import * as fs from 'fs/promises';
import * as path from 'path';

// 单例类:单实例管理器
class SingleInstanceManager {
  private static instance: SingleInstanceManager | null = null;
  private lockFilePath: string = path.join(__dirname, 'app.lock');
  private isRunning: boolean = false;

  // 私有构造函数
  private constructor() {
    console.log("单实例管理器实例已创建");
  }

  // 静态方法:获取单例实例
  public static getInstance(): SingleInstanceManager {
    if (!SingleInstanceManager.instance) {
      SingleInstanceManager.instance = new SingleInstanceManager();
    }
    return SingleInstanceManager.instance;
  }

  // 检查是否已有实例运行
  public async checkInstance(): Promise<boolean> {
    try {
      // 尝试访问锁文件
      await fs.access(this.lockFilePath);
      console.log("已有另一个实例正在运行");
      return true;
    } catch {
      // 锁文件不存在,创建锁文件表示当前实例
      await fs.writeFile(this.lockFilePath, process.pid.toString());
      this.isRunning = true;
      console.log("无现有实例,启动新实例");
      return false;
    }
  }

  // 模拟启动应用
  public async startApp(): Promise<void> {
    if (await this.checkInstance()) {
      console.log("无法启动:已有实例正在运行");
      // 模拟通知已有实例(如聚焦窗口)
      this.notifyExistingInstance();
      return;
    }

    console.log("应用启动成功");
    // 模拟应用运行逻辑
    await this.runMainLoop();
  }

  // 模拟通知已有实例(如聚焦窗口)
  private notifyExistingInstance(): void {
    console.log("通知已有实例以聚焦窗口");
    // 在真实桌面应用中,可能通过 IPC 或系统 API 通知
  }

  // 模拟应用主循环
  private async runMainLoop(): Promise<void> {
    console.log("运行应用主循环");
    // 模拟运行一段时间
    await new Promise(resolve => setTimeout(resolve, 2000));
    console.log("应用主循环结束");
  }

  // 清理锁文件(模拟应用退出)
  public async cleanup(): Promise<void> {
    if (this.isRunning) {
      await fs.unlink(this.lockFilePath).catch(() => {});
      console.log("锁文件已删除,应用已关闭");
      this.isRunning = false;
    }
  }
}

// 客户端代码
async function main() {
  const manager1 = SingleInstanceManager.getInstance();

  // 第一次启动
  await manager1.startApp();

  // 模拟另一个实例尝试启动
  const manager2 = SingleInstanceManager.getInstance();
  await manager2.startApp();

  // 验证同一实例
  console.log("是否为同一实例?", manager1 === manager2);

  // 清理(模拟应用退出)
  await manager1.cleanup();
}

main().catch(console.error);

运行与测试

  1. 准备 :确保 Node.js 环境已安装,保存代码为 singleton.ts

  2. 运行 :执行 npx ts-node singleton.ts

  3. 输出:模拟单实例启动,第二次尝试会检测到已有实例。

  4. 控制台输出示例

    arduino 复制代码
    单实例管理器实例已创建
    无现有实例,启动新实例
    应用启动成功
    运行应用主循环
    应用主循环结束
    已有另一个实例正在运行
    通知已有实例以聚焦窗口
    无法启动:已有实例正在运行
    是否为同一实例? true
    锁文件已删除,应用已关闭

代码说明

  • 单例实现SingleInstanceManager 使用私有构造函数和 getInstance() 确保唯一实例。
  • 锁文件机制 :通过 app.lock 文件检测运行实例,模拟桌面应用的单实例检查(真实场景可能使用系统锁或 IPC)。
  • 核心逻辑checkInstance() 检查锁文件,startApp() 决定启动或通知已有实例,cleanup() 释放资源。
  • 现实场景:在桌面应用(如基于 Electron 的 VS Code 或 Notepad++)中,单例模式确保单实例运行,防止资源冲突并统一用户体验(如聚焦已有窗口)。

总结

单例模式通过保证唯一实例和全局访问,解决了资源控制和状态一致性问题。从简单的配置管理器到桌面应用单例启动示例,展示了单例模式在现实世界中的强大应用。结合 TypeScript 的类型安全和锁文件机制,单例模式特别适合需要限制实例的场景,如桌面应用启动管理、日志记录或全局配置。未来可扩展到更复杂的场景,如多进程通信或跨平台单实例管理。

相关推荐
小公主2 小时前
我的第一个 React Flow 小实验
前端
RoyLin2 小时前
TypeScript设计模式:工厂方法模式
前端·后端·node.js
知其然亦知其所以然2 小时前
MySQL 社招必考题:如何优化查询过程中的数据访问?
后端·mysql·面试
用户4099322502122 小时前
FastAPI秒杀库存总变负数?Redis分布式锁能帮你守住底线吗
后端·ai编程·trae
平平无奇的开发仔2 小时前
# Springboot 中BeanDefinition是在什么阶段被创建成Bean的
后端
掘金酱2 小时前
🎉 2025年8月金石计划开奖公示
前端·人工智能·后端
SimonKing2 小时前
接口调用总失败?试试Spring官方重试框架Spring-Retry
java·后端·程序员
Cache技术分享2 小时前
191. Java 异常 - 捕获与处理异常
前端·后端
努力的小郑2 小时前
从一次分表实践谈起:我们真的需要复杂的分布式ID吗?
分布式·后端·面试