Skip to content

单例模式详解:概念、实现与应用

引言

单例模式是面向对象编程中最简单但最常用的设计模式之一。它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。虽然概念简单,但在实际应用中有许多细节需要注意。

什么是单例模式?

单例模式是一种创建型设计模式,它保证一个类只有一个实例,并提供一个全局访问点来访问这个实例。这个定义包含了几个关键点:

  1. 唯一实例:一个类只能创建一个实例
  2. 全局访问:提供一个全局访问点来获取这个实例
  3. 自行创建:类自己负责创建和管理唯一的实例

为什么需要单例模式?

在许多情况下,我们需要确保系统中只有一个实例存在:

1. 资源管理

某些资源非常稀缺或者昂贵,创建多个实例会造成资源浪费:

  • 数据库连接池
  • 线程池
  • 日志文件句柄

2. 状态一致性

当需要在整个应用程序中维护一致的状态时:

  • 全局配置对象
  • 缓存管理器
  • 应用状态管理器

3. 协调操作

当需要协调系统中的各种操作时:

  • 打印任务队列
  • 文件系统操作
  • 设备驱动程序

单例模式的基本实现

让我们从最简单的单例模式实现开始:

javascript
// ES5 实现方式
var Singleton = (function() {
  var instance;
  
  function createInstance() {
    var object = new Object("I am the instance");
    return object;
  }
  
  return {
    getInstance: function() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    }
  };
})();

// 使用示例
var instance1 = Singleton.getInstance();
var instance2 = Singleton.getInstance();

console.log(instance1 === instance2); // true

实现要点分析

  1. 私有实例变量:使用闭包创建私有变量 instance 来存储唯一实例
  2. 延迟初始化:只有在第一次调用 getInstance 时才创建实例
  3. 实例检查:每次调用都检查实例是否存在,不存在则创建,存在则返回

经典实现方式

1. 饿汉式(Eager Initialization)

饿汉式在类加载时就创建实例,这种方式简单但可能造成资源浪费:

javascript
class EagerSingleton {
  // 在类定义时就创建实例
  static instance = new EagerSingleton();
  
  constructor() {
    // 防止通过 new 直接创建实例
    if (EagerSingleton.instance) {
      return EagerSingleton.instance;
    }
    
    // 初始化操作
    this.initialized = true;
    EagerSingleton.instance = this;
  }
  
  static getInstance() {
    return EagerSingleton.instance;
  }
  
  getInfo() {
    return "Eager Singleton Instance";
  }
}

// 使用示例
const instance1 = EagerSingleton.getInstance();
const instance2 = EagerSingleton.getInstance();
console.log(instance1 === instance2); // true

2. 懒汉式(Lazy Initialization)

懒汉式在第一次使用时才创建实例,实现了延迟加载:

javascript
class LazySingleton {
  constructor() {
    if (LazySingleton.instance) {
      return LazySingleton.instance;
    }
    
    // 初始化操作
    this.createdAt = new Date();
    LazySingleton.instance = this;
  }
  
  static getInstance() {
    if (!LazySingleton.instance) {
      LazySingleton.instance = new LazySingleton();
    }
    return LazySingleton.instance;
  }
  
  getCreationTime() {
    return this.createdAt;
  }
}

// 使用示例
console.log("程序启动");
// 延迟一段时间后才创建实例
setTimeout(() => {
  const instance1 = LazySingleton.getInstance();
  console.log("实例创建时间:", instance1.getCreationTime());
}, 2000);

现代 JavaScript 实现方式

1. ES6 模块模式

利用 ES6 模块的特性,可以非常简洁地实现单例:

javascript
// logger.js
class Logger {
  constructor() {
    this.logs = [];
  }
  
  log(message) {
    const timestamp = new Date().toISOString();
    this.logs.push(`[${timestamp}] ${message}`);
    console.log(`[${timestamp}] ${message}`);
  }
  
  getLogs() {
    return [...this.logs]; // 返回副本,避免外部修改
  }
}

// 导出单例实例
export default new Logger();
javascript
// 使用示例
// app.js
import logger from './logger.js';

logger.log("应用启动");

// utils.js
import logger from './logger.js';

logger.log("执行工具函数");

// main.js
import logger from './logger.js';
import './app.js';
import './utils.js';

console.log("所有日志:", logger.getLogs());

2. Symbol 实现私有构造函数

使用 Symbol 可以更好地实现私有构造函数:

javascript
const INSTANCE = Symbol('instance');
const PRIVATE = Symbol('private');

class ModernSingleton {
  constructor(privateKey) {
    // 检查是否通过 getInstance 方法调用
    if (privateKey !== PRIVATE) {
      throw new Error('请使用 ModernSingleton.getInstance() 获取实例');
    }
    
    this.data = "Modern Singleton Data";
  }
  
  static getInstance() {
    if (!this[INSTANCE]) {
      this[INSTANCE] = new ModernSingleton(PRIVATE);
    }
    return this[INSTANCE];
  }
  
  getData() {
    return this.data;
  }
  
  setData(data) {
    this.data = data;
  }
}

// 使用示例
try {
  const wrongInstance = new ModernSingleton(); // 抛出错误
} catch (e) {
  console.log(e.message);
}

const instance1 = ModernSingleton.getInstance();
const instance2 = ModernSingleton.getInstance();
console.log(instance1 === instance2); // true

单例模式的实际应用场景

1. 配置管理器实现

配置管理器是单例模式最典型的应用之一。在任何应用程序中,都需要一个全局可访问的配置系统来管理各种设置。

