设计模式之单例模式

单例模式的基本概念

单例模式是一种保证一个类仅有一个实例,并提供一个全局访问点的设计模式,它还有些许其他的叫法,比如说懒汉模式、单子模式等。那么这种设计模式解决了一个什么事情呢?我们来看下这样一段代码

function Foo() {}

const s1 = new Foo();
const s2 = new Foo();
console.log(s1 === s2); // false

从结果上看,每通过构造函数创建一个对象,就会新开辟一片内存去存储,所以两个对象的值是不相等,而我们要做的事情是,让它每次创建出来的结果都是同一个,那这就是单例模式,运用到现实生活中的场景,比如说全局状态、前端页面中的模态框等等。下面跟着单例模式的实现,我们来一步一步地改造它。

单例模式的实现

随着 ECMAScript 标准的更新换代,最开始我们是通过函数+全局变量,或者函数加闭包的形式去实现单例模式,到后来 ES6 中有了 Class 的语法,我们可以用 Class 去写单例,所以本文通过三种方式介绍单例模式的实现。

全局变量 + 函数

function Singleton() {}

Singleton.getInstance = function () {
  if (!window.instance) {
    window.instance = new Singleton();
  }
  return window.instance;
};

const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1 === s2); // true

这里思考下这样写有什么不好的吗?有的。例如程序员李雷在全局变量上挂载了一个 instance 属性,程序员韩梅梅也在全局变量 windows 上挂载了一个 instance 属性。他们互相都不告诉对方自己在 windows 上挂载了一个 instance 属性,那么这个时候是不是就会产生冲突呢?所以这样子写,不好。

函数+闭包

function Singleton() {}

Singleton.getInstance = (function () {
  let instance = null;
  return function () {
    if (!instance) {
      instance = new Singleton();
    }
    return instance;
  };
})();

const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1 === s2); // true

ES 中面向对象的 Class

class Singleton {
  static getInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1 === s2); // true

单例模式的应用

单例模式作为最简单的设计模式之一,在软件开发中应用也很广泛,下面笔者结合自己的经历,主要从前端和后端分别举一个例子来介绍设计模式的应用。

单例模式在前端的应用

前面我们说了模态框,比如说,你页面有个登录按钮,点击后会弹出一个登录框,这里每次点击登录都重新弹一个新的模态框,显然是不必要的,因为他们内容是一样的,所以我们期望把它缓存下来,核心代码如下:

class Modal {
  static getInstance() {
    if (!Modal.instance) {
      Modal.instance = new Modal();
      Modal.instance.createElement();
    }
    return Modal.instance;
  }

  createElement() {
    this.div = document.createElement('div');
    this.div.id = 'modal';
    this.div.innerHTML = '全局模态框';
    this.div.style.display = 'none';
    document.body.appendChild(this.div);
  }

  open() {
    this.div.style.display = 'block';
  }

  close() {
    this.div.style.display = 'none';
  }
}

document.getElementById('BtnOpen').addEventListener('click', () => {
  const modal = Modal.getInstance();
  modal.open();
});

document.getElementById('BtnClose').addEventListener('click', () => {
  const modal = Modal.getInstance();
  modal.close();
});

具体的 demo 地址: https://zhengjiangtao.cn/show/design-mode/singleton.html

单例模式在后端的应用

这个是笔者在通过 nodejs 做微信开发的时候,借助单例模式的思想优化相关的业务代码的实践所得,就是不能每次前端这边来一个请求,或者别的地方引用或者使用到封装的微信接口 API,就重新创建一个新的,那么数据量上去了,这边开销是会很大的,比如百万、千万等等,所以我们期望把它缓存下来,然后用到直接取就好了。

// 创建一个微信公众号相关的API类
class WechatOfficalAccountApi {
  constructor(appId, appSecret, token) {
    // code...
  }
}

// 单例模式的实现
const createWechatOfficalAccountApi = (function (appId, appSecret, token) {
  let instance = null;
  return function () {
    if (!instance) {
      instance = new WechatOfficalAccountApi(appId, appSecret, token);
    }
    return instance;
  };
})();

考虑到微信公众号的类另有用途,所以就没有都封装到类里面,而是单独抛出一个函数去做这件事,大家想一下这样写好不好啊? 是的,不好。问题就在于,比如说我创建了一个单例对象实例是去处理公众号”江涛学编程“的相关业务的,后来迫于生计,老板决定卖艺,又搞了个”江涛学音乐“,那么这个时候你这个单例就歇菜了,因为它只有一个实现例的全局访问点,而 appid 每个微信公众号都是不同的。

考虑到楼上这个场景,其实不能简单地去像楼上去设计单例模式。我想到一个例子,就好比水产养殖这个专业,海王他就知道,单纯地在池子里养草鱼,草鱼会有点孤单,它会不会不快乐呢?它会不会绝食呢?于是它把龙虾也放了进来,这样子至少显得不那么孤单,可以聊聊天,龙虾你今天吃什么?草鱼你今天吃什么?池子里充满了欢声笑语,哦,我明白了,我也给咱微信 API 接口造一个池子,开干。

// 创建一个连接池
const wechatOfficalAccountApiPool = {};

// 创建一个微信公众号相关的API类
class WechatOfficalAccountApi {
  constructor(appId, appSecret, token) {
    // code...
  }
}

// 单例模式的实现
function createWechatOfficalAccountApi(appId, appSecret, token) {
  let instance = wechatOfficalAccountApiPool[appId];
  if (!instance) {
    instance = new WechatOfficalAccountApi(appId, appSecret, token);
    wechatOfficalAccountApiPool[appId] = instance;
  }
  if (instance.appSecret !== appSecret || instance.token !== token) {
    throw new Error(
      `createWechatOfficalAccountApi(${appId}, ${appSecret}, ${token}): ` +
        `conflict with existing one: (${instance.appId}, ${instance.appSecret}, ${instance.token})`
    );
  }
  return instance;
}

为了更健壮鲁棒一点,我们已知微信的 appid 是唯一的,就以它作为 key 来搞,这样子的话就可以处理多个业务场景了,比如老板开了好多个媒体号,有“江涛学编程”,”江涛学音乐“,”江涛去旅行“等等,根据不同的业务场景和用途,就可以在最基础的通用性强的微信接口 API 上去扩展实现对应的业务场景的功能。

参考文献

results matching ""

    No results matching ""