单例模式

238 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第17天,点击查看活动详情

单例模式描述

单例模式 (Singleton Pattern)又称为单体模式,顾名思义,单例模式中Class的实例个数最多为1,保证一个类只有一个实例,并提供一个访问它的全局访问点。也就是说,第二次使用同一个类创建新对象的时候,应该得到与第一次创建的对象完全相同的对象。

当需要一个对象去贯穿整个系统执行某些任务时,单例模式就派上了用场。而除此之外的场景尽量避免单例模式的使用,因为单例模式会引入全局状态,而一个健康的系统应该避免引入过多的全局状态。

单例模式可能是设计模式里面最简单的模式了,虽然简单,但在我们日常生活和编程中却经常接触到,来看一下实际遇到的单例模式的场景:

  • 比如玩单机或者运营类游戏,第二天继续玩时,我们希望是拿到同一份存档接着之前的玩,而不是每次都是从头开始。
  • 编程中也有很多对象我们只需要唯一一个,比如数据库连接、线程池、配置文件缓存、浏览器中的 window/document 等,如果创建多个实例,会带来资源耗费严重,或访问行为不一致等情况。
  • 类似于数据库连接实例,我们可能频繁使用,但是创建它所需要的开销又比较大,这时只使用一个数据库连接就可以节约很多开销。一些文件的读取场景也类似,如果文件比较大,那么文件读取就是一个比较重的操作。比如这个文件是一个配置文件,那么完全可以将读取到的文件内容缓存一份,每次来读取的时候访问缓存即可,这样也可以达到节约开销的目的。

另外前端业务中可能遇到的单例模式:

  • 定义命名空间和实现分支型方法
  • 登录框/单点登录
  • vuexredux中的store

在类似场景中,这些例子有以下特点:

  • 每次访问者来访问,返回的都是同一个实例;
  • 如果一开始实例没有创建,那么这个特定类需要自行创建这个实例;

实现单例模式

实现单例模式需要解决以下几个问题:

  • 如何确定Class只有一个实例?
  • 如何简便的访问Class的唯一实例?
  • Class如何控制实例化的过程?
  • 如何将Class的实例个数限制为1?

我们一般通过实现以下两点来解决上述问题:

  • 隐藏Class的构造函数,避免多次实例化
  • 通过暴露一个 getInstance() 方法来创建/获取唯一实例

根据上面游戏的例子提炼一下单例模式,游戏可以被认为是一个特定的类(Singleton),而存档是单例(instance),每次访问特定类的时候,都会拿到同一个实例。主要有下面几个概念:

  • Singleton :特定类,这是我们需要访问的类,访问者要拿到的是它的实例;
  • instance :单例,是特定类的实例,特定类一般会提供 getInstance 方法来获取该单例;
  • getInstance :获取单例的方法,或者直接由 new 操作符获取;

1、IIFE 方式创建单例模式

const Singleton = (function() {
    let _instance = null        // 存储单例
    const Singleton = function() {
        if (_instance) return _instance     // 判断是否已有单例
        _instance = this
        this.init()                         // 初始化操作
        return _instance
    }
    // 初始化方法
    Singleton.prototype.init = function() {
        this.foo = 'Singleton Pattern'
    }
    // 访问实例方法
    Singleton.getInstance = function() {
        if (_instance) return _instance
        _instance = new Singleton()
        return _instance
    }
    
    return Singleton
})()
​
const visitor1 = new Singleton()
const visitor2 = new Singleton()         // 既可以 new 获取单例
const visitor3 = Singleton.getInstance() // 也可以 getInstance 获取单例
// 每次生成及访问都是同一个实例
console.log(visitor1 === visitor2)  // true
console.log(visitor1 === visitor3)  // true
  • 闭包开销,并且因为 IIFE 操作带来了额外的复杂度,让可读性变差。
  • IIFE 内部返回的 Singleton 才是我们真正需要的单例的构造函数,外部的 Singleton 把它和一些单例模式的创建逻辑进行了一些封装。
  • IIFE 方式除了直接返回一个方法/类实例之外,还可以通过模块模式的方式来进行。

IIFE 方式本质还是通过函数作用域的方式来隐藏内部作用域的变量,有了 ES6 的 let/const 之后,可以通过 { } 块级作用域的方式来隐藏内部变量

2、块级作用域方式创建单例

let getInstance;
{
    let _instance = null        // 存储单例
    // 实际单例的构造函数
    const Singleton = function() {
        if (_instance) return _instance     // 判断是否已有单例
        _instance = this
        this.init()                         // 初始化操作
        return _instance
    }
    // 初始化
    Singleton.prototype.init = function() {
        this.foo = 'Singleton Pattern'
    }
    // 访问/获取实例方法
    getInstance = function() {
        if (_instance) return _instance
        _instance = new Singleton()
        return _instance
    }
}
const visitor1 = getInstance()
const visitor2 = getInstance()
console.log(visitor1 === visitor2)  // true

3、单例模式赋能

之前的例子中,单例模式的创建逻辑和原先这个类的一些功能逻辑(比如 init 等操作)混杂在一起,根据单一职责原则,这个例子我们还可以继续改进一下,将单例模式的创建逻辑和特定类的功能逻辑拆开,这样功能逻辑就可以和正常的类一样。

/* 功能类 */
class FuncClass {
    constructor(bar) { 
        this.bar = bar
        this.init()
    }
    init() {
        this.foo = 'Singleton Pattern'
    }
}
/* 单例模式的赋能类 */
const Singleton = (function() {
    let _instance = null        // 存储单例
    
    const ProxySingleton = function(bar) {
        if (_instance) return _instance     // 判断是否已有单例
        _instance = new FuncClass(bar)
        return _instance
    }
    
    ProxySingleton.getInstance = function(bar) {
        if (_instance) return _instance
        _instance = new Singleton(bar)
        return _instance
    }
    
    return ProxySingleton
})()
const visitor1 = new Singleton('单例1')
const visitor2 = new Singleton('单例2')
const visitor3 = Singleton.getInstance()
console.log(visitor1 === visitor2)  // true
console.log(visitor1 === visitor3)  // true

总结

优点

  • 划分命名空间,减少全局变量
  • 增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护
  • 且只会实例化一次。简化了代码的调试和维护

缺点

  • 由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合
  • 从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一 个单元一起测试。

我们应该在什么场景下使用单例模式呢:

  • 当一个类的实例化过程消耗的资源过多,可以使用单例模式来避免性能浪费;
  • 当项目中需要一个公共的状态,那么需要使用单例模式来保证访问一致性;