javascript
// ConfigManager.js
class ConfigManager {
  constructor() {
    // 防止重复实例化
    if (ConfigManager.instance) {
      return ConfigManager.instance;
    }
    
    // 默认配置
    this.config = {
      apiUrl: 'https://api.example.com',
      timeout: 5000,
      debug: false,
      version: '1.0.0'
    };
    
    // 保存实例
    ConfigManager.instance = this;
    return this;
  }
  
  // 获取配置项
  get(key) {
    return this.config[key];
  }
  
  // 设置配置项
  set(key, value) {
    this.config[key] = value;
  }
  
  // 批量设置配置
  setBulk(configObj) {
    Object.assign(this.config, configObj);
  }
  
  // 获取所有配置
  getAll() {
    return { ...this.config };
  }
}

// 导出单例实例
const configManager = new ConfigManager();
Object.freeze(configManager); // 冻结对象,防止意外修改

export default configManager;

2. 日志记录器实现

日志记录器是另一个常见的单例模式应用场景。在整个应用程序中,我们通常希望所有的日志都通过统一的接口进行记录,以便于管理和分析。

javascript
// Logger.js
class Logger {
  constructor() {
    if (Logger.instance) {
      return Logger.instance;
    }
    
    this.logs = [];
    this.levels = ['DEBUG', 'INFO', 'WARN', 'ERROR'];
    this.currentLevel = 'INFO';
    
    Logger.instance = this;
    return this;
  }
  
  // 设置日志级别
  setLevel(level) {
    if (this.levels.includes(level)) {
      this.currentLevel = level;
    }
  }
  
  // 检查日志级别是否应该被记录
  shouldLog(level) {
    return this.levels.indexOf(level) >= this.levels.indexOf(this.currentLevel);
  }
  
  // 格式化日志消息
  formatMessage(level, message) {
    const timestamp = new Date().toISOString();
    return `[${timestamp}] ${level}: ${message}`;
  }
  
  // 记录日志
  log(level, message) {
    if (!this.shouldLog(level)) {
      return;
    }
    
    const formattedMessage = this.formatMessage(level, message);
    this.logs.push(formattedMessage);
    
    // 在控制台输出
    switch (level) {
      case 'ERROR':
        console.error(formattedMessage);
        break;
      case 'WARN':
        console.warn(formattedMessage);
        break;
      case 'INFO':
        console.info(formattedMessage);
        break;
      default:
        console.log(formattedMessage);
    }
  }
  
  // 便捷方法
  debug(message) { this.log('DEBUG', message); }
  info(message) { this.log('INFO', message); }
  warn(message) { this.log('WARN', message); }
  error(message) { this.log('ERROR', message); }
  
  // 获取日志
  getLogs() {
    return [...this.logs];
  }
}

// 导出单例实例
const logger = new Logger();
Object.freeze(logger);

export default logger;

3. 缓存管理器实现

缓存管理器是单例模式的另一个重要应用场景。通过单例模式,我们可以确保整个应用程序共享同一个缓存实例,避免重复缓存相同的数据。

javascript
// CacheManager.js
class CacheManager {
  constructor() {
    if (CacheManager.instance) {
      return CacheManager.instance;
    }
    
    this.cache = new Map();
    this.timeouts = new Map();
    
    CacheManager.instance = this;
    return this;
  }
  
  // 设置缓存
  set(key, value, ttl = 0) {
    // 清除已存在的定时器
    if (this.timeouts.has(key)) {
      clearTimeout(this.timeouts.get(key));
      this.timeouts.delete(key);
    }
    
    // 设置缓存值
    this.cache.set(key, value);
    
    // 如果设置了过期时间
    if (ttl > 0) {
      const timeout = setTimeout(() => {
        this.cache.delete(key);
        this.timeouts.delete(key);
      }, ttl);
      
      this.timeouts.set(key, timeout);
    }
  }
  
  // 获取缓存
  get(key) {
    return this.cache.get(key);
  }
  
  // 检查缓存是否存在
  has(key) {
    return this.cache.has(key);
  }
  
  // 删除缓存
  delete(key) {
    if (this.timeouts.has(key)) {
      clearTimeout(this.timeouts.get(key));
      this.timeouts.delete(key);
    }
    
    return this.cache.delete(key);
  }
  
  // 清空所有缓存
  clear() {
    // 清除所有定时器
    for (const timeout of this.timeouts.values()) {
      clearTimeout(timeout);
    }
    
    this.timeouts.clear();
    this.cache.clear();
  }
  
  // 获取缓存大小
  size() {
    return this.cache.size;
  }
}

// 导出单例实例
const cacheManager = new CacheManager();
Object.freeze(cacheManager);

export default cacheManager;

单例模式的优缺点

优点

  1. 内存节约:确保只创建一个实例,节省内存
  2. 全局访问:提供全局访问点,方便使用
  3. 延迟初始化:可以延迟创建实例,提高性能
  4. 实例控制:可以精确控制实例的数量

缺点

  1. 违反单一职责原则:既负责创建实例,又负责业务逻辑
  2. 难以测试:全局状态使得单元测试变得困难
  3. 隐藏依赖:使用者可能不清楚他们依赖于单例
  4. 多线程问题:在多线程环境中需要特殊处理(虽然 JavaScript 是单线程的)

总结

单例模式是最简单的设计模式之一,但在实际应用中需要注意其实现细节。在 JavaScript 环境中,由于其单线程特性和 ES6 模块系统,实现单例模式相对简单。

通过本章的学习,我们了解了:

  1. 单例模式的基本概念和用途
  2. 多种实现方式(饿汉式、懒汉式、模块模式等)
  3. 实际应用场景(配置管理器、日志记录器、缓存管理器)
  4. 单例模式的优缺点

在下一章中,我们将探讨单例模式的一些争议以及现代开发中的替代方案。