引言
在軟體開發中,物件的建立是最常見的操作之一。我們通常直接使用 new
關鍵字來實例化物件,但當建立過程變得複雜、需要非同步操作,或者我們想對建立過程有更多控制時,直接使用 new
可能就不夠了。
本文將介紹靜態工廠模式如何解決這些問題,並以 ImageLoader
重構為例,展示實際應用場景
什麼是工廠模式?
工廠模式的核心概念是將物件的建立邏輯從直接的 new
呼叫中抽離出來,委託給一個專門的「工廠」來負責。這樣做的主要目的是降低耦合、提高彈性。
工廠模式有多種變體:
靜態工廠方法詳解
靜態工廠方法是類別內部的一個 static
方法,負責建立並返回該類別的實例(或其子類別的實例)。
命名慣例
常見的靜態工廠方法名稱:
create()
- 建立新實例
最常見的命名,表示建立一個全新的實例:
// 組件建立
const button = Button.create({ text: 'Click me', onClick: handleClick });
// API 客戶端建立
const apiClient = ApiClient.create({ baseURL: 'https://api.example.com' });
// 資料庫連接建立
const dbConnection = await DatabaseConnection.create(config);
of()
- 從參數建立實例
表示從某些參數或值建立實例:
// 從顏色名稱建立顏色物件
const red = Color.of('red');
const blue = Color.of('blue');
// 從數字建立點座標
const point = Point.of(10, 20);
// 從字串建立使用者
const user = User.of('john', 'doe');
from()
- 從來源轉換建立實例
表示從某種來源或格式轉換建立實例:
// 從 JSON 建立物件
const user = User.from(jsonData);
// 從 URL 建立圖片載入器
const imageLoader = await ImageLoader.from('https://example.com/image.jpg');
// 從檔案建立配置
const config = Config.fromFile('config.json');
getInstance()
- 單例或快取實例
通常用於單例模式或快取實例:
// 全域的設定管理器
const configManager = ConfigManager.getInstance();
// 應用程式狀態管理
const appState = AppState.getInstance();
// 快取管理器
const cacheManager = CacheManager.getInstance();
基本範例
class MyClass {
private constructor(public id: string) {} // 私有建構子
public static create(id: string): MyClass {
// 可以在這裡加入額外邏輯
return new MyClass(id);
}
}
// 使用方式: MyClass.create('abc') 而不是 new MyClass('abc')
靜態工廠模式解決的問題
1. 複雜或非同步的初始化流程
問題:constructor 必須是同步的,因為 constructor 是在類別實例化時自動同步呼叫的函數,無法回傳 Promise 。如果物件建立需要讀取檔案、網路請求、大量計算等非同步或耗時操作,建構子無法直接處理。
class MyClass {
async constructor() { // ❌ 語法錯誤
await someAsyncTask();
}
}
我們很直覺的會想到,如果建構子必須是同步的,那就分開建構與初始化就好,不過總覺得不夠優雅。
class MyClass {
data: string = '';
async init() {
this.data = await fetchData();
}
}
// 使用方式
const instance = new MyClass();
await instance.init();
解決方案:使用 static async create() 靜態工廠方法,此作法可以是 async
的,允許在物件完全準備好後才返回實例。
class MyClass {
private constructor(private data: string) {
// 同步初始化邏輯
}
static async create() {
const result = await fetchData(); // 非同步處理
return new MyClass(result);
}
}
// 使用方式
const instance = await MyClass.create();
2. 強制控制實例的建立
問題:直接使用 new
允許任何人隨意建立物件,難以控制實例的生命週期或狀態。
解決方案:將建構子設為 private
,強制所有實例都必須透過靜態工廠方法建立。
class ImageLoader {
private constructor(private url: string, private element: HTMLImageElement) {} // 私有建構子
public static async create(url: string, element: HTMLImageElement): Promise<ImageLoader> {
// 工廠方法作為「守門員」,確保實例符合特定規則
const instance = new ImageLoader(url, element);
await instance.load();
return instance;
}
private async load(): Promise<void> {
// 載入圖片的邏輯
return new Promise((resolve, reject) => {
this.element.onload = () => resolve();
this.element.onerror = () => reject(new Error('Failed to load image'));
this.element.src = this.url;
});
}
}
3. 返回子類型或不同實作 (多型)
問題:建構子永遠只能返回其自身的實例。
解決方案:靜態工廠方法可以根據輸入參數,返回不同子類別或不同實作的實例。
class Vehicle {
static create(type: 'car' | 'bike', config: any): Vehicle {
switch (type) {
case 'car': return new Car(config);
case 'bike': return new Bike(config);
default: throw new Error('Unknown vehicle type');
}
}
}
4. 實例的快取與重用 (單例、物件池)
問題:每次 new
都會建立新實例,可能造成資源浪費。
解決方案:工廠方法可以在內部維護一個快取,如果實例已存在,就直接返回舊實例。
class ConfigManager {
private static instance: ConfigManager;
public static getInstance(): ConfigManager {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager();
}
return ConfigManager.instance;
}
}
靜態工廠模式的優勢
- 更清晰的意圖:方法名稱比建構子名稱更能表達建立的意圖
- 更好的控制:可以控制實例的建立過程、數量和類型
- 更高的彈性:易於修改建立邏輯,而無需修改客戶端程式碼
- 支援非同步建立:解決了建構子無法處理非同步操作的限制
- 保證實例狀態:確保返回的實例是完全初始化且可用的
實際案例:圖片載入器重構
完整範例 👉 github repo
重構前的問題
// 舊的 ImageLoader 實作
class ImageLoader implements ILoader {
private element: HTMLImageElement | null = null;
private isLoaded: boolean = false;
constructor() {
// 只能同步初始化
}
async load(url: string, element: HTMLImageElement): Promise<ImageLoader> {
// 雙重職責:載入圖片和返回已存在實例
// 物件在一段時間內處於「半初始化」狀態
this.element = element;
this.isLoaded = false;
return new Promise((resolve, reject) => {
element.onload = () => {
this.isLoaded = true;
resolve(this);
};
element.onerror = () => reject(new Error('Failed to load image'));
element.src = url;
});
}
getElement(): HTMLImageElement | null {
return this.element;
}
}
// 使用方式
const loader = new ImageLoader();
await loader.load('https://example.com/image.jpg', imgElement); // 流程不夠直觀
主要問題:
constructor
只能同步初始化,但實際載入圖片是透過async load()
方法load()
方法有雙重職責(載入和返回已存在實例)ILoader
介面與實際的建立流程不符- 客戶端必須先
new ImageLoader()
再await loader.load()
,流程不夠直觀
重構後的解決方案
// 新的 ImageLoader 實作
class ImageLoader implements ILoader {
private constructor(
private element: HTMLImageElement,
private url: string
) {
// 私有建構子,強制透過工廠方法建立
}
public static async create(url: string, element: HTMLImageElement): Promise<ImageLoader> {
const instance = new ImageLoader(element, url);
await instance._loadImage();
return instance; // 保證返回完全載入的實例
}
private async _loadImage(): Promise<void> {
return new Promise((resolve, reject) => {
this.element.onload = () => resolve();
this.element.onerror = () => reject(new Error('Failed to load image'));
this.element.src = this.url;
});
}
public getElement(): HTMLImageElement {
return this.element;
}
public getUrl(): string {
return this.url;
}
}
// 使用方式
const loader = await ImageLoader.create('https://example.com/image.jpg', imgElement); // 程式碼更簡潔、意圖更明確
重構優勢:
- 單一職責:
create
方法專注於建立和初始化 - 狀態保證:返回的實例是完全載入且可用的
- 更清晰的介面:
ILoader
移除了load
方法,使其更符合「實例契約」的職責 - 更好的客戶端體驗:客戶端現在可以直接使用
await ImageLoader.create(...)
總結
靜態工廠模式特別適合處理複雜的物件建立邏輯,它不僅解決了建構子的限制,還提供了更好的封裝和控制能力。
在我們的 ImageLoader
重構案例中,靜態工廠模式幫助我們:
- 解決了非同步初始化的問題
- 提供了更清晰的 API 設計
- 確保了物件狀態的一致性
- 改善了程式碼的可讀性和可維護性
當你遇到複雜的物件建立需求時,不妨考慮使用靜態工廠模式來改善你的程式碼架構。