Copyright © 2008 ~ 2024 MUKI space* / omegaBook theme All Rights Reserved.

緣起

最近在看 JavaScript 設計模式學習手冊 第二版,才發現我在寫 JavaScript 時,一直都有用到 Singleton Pattern,但我卻不知道他叫做 Singleton Pattern,也沒有深入理解過他的特性與優缺點。

所以這次要幫自己補充知識點,分享我理解的 Singleton Pattern,也許還只是皮毛,也許還有錯誤與不足,都歡迎大家指正,感謝 🙏

設計模式的基本概念

免不俗,先講一下什麼是設計模式。

簡單來說,就是一種可以重複使用的解決方案,我個人喜歡拿數學公式做比喻:「某種類型的問題,套上特定的公式,就能得到正確的答案」。

而設計模式就是那些數學公式,我們在不同情境下碰到的問題,可以使用對應的設計模式來解決。雖然我們也能自己想出解法,但使用前人結構化的設計模式,能幫助我們提高程式碼的可讀性以及可維護性;也能讓我們愉快的重用,並提高開發效率。

設計模式有哪些

根據「JavaScript 設計模式學習手冊 第二版」一書的內容,作者 Addy 將設計模式分為三類:

  • 建立型設計模式 creational design pattern
  • 結構型設計模式 structural design pattern
  • 行為型設計模式 behaviroal design pattern

我發現一個可愛的寶藏網站:Refactoring Guru,擁有圖文並茂的版面,分享這三種設計模式的類別。

建立型設計模式

◿ 來源:https://refactoringguru.cn/design-patterns/creational-patterns

結構型設計模式

◿ 來源:https://refactoringguru.cn/design-patterns/structural-patterns

行為型設計模式

◿ 來源:https://refactoringguru.cn/design-patterns/behavioral-patterns

什麼是 Singleton 設計模式

今天要分享的 Singleton 設計模式,是分類在「建立型設計模式」裡,所以我們是在處理建立物件機制時,會用到 Singleton 設計模式。

以往我們在建立類別時,可以有很多個實例,而且每個實例都是獨立的。

但,Singleton 設計模式是讓一個類別只會產生一個實例。即使你試圖建立該類別的多個實例,你會發現,儘管看似是獨立的實例,但實際上,他們都指向同一個實例。這就是 Singleton 設計模式的魅力所在。

接下來,我會先寫一個基本的 Class,再寫一個有 Singleton Pattern 的 Class,再來比較他們之間的差別。

基本的 Class 範例

▼ 在 JavaScript 中,我們可以使用 class 關鍵字來定義一個類別,並使用 constructor 來定義該類別的建構子。

// 定義一個 Human 類別
class Human {
  // 定義建構子
  constructor(name, sex) {
    this.name = name;
    this.sex = sex;
  }

  // 定義一個方法
  drink(val) {
    console.log(`${this.name} 喜歡喝${val}`)
  }
}

// 建立 Human 實例
let muki = new Human("MUKI", "female")
let dca = new Human("DCA", "male")

// 使用實例的方法
muki.drink('咀嚼系飲料') // 輸出:MUKI 喜歡喝咀嚼系飲料
dca.drink('果汁') // 輸出:DCA 喜歡喝果汁

▼ 我們建立了兩個實例,分別為 mukidca,可以透過運算子來判斷這兩個實例是否相同

console.log(muki === dca)
// false

回傳 false 的結果可知,我建立了兩個獨立的實例,他們完全不同。

改寫為 Singleton 設計模式

Singleton 設計模式的精髓,就是一個類別只能有一個實例。我們在建立類別時,會先判斷這個類別的實例存在與否?如果不存在,才能建立一個新的實例;如果存在,就回傳對物件的參照。

▼ 將剛剛的 Human Class 改寫為 Singleton 設計模式,此外將 Human 設為私有,避免其他人繞過 HumanSingleton 直接建立 Human

// 使用函數表達式(IIFE)來建立一個私有的 Human 類別
let HumanSingleton = (function () {
  // 私有的 Human 類別
  class Human {
    constructor(name, sex) {
      this.name = name
      this.sex = sex
    }

    drink(val) {
      console.log(`${this.name} 喜歡喝${val}`)
    }
}

let instance

return {
    // 加入了 Singleton 設計模式
    // 建立類別時,會先判斷這個類別的實例存在與否?如果不存在,才能建立一個新的實例;如果存在,就回傳對物件的參照。
    getInstance: function (name, sex) {
      if (!instance) {
        instance = new Human(name, sex)
      }
      return instance
    }
  }
})()

