單例模式可以建立一個獨一無二的類別實例,並讓整個應用程式存取內部的狀態和資源。
現在很多網站都提供深色模式,來符合使用者的視覺習慣。通常這樣的設定只會有一份,以確保整體畫面一致,避免深色和淺色模式交錯而產生的不協調感。
這類設定還有很多,比如使用語言和登入狀態。我們希望網站能採用統一的設定,以維持畫面與功能的和諧。透過單例模式,網站可以使用唯一的類別實例,確保這些設定來自單一真實來源。
在我們的網站中,有許多資料必須透過網路請求來取得。然而,每次網路請求都會消耗伺服器資源,也可能增加雲端服務的託管費用。因此,我們希望藉由限制網路請求的數量來控制營運成本。
我們可以設計一個暫存機制,將每次網路請求的回傳結果保存起來。這樣,當使用者再次請求相同資料時,我們就能直接從暫存區中取得資料,而無需重複發送請求。為了實現這個功能,我們可以使用單例模式來設計暫存區,確保其唯一性,並讓整個應用程式都能存取相同的資源。
使用單例模式來實作暫存區,透過私有建構式限制類別的實例化行為,確保該類別只會被實例化一次。
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,因為它的請求帶有查詢參數,被視為一個全新的請求,因此需要重新向後端發出請求。
單例模式通過私有的建構函式來控制實例化行為,以確保應用程式中只會有一個實例。它還提供全局的訪問方式,讓應用程式中的每個部分都能訪問該實例並使用其中的狀態與資訊,從而統一資料來源。
由於單例模式具有唯一性和全局訪問的特性,非常適合用來管理應用程式層級的資源,如環境設定和資料庫連線池等。然而,在使用單例模式時,應注意保持介面的簡潔與職責的單一性,否則類別可能會變得過於複雜,導致程式的耦合性增加,進而提高維護難度。
https://github.com/chengen0612/design-patterns-typescript/blob/main/patterns/creational/singleton.ts