iT邦幫忙

2024 iThome 鐵人賽

DAY 10
0

單例模式可以建立一個獨一無二的類別實例,並讓整個應用程式存取內部的狀態和資源。

生活案例

現在很多網站都提供深色模式,來符合使用者的視覺習慣。通常這樣的設定只會有一份,以確保整體畫面一致,避免深色和淺色模式交錯而產生的不協調感。

這類設定還有很多,比如使用語言和登入狀態。我們希望網站能採用統一的設定,以維持畫面與功能的和諧。透過單例模式,網站可以使用唯一的類別實例,確保這些設定來自單一真實來源。

舉個例子

在我們的網站中,有許多資料必須透過網路請求來取得。然而,每次網路請求都會消耗伺服器資源,也可能增加雲端服務的託管費用。因此,我們希望藉由限制網路請求的數量來控制營運成本。

我們可以設計一個暫存機制,將每次網路請求的回傳結果保存起來。這樣,當使用者再次請求相同資料時,我們就能直接從暫存區中取得資料,而無需重複發送請求。為了實現這個功能,我們可以使用單例模式來設計暫存區,確保其唯一性,並讓整個應用程式都能存取相同的資源。

使用單例模式來實作暫存區,透過私有建構式限制類別的實例化行為,確保該類別只會被實例化一次。

class QueryCache {
  private static instance: QueryCache;
  private cache: Map<string, any>;

  private constructor() {
    this.cache = new Map();
  }

  public static getInstance() {
    if (!QueryCache.instance) {
      QueryCache.instance = new QueryCache();
    }

    return QueryCache.instance;
  }

  get(key: string) {
    return this.cache.get(key);
  }

  set(key: string, data: any) {
    this.cache.set(key, data);
  }

  has(key: string) {
    return this.cache.has(key);
  }
}

定義隨機信箱產生器。getOne 方法會從 randomuser 取得一個隨機信箱,我們可以利用這個 API 的隨機性來測試暫存功能是否正常運作。

class RandomEmail {
  async getOne(params?: EmailQueryParams) {
    const url = this.getUrl(params);
    const data = await this._getOne(url);

    if (data && data.results.length) {
      console.log(data.results[0].email);
    }
  }

  private async _getOne(url: string): Promise<EmailQueryResponse | undefined> {
    const cache = QueryCache.getInstance();

    if (cache.has(url)) {
      console.log("Getting email from cache...");
      return cache.get(url);
    }

    try {
      const response = await fetch(url);
      const data = await response.json();
      console.log("Getting email from server...");
      cache.set(url, data);
      return data;
    } catch {
      console.error("Failed to fetch email");
    }
  }

  private getUrl(params: EmailQueryParams = {}) {
    const url = new URL("https://randomuser.me/api/");

    url.searchParams.set("inc", "email");

    for (const [key, value] of Object.entries(params)) {
      url.searchParams.set(key, value);
    }

    return url.href;
  }
}

建立三個客戶端程式。程式 A 和程式 B 會取得一個隨機信箱,而程式 C 則會攜帶識別碼 'foo' 取得指定資料。

class ClientA {
  static async main() {
    const randomEmail = new RandomEmail();
    await randomEmail.getOne();
  }
}

class ClientB {
  static async main() {
    const randomEmail = new RandomEmail();
    await randomEmail.getOne();
  }
}

class ClientC {
  static async main() {
    const randomEmail = new RandomEmail();
    await randomEmail.getOne({ seed: "foo" });
  }
}

測試時間。

class RandomEmailTestDrive {
  static async main() {
    await ClientA.main();
    await ClientB.main();
    await ClientC.main();
  }
}

RandomEmailTestDrive.main();

輸出結果

Getting email from server...
edward.nguyen@example.com
Getting email from cache...
edward.nguyen@example.com
Getting email from server...
andreia.monteiro@example.com

當類別 A 發送請求時,由於暫存區中尚無相關資料,該請求會被傳送至後端,並將回傳結果儲存至暫存區。當類別 B 發出同樣的請求時,暫存區內已有對應的資料,因此類別 B 可以直接從暫存區取得查詢結果,而不必再次發出請求。至於類別 C,因為它的請求帶有查詢參數,被視為一個全新的請求,因此需要重新向後端發出請求。

定義

Singleton Pattern

  • 單例(Singleton): 建立獨一無二的類別實例

單例模式通過私有的建構函式來控制實例化行為,以確保應用程式中只會有一個實例。它還提供全局的訪問方式,讓應用程式中的每個部分都能訪問該實例並使用其中的狀態與資訊,從而統一資料來源。

由於單例模式具有唯一性和全局訪問的特性,非常適合用來管理應用程式層級的資源,如環境設定和資料庫連線池等。然而,在使用單例模式時,應注意保持介面的簡潔與職責的單一性,否則類別可能會變得過於複雜,導致程式的耦合性增加,進而提高維護難度。

總結

  • 單例模式確保一個應用程式當中只會存在一個類別實例
  • 單例模式提供一個全局訪問的方式,為整個應用程式提供一個統一的資料來源
  • 延遲載入必要性較低的單例類別可以避免潛在的效能浪費
  • 在多執行續環境中,處理不當的單例類別可能會被重複實例化,需要使用同步機制來確保單例模式的唯一性

完整範例

https://github.com/chengen0612/design-patterns-typescript/blob/main/patterns/creational/singleton.ts


上一篇
Day 09 - Abstract Factory 抽象工廠
下一篇
Day 11 - Command 命令
系列文
前端也想學設計模式30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言