设计模式——单例模式

429 阅读5分钟

定义

单例模式定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点

单例模式(Singleton Pattern)是最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式优点

  1. 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时;
  2. 由于单例模式只生成一个实例,减少了系统的性能开销;
  3. 单例模式避免了对资源的多重占用,例如在对一个文件执行写操作时,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作;
  4. 单例模式可以在系统设置全局的访问点,优化和共享资源访问。

单例模式缺点

  1. 单例模式一般没有接口,扩展困难,如果需要扩展,只能修改代码;
  2. 单例模式对测试不利,在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象;
  3. 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例,是不是单例取决于环境,单例模式把单例和业务逻辑融合在一个类中。

单例模式应用场景

  1. 要求生成唯一序列号的环境;
  2. web上的计数器,可以不用每次刷新都同步到数据库中,使用单例模式缓存计数器的值,并确保线程安全;
  3. 创建的一个对象需要消耗的资源过多,比如IO操作或链接数据库等

实现

传统单例模式

要实现一个标准的单例模式并不难,只需要使用一个变量来标识当前是否已经为某个类创建过对象,如果是,则在下次获取该类的实例时,直接返回之前创建的对象;如果不是,则创建一个对象。具体代码实现如下

var Singleton = function (name) {
    this.name = name;
    this.instance = null;
}
Singleton.prototype.getName = function () {
    return this.name;
}
Singleton.getInstance = function (name) {
    if (!this.instance) {
       this.instance = new Singleton(name) 
    }
    return this.instance;
}
const a = Singleton.getInstance('Tom');
const b = Singleton.getInstance('Mark');
console.log(a === b); // true

或者

var Singleton = function (name) {
    this.name = name;
}
Singleton.prototype.getName = function () {
    return this.name;
}
Singleton.getInstance = (function () {
    let instance = null
    return function (name) {
        if (!instance) {
            instance = new Singleton(name) 
        }
        return instance;
    }
})();
const a = Singleton.getInstance('Tom');
const b = Singleton.getInstance('Mark');
console.log(a === b); // true

我们通过Singleton.getInstance来获取Singleton类的唯一实例,这种方式相对简单,但是有一个问题就是增加了类的“不透明性”,Singleton类的使用者必须知道这是一个单例类,跟以前通过new XXX获取实例不同,这里是通过Singleton.getInstance来获取实例。虽然这里完成了一个单例模式的编写,但是这段单例模式的意义并不大,接下来我们逐步对单例模式的编码进行优化改进。

透明的单例模式

我们的目标是实现“透明”的单例类,可以通过new XXX的方式来得到类的实例,下图代码清单,我们使用CreatDiv单例类,它的作用是负责在页面中创建唯一的div节点,代码如下:

var CreateDiv =(function (){
  var instance;
  var CreateDiv = function(html){
    if(instance){
      return instance;
    }
    this.html = html;
    this.init();
    return instance = this;
  } ;
  CreateDiv.prototype.init = function(){
    var div = document.createElement('div');
    div.innerHTML = this.html;
    document.body.appendChild( div );
  };
  return CreateDiv;
})();

var a =  new CreateDiv('Tom');
var b = new CreateDiv('Mark');

console.log( a === b);  // true

虽然我们实现了透明单例类的编写,但它同样也有一些缺点 我们观察单例类的构造函数

  var CreateDiv = function(html){
    if(instance){
      return instance;
    }
    this.html = html;
    this.init();
    return instance = this;
  } ;

在这段代码中,CreateDiv的构造函数实际上负责了两件事,一个是创建对象和执行初始化init方法,另一个是保证只有一个对象。这不符合六大设计原则中的“单一职责原则”。 假设某天我们业务场景变化,需要创建多个div,就需要把这个类从单例类变成一个可生产多个实例的普通类,那我们需要修改CreateDiv构造函数,把控制创建唯一对象的代码去掉,这会导致我们的代码扩展性不好。

使用代理实现单例模式

现在我们通过引入代理类的方式,来解决上面提到的问题。 首先我们把CreateDiv构造函数中,把负责管理单例的控制代码移除,使它成为一个普通的创建类,代码如下:


var CreateDiv = function(html){
    this.html = html;
    this.init();
} ;
CreateDiv.prototype.init = function(){
    var div = document.createElement('div');
    div.innerHTML = this.html;
    document.body.appendChild( div );
};

接下来引入代理类proxySingletonCreateDiv:

var proxySingletonCreateDiv = (function(){
  var instance;
  return function ( html ){
    if(!instance){
      instance new CreateDiv(html);
    }
    return instance;
  }
})();

var a = new proxySingletonCreateDiv('Tom');
var b = new proxySingletonCreateDiv('Mark');
console.log( a === b);  // true

我们通过引入代理类的方式,同样完成了一个单例模式的编写,与之前不同的是,我们把控制单例的逻辑转移到了proxySingletonCreateDiv类中,这样通过CreateDiv这个普通类与proxySingletonCreateDiv类组合起来可以达到单例模式的效果。