let muki = HumanSingleton.getInstance("MUKI", "female")
let dca = HumanSingleton.getInstance("DCA", "male")

// 由於 HumanSingleton 是一個 Singleton,所以 muki 和 dca 實際上是同一個實例。
// 因此調用 dca.drink('果汁') 時,輸出的名字仍然是 "MUKI"。

muki.drink('咀嚼系飲料') // 輸出:MUKI 喜歡喝咀嚼系飲料
dca.drink('果汁') // 輸出:MUKI 喜歡喝果汁

▼ 一樣來判斷這兩個實例是否相同,回傳 true 可知,他們是一樣的實例

console.log(muki === dca)
// true

跟前面的範例比對結果,會發現有兩個地方不同:

  1. 即使我建立了 dca 實例,但他其實是 muki 實例,所以輸入 dca.drink('果汁') 時,會顯示的是 MUKI 喜歡喝果汁
  2. mukidca 實例回傳 true,表示他們是同一個實例

會產生不同的結果,最大的原因是 21 ~ 24 行的程式碼加入了 Singleton 設計模式:「先判斷實例存在與否」這段程式碼,導致後面的結果不同。此時真正實現了一個 Class 只能有一個實例的設計模式

延伸應用:JavasScript 的全域實例

在 ES2015+ 後,我們可以用 Singleton 設計模式建立 JavaScript Class 的全域實例,並能透過 export / import module 的方式來設置並取得資料,也就是說在 Vue, React 這種現代框架上都能實作!

以下是一個簡易的 Vue.js 範例:

▼ 在 js 模組檔案,設定 setget 的 function

let a = 0

export const setA = (val: number) => {
  a = val
}

export const getA = () => {
  return a
}

▼ 假設我有兩個 Vue 檔案,分別為:home.vue 以及 setting.vue,都載入了 singleton.ts 模組檔

<script>
  import { setA } from '@/models/singleton.ts'
  
  setA(1234)
</script>
<script>
  import { getA } from '@/models/singleton.ts'
  
  console.log('getA', getA())
</script>

此時,會有兩種結果:

  • 如果我們先瀏覽 setting.vue,那 getA() 的結果就是他初始化的數值 0
  • 如果我們先瀏覽 home.vue,再瀏覽 setting.vue,geA() 的結果,就是在 home.vue 呼叫 setA(1234) 而修改過的數值 1234

由此可知,透過 import 匯入的模組檔案,就是 Singleton 設計模式的一種實現方式,因為它們在整個應用程式中只會被建立一次,並且可以在多個地方被重複使用。這種特性,有效的減少了記憶體的浪費,也使得模組檔案適合用於儲存與管理全域的狀態,或共享資料 ... 等等。

Singleton 使用情境

我們在什麼情況下,會「刻意」選擇 Singleton 設計模式來設計我們的程式碼呢?

稍微翻了書跟查了資料,大部分還是在講當有「全域狀態」以及「共享資料」的需求時,可以考慮使用 Singleton 來規劃你的程式碼:

  1. 全域狀態:應用程式需要管理全域狀態時,可以做一個 set()get() 來處理,就像我前面提到的範例。
  2. 資源共享:可以用 Sinleton 管理共享資源,例如連結資料庫,確保所有的資料庫操作都是用同一個資料庫連結,以避免資源浪費。

但大家也不提倡過度使用 Singleton 設計模式,因為不是所有情況都適合使用。例,如果應用程式需要大量的狀態管理,並且需要追蹤狀態變化時,使用狀態管理的 Library 可能是更好的選擇。

Singleton 的優點與缺點

我覺得 Singleton 有點像雙面刃,他的優點正好也是他的缺點。他比較適合處理簡單不複雜的情境,如果情境複雜化,那當初的優點就會全部變成缺點。

  • 例如,他可以管理全域狀態,但過多的使用,會使得全域狀態很難管理與追蹤。
  • 例如,他可以確保我們共用一個實例,但也導致非常難做測試。

Singleton 是一種強大而靈活的設計模式,它在許多情況下都非常有用。然而,就像所有的工具一樣,我們需要根據具體的需求和情況來選擇是否使用它。

結語

文章跟大家分享了我自己對 Singleton 的看法和理解,也許還只是皮毛,也許還有錯誤與不足,都歡迎大家指正,感謝 🙏

歡迎給我點鼓勵,讓我知道你來過 :)

11
4
4
1
Subscribe
Notify of
guest

0 則留言
Inline Feedbacks
View all comments