設計模式之單例模式

單例模式的基本概念

單例模式是一種保證一個類僅有一個實例,並提供一個全局訪問點的設計模式,它還有些許其他的叫法,比如說懶漢模式、單子模式等。那麼這種設計模式解決了一個什麼事情呢?我們來看下這樣一段代碼

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 上去擴展實現對應的業務場景的功能。

參考文獻

原文地址:https://zhengjiangtao.cn/coding/singleton.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/UVSRsIIqjv6TWEVnSmGyOw