Design Pattern: 工廠方法模式(Factory Method Pattern)
前言
在學習設計模式的時候,我們常常會遇到 「工廠模式」 這個詞,而最常被介紹的就是 「簡單工廠模式 (Simple Factory Pattern)」。如果你已經讀過上一篇關於簡單工廠模式的文章,應該已經對它的概念有一定的理解。這裡我們先快速回顧一下簡單工廠模式的核心概念:
簡單工廠模式 將「物件的創建」封裝在一個工廠類別中,客戶端無需直接使用
new關鍵字去實例化物件,而是透過工廠方法來獲得所需的物件。
這樣的設計帶來了兩個好處:
- 降低耦合度:客戶端不需要了解具體產品類別的細節,只需向工廠請求物件即可。
- 集中管理創建邏輯:當產品的創建過程發生改變時,只需要修改工廠內部的邏輯,而無需動及客戶端程式碼。
然而,儘管簡單工廠模式成功解決了物件創建的耦合問題,但在面對產品種類不斷增加的情況下,其內部往往會出現過多的條件分支(例如 if-else 或 switch-case),導致程式碼逐漸複雜,甚至變成難以維護的「條件分支迷宮」。這不僅違反了 開放-封閉原則 (OCP, Open-Closed Principle),也讓每次新增產品時,都必須修 改已存在的工廠類別,增加了程式出錯的風險。
來看看這個例子:
class Factory {
static createProduct(type: string): Product {
if (type === "A") {
return new ProductA();
} else if (type === "B") {
return new ProductB();
} else {
throw new Error("Invalid product type");
}
}
}
如果今天有個新產品 ProductC 要加入,我們就必須修改 Factory 類別,把 if-else 或 switch-case 再加上 ProductC 的處理邏輯。這樣一來每次的修改都可能影響既有程式碼,增加維護的複雜度與風險。
那麼,有沒有不需要動到既有程式碼,卻又能靈活擴充新產品的方法呢?這篇文章要介紹的工廠方法 模式(Factory Method Pattern) 便是解決這個問題的方案!
動機與問題背景
當簡單工廠不再簡單時
剛剛我們提到,簡單工廠模式在系統規模小的時候還挺好用,但隨著系統變複雜,簡單工廠也會跟著變得複雜起來。現在,想像一下這樣一個情境:你正在開發一個 物流管理系統,一開始系統只支援 卡車 (Truck) 運輸,所以大部分程式碼都是圍繞著 Truck 類別運行:
class Truck {
deliver() {
console.log("貨物已透過卡車送達");
}
}
// 簡單工廠負責創建卡車
class LogisticsFactory {
static createTransport(): Truck {
return new Truck();
}
}
// 客戶端使用工廠來獲取運輸工具
const transport = LogisticsFactory.createTransport();
transport.deliver();
這套系統運行得很好,直到有一天,海運公司來找你合作,希望你的系統能夠支援 船運 (Ship)。
這時候你該怎麼辦?
最直覺的做法就是 直接修改 LogisticsFactory,加入對 Ship 的處理:
class Ship {
deliver() {
console.log("貨物已透過船運送達");
}
}
class LogisticsFactory {
static createTransport(type: string) {
if (type === "truck") {
return new Truck();
} else if (type === "ship") {
return new Ship();
} else {
throw new Error("未知的運輸方式");
}
}
}
// 使用時要傳入運輸方式
const transport = LogisticsFactory.createTransport("ship");
transport.deliver(); // 貨物已透過船運送達
這樣雖然暫時解決了問題,但你有沒有發現其實我們修改了 LogisticsFactory,這就違反了 開放-封閉原則 (OCP)。
🚨 簡單工廠的 if-else 地獄
隨著系統越來越受歡迎,各種運輸需求接踵而來,老闆還希望我們支援更多運輸方式,例如:
- 🚢 貨輪 (Cargo Ship)
- 🚆 火車 (Train)
- ✈️ 飛機 (Airplane)
這樣一來,我們的 LogisticsFactory 變成了這樣的災難現場:
class LogisticsFactory {
static createTransport(type: string) {
if (type === "truck") {
return new Truck();
} else if (type === "ship") {
return new Ship();
} else if (type === "cargo-ship") {
return new CargoShip();
} else if (type === "train") {
return new Train();
} else if (type === "airplane") {
return new Airplane();
} else {
throw new Error("未知的運輸方式");
}
}
}
每次有新運輸方式,就得修改 LogisticsFactory,程式碼就越寫越亂,這讓程式變得愈來愈不穩定。如果我們繼續這樣下去,未來每次擴充功能都會變成一場災難。
所以,我們迫切需要一種更靈活、更不容易出錯的方式來處理這些問題,而這正是工廠方法模式 (Factory Method Pattern) 要來解決的問題。
工廠方法模式的解決方案
當我們需要更靈活的工廠
之前說到簡單工廠模式會變成 if-else 的大亂鬥,當系統裡的運輸工具越來越多時,我們不得不每次都去修改那個大工廠 (LogisticsFactory),這不但讓程式碼變得難以維護,也完全違背了「開放-封閉原則 (OCP)」,也就是說,每次加新功能都得動現有的程式碼。
那我們該怎麼辦呢?
簡單來說,我們需要把「運輸工具的創建邏輯」從那個大工廠中拆出來,讓每種運輸方式都有自己專屬的工廠來管理。
這樣一來,新增運輸方式就只需要新增一個工廠,而不用改動現有的程式碼。 這正是工廠方法模式 (Factory Method Pattern) 要解決的問題!
將工廠抽象化
工廠方法模式的精髓在於
「讓每種產品都有自己的工廠」,而不是用一個統一的大工廠來處理所有事情。
也就是說,我們會把工廠的概念抽象成一個抽象類別或介面,然後由各自的具體工廠來決定要創建哪一種運輸工具。這樣做有幾個好處:
- 擴充性提升:未來有新運輸方式時,你只需新增一個新的工廠類別,現有的程式碼都不用動。
- 降低耦合性:客戶端只依賴抽象的工廠,不需要關心具體的運輸工具細節,這樣程式碼的穩定性也會更好。
接下來,我們就用物流管理系統的例子,來看看如何從簡單工廠模式重構成工廠方法模式。
重構簡單工廠模式 → 工廠方法模式
我們來看看如何將物流管理系統從簡單工廠模式重構成工廠方法模式:
1️⃣ 定義抽象產品類別
首先,所有的運輸方式都應該符合統一的介面 Transport,這樣無論是 Truck 還是 Ship,客戶端都可以統一使用它們,而不需要知道具體類別。
// 抽象產品類別:所有運輸工具都要實作 `deliver()` 方法
abstract class Transport {
abstract deliver(): void;
}
2️⃣ 具體產品類別
我們將不同的運輸方式各自定義成獨立的類別,並讓它們繼承 Transport。
// 具體產品類別:卡車運輸
class Truck extends Transport {
deliver() {
console.log("🚚 貨物已透過卡車送達");
}
}
// 具體產品類別:船運
class Ship extends Transport {
deliver() {
console.log("