单例模式(Singleton Pattern)是一种创建型设计模式,用于确保一个类只有一个实例,并提供全局访问点以访问该实例。这在需要控制资源、避免重复实例化或确保状态一致的场景中非常有用。
什么是单例模式?
单例模式的核心是通过限制类的实例化来保证全局唯一性,使用私有构造函数和静态方法(如 getInstance()
)提供访问入口。其关键特性包括:
- 唯一实例:整个应用中只有一个实例,避免资源浪费或状态冲突。
- 全局访问:通过静态方法提供统一的访问点,简化调用。
- 懒加载:实例在首次需要时创建,优化性能。
在 TypeScript 中,单例模式通过私有构造函数和模块系统实现,结合类型系统确保安全性和一致性。
单例模式的结构
单例模式通常包含以下元素:
- 单例类(Singleton) :包含私有构造函数、静态实例变量和
getInstance()
方法。 - 客户端 :通过
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);
运行与测试
-
准备 :确保 Node.js 环境已安装,保存代码为
singleton.ts
。 -
运行 :执行
npx ts-node singleton.ts
。 -
输出:模拟单实例启动,第二次尝试会检测到已有实例。
-
控制台输出示例 :
arduino单实例管理器实例已创建 无现有实例,启动新实例 应用启动成功 运行应用主循环 应用主循环结束 已有另一个实例正在运行 通知已有实例以聚焦窗口 无法启动:已有实例正在运行 是否为同一实例? true 锁文件已删除,应用已关闭
代码说明
- 单例实现 :
SingleInstanceManager
使用私有构造函数和getInstance()
确保唯一实例。 - 锁文件机制 :通过
app.lock
文件检测运行实例,模拟桌面应用的单实例检查(真实场景可能使用系统锁或 IPC)。 - 核心逻辑 :
checkInstance()
检查锁文件,startApp()
决定启动或通知已有实例,cleanup()
释放资源。 - 现实场景:在桌面应用(如基于 Electron 的 VS Code 或 Notepad++)中,单例模式确保单实例运行,防止资源冲突并统一用户体验(如聚焦已有窗口)。
总结
单例模式通过保证唯一实例和全局访问,解决了资源控制和状态一致性问题。从简单的配置管理器到桌面应用单例启动示例,展示了单例模式在现实世界中的强大应用。结合 TypeScript 的类型安全和锁文件机制,单例模式特别适合需要限制实例的场景,如桌面应用启动管理、日志记录或全局配置。未来可扩展到更复杂的场景,如多进程通信或跨平台单实例管理。