每天一個設計模式之享元模式

godbmw發表於2018-12-20

作者按:《每天一個設計模式》旨在初步領會設計模式的精髓,目前採用javascriptpython兩種語言實現。誠然,每種設計模式都有多種實現方式,但此小冊只記錄最直截了當的實現方式 ?

個人技術部落格-godbmw.com 歡迎來玩! 每週至少 1 篇原創技術分享,還有開源教程(webpack、設計模式)、面試刷題(偏前端)、知識整理(每週零碎),歡迎長期關注!本篇部落格地址是:《每天一個設計模式之享元模式》

如果您也想進行知識整理 + 搭建功能完善/設計簡約/快速啟動的個人部落格,請直接戳theme-bmw

0. 專案地址

1. 什麼是“享元模式”?

享元模式:運用共享技術來減少建立物件的數量,從而減少記憶體佔用、提高效能。

  1. 享元模式提醒我們將一個物件的屬性劃分為內部和外部狀態
    • 內部狀態:可以被物件集合共享,通常不會改變
    • 外部狀態:根據應用場景經常改變
  2. 享元模式是利用時間換取空間的優化模式。

2. 應用場景

享元模式雖然名字聽起來比較高深,但是實際使用非常容易:只要是需要大量建立重複的類的程式碼塊,均可以使用享元模式抽離內部/外部狀態,減少重複類的建立。

為了顯示它的強大,下面的程式碼是簡單地實現了大家耳熟能詳的“物件池”,以彰顯這種設計模式的魅力。

3. 程式碼實現

這裡利用pythonjavascript實現了一個“通用物件池”類–ObjectPool。這個類管理一個裝載空閒物件的陣列,如果外部需要一個物件,直接從物件池中獲取,而不是通過new操作

物件池可以大量減少重複建立相同的物件,從而節省了系統記憶體,提高執行效率。

為了形象說明“享元模式”在“物件池”實現和應用,特別準備了模擬了File類,並且模擬了“檔案下載”操作。

通過閱讀下方程式碼可以發現:對於File類,內部狀態是pool屬性和download方法;外部狀態是namesrc(檔名和檔案連結)。藉助物件池,實現了File類的複用。

注:為了方便演示,Javascript實現的是併發操作,Python實現的是序列操作。輸出結果略有不同。

3.1 Python3 實現

from time import sleep


class ObjectPool:  # 通用物件池
    def __init__(self):
        self.__pool = []

    # 建立物件
    def create(self, Obj):
        # 物件池中沒有空閒物件,則建立一個新的物件
        # 物件池中有空閒物件,直接取出,無需再次建立
        return self.__pool.pop() if len(self.__pool) > 0 else Obj(self)

    # 物件回收
    def recover(self, obj):
        return self.__pool.append(obj)

    # 物件池大小
    def size(self):
        return len(self.__pool)


class File:  # 模擬檔案物件
    def __init__(self, pool):
        self.__pool = pool

    def download(self):  # 模擬下載操作
        print(`+ 從`, self.src, `開始下載`, self.name)
        sleep(0.1)
        print(`-`, self.name, `下載完成`)
        # 下載完畢後,將物件重新放入物件池
        self.__pool.recover(self)


if __name__ == `__main__`:
    obj_pool = ObjectPool()

    file1 = obj_pool.create(File)
    file1.name = `檔案1`
    file1.src = `https://download1.com`
    file1.download()

    file2 = obj_pool.create(File)
    file2.name = `檔案2`
    file2.src = `https://download2.com`
    file2.download()

    file3 = obj_pool.create(File)
    file3.name = `檔案3`
    file3.src = `https://download3.com`
    file3.download()

    print(`*` * 20)
    print(`下載了3個檔案, 但其實只建立了`, obj_pool.size(), `個物件`)

輸出結果(這裡為了方便演示直接使用了sleep方法,沒有再用多執行緒模擬):

+ 從 https://download1.com 開始下載 檔案1
- 檔案1 下載完成
+ 從 https://download2.com 開始下載 檔案2
- 檔案2 下載完成
+ 從 https://download3.com 開始下載 檔案3
- 檔案3 下載完成
********************
下載了3個檔案, 但其實只建立了 1 個物件

3.2 ES6 實現

// 物件池
class ObjectPool {
  constructor() {
    this._pool = []; //
  }

  // 建立物件
  create(Obj) {
    return this._pool.length === 0
      ? new Obj(this) // 物件池中沒有空閒物件,則建立一個新的物件
      : this._pool.shift(); // 物件池中有空閒物件,直接取出,無需再次建立
  }

  // 物件回收
  recover(obj) {
    return this._pool.push(obj);
  }

  // 物件池大小
  size() {
    return this._pool.length;
  }
}

// 模擬檔案物件
class File {
  constructor(pool) {
    this.pool = pool;
  }

  // 模擬下載操作
  download() {
    console.log(`+ 從 ${this.src} 開始下載 ${this.name}`);
    setTimeout(() => {
      console.log(`- ${this.name} 下載完畢`); // 下載完畢後, 將物件重新放入物件池
      this.pool.recover(this);
    }, 100);
  }
}

/****************** 以下是測試函式 **********************/

let objPool = new ObjectPool();

let file1 = objPool.create(File);
file1.name = "檔案1";
file1.src = "https://download1.com";
file1.download();

let file2 = objPool.create(File);
file2.name = "檔案2";
file2.src = "https://download2.com";
file2.download();

setTimeout(() => {
  let file3 = objPool.create(File);
  file3.name = "檔案3";
  file3.src = "https://download3.com";
  file3.download();
}, 200);

setTimeout(
  () =>
    console.log(
      `${"*".repeat(50)}
下載了3個檔案,但其實只建立了${objPool.size()}個物件`
    ),
  1000
);

輸出結果如下:

+ 從 https://download1.com 開始下載 檔案1
+ 從 https://download2.com 開始下載 檔案2
- 檔案1 下載完畢
- 檔案2 下載完畢
+ 從 https://download3.com 開始下載 檔案3
- 檔案3 下載完畢
**************************************************
下載了3個檔案,但其實只建立了2個物件

4. 參考

  • 《JavaScript 設計模式和開發實踐》

相關文章