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("🚢 貨物已透過船運送達");
}
}
這樣一來,無論有多少種運輸方式,它們都符合 Transport
這個介面,讓系統更加一致。
3️⃣ 定義抽象工廠
我們現在不再用一個大 LogisticsFactory
來決定要回傳哪 種 Transport
,而是將「創建產品的行為」推給不同的子工廠。
// 抽象工廠類別:定義 `createTransport()` 方法
abstract class Logistics {
abstract createTransport(): Transport;
}
這個 Logistics
類別本身並不會真正創建 Truck
或 Ship
,它只是定義了一個 createTransport()
方法,讓具體的子工廠來決定要創建什麼樣的運輸方式。
4️⃣ 具體工廠類別
現在,我們讓每個運輸方式都有自己的工廠,並且各自實作 createTransport()
方法。
// 具體工廠類別:卡車物流
class RoadLogistics extends Logistics {
createTransport(): Transport {
return new Truck(); // 回傳卡車物件
}
}
// 具體工廠類別:船運物流
class SeaLogistics extends Logistics {
createTransport(): Transport {
return new Ship(); // 回傳船運物件
}
}
這樣的設計有什麼好處呢?
- 當有新運輸方式(例如 Airplane)時,只需要新增一個新的工廠,而不需要修改任何現有的程式碼!
- 符合開放-封閉原則 (OCP),讓系統能夠持續擴展而不影響既有程式碼。
5️⃣ 客戶端使用工廠方法模式
現在,客戶端不需要關心具體的運輸方式是什麼,它只要透過 Logistics
來獲取運輸工具,並執行 deliver()
方法即可。
// 定義一個函式來執行配送
function startDelivery(logistics: Logistics) {
const transport = logistics.createTransport();
transport.deliver();
}
// 使用不同的工廠來創建運輸工具
startDelivery(new RoadLogistics()); // 🚚 貨物已透過卡車送達
startDelivery(new SeaLogistics()); // 🚢 貨物已透過船運送達
這樣的設計,讓客戶端只需要依賴 Logistics
,而不需要知道 Truck
或 Ship
的存在,大幅降低了耦合性。
工廠方法模式的核心概念
設計概念
從前一章的例子可以看到,工廠方法模式的奧妙之處就在於:
把「誰來創建物件」這個責任轉移到具體的子工廠身上。
簡單來說,工廠方法模式並沒有改變物件創建的本質——我們還是會用 new
來建立物件,只不過這個 new
操作只會出現在「具體工廠類別」裡,而客戶端就不需要直接看到或操作它。
有些人可能會問:「這樣看起來不就是把 new
放到另一個地方而已,有什麼好處?」
答案就在於:
不同的子工廠(例如
RoadLogistics
或SeaLogistics
)可以各自定制自己的創建邏輯,而不會互相干擾。
這樣一來,當我們新增運輸方式時,只要新增一個新的工廠,而不必動到現有的程式碼,客戶端也只需依賴統一的 Transport
介面,無需知道背後實際使用的是哪一個具體產品。
總結一下,工廠方法模式有以下幾個重點好處:
- 擴充方便:新增運輸方式只需新增一個新工廠,現有的 if-else 邏輯不用再煩惱。
- 降低耦合性:客戶端只依賴抽象的
Transport
,不 必關心是Truck
還是Ship
。
接下來,我們整理一下工廠方法模式的設計流程:
-
定義抽象工廠介面 (Creator)
建立一個包含抽象工廠方法createTransport
的介面或抽象類別,這個方法負責「創建產品」,但不決定具體產生哪個產品。 -
實作具體工廠 (Concrete Creator)
由各個具體工廠子類別來實作createTransport
,決定實際要產生哪一種產品(例如Truck
或Ship
)。 -
統一產品介面 (Product)
所有具體的產品都必須實作同一個介面或繼承同一個抽象類別(例如Transport
),確保客戶端使用時是一致的。 -
客戶端只依賴抽象
客戶端透過抽象工廠取得產品物件,而不直接new
具體產品。這樣,新增產品時,客戶端程式碼都不用改動。
設計原則
工廠方法模式能帶來這些好處,主要是因為它符合了兩個重要的設計原則:
-
開放-封閉原則 (OCP)
程式應該對「擴展」開放,對「修改」封閉。
換句話說,當你需要新增功能(例如新增一種運輸方式),你只需要新增新類別,不必修改原有程式碼。舉個例子,若要加入 飛機 (Airplane),在簡單工廠模式下,你得修改
LogisticsFactory
;但在工廠方法模式下,只要新增一個AirLogistics
就搞定了! -
依賴反轉原則 (DIP)
高層模組不應該依賴低層模組,而應該依賴於抽象。
也就是說,客戶端只需要依賴抽象的Transport
,而不需要知道具體是Truck
或Ship
。
這樣當產品種類增加時,客戶端不會因為直接依賴具體類別而受到影響。舉例來說,如果
Logistics
裡直接用new Truck()
,那它就依賴了具體的Truck
類別;而使用工廠方法模式後,Logistics
只會依賴抽象的Transport
,無論新增什麼運輸方式,它的邏輯都不會被改到。
模式結構
為了讓大家更清楚地了解工廠方法模式的整體架構,我們用 UML 類別圖來做個視覺化的說明。
角色
-
抽象產品 (Product)
定義所有產品的共同行為,基本上就像是一個規範,規定每個產品都該會做什麼。這可以是一個介面或者抽象類別,讓所有具體的產品都依照這個規格來實作。 -
具體產品 (ConcreteProduct)
真正實作抽象產品中定義的行為。不同的具體產品會有各自獨特的實作方式,就像不同品牌的手機雖然基本功能一樣,但各有特色一樣。 -
抽象工廠 (Creator)
這個角色 定義了一個「工廠方法」(在這裡叫作createProduct
),專門用來生產產品。不過,這個抽象工廠本身並不會真的去生產產品,而是將這個責任交給它的子類別。可以把它想成是一個統一的規範。 -
具體工廠 (ConcreteCreator)
實際負責生產產品的角色。每個具體工廠只負責生產一種產品,這樣客戶端在使用時就不會直接用new
來建立產品,而是透過這些具體工廠來獲得產品。就像你去咖啡店點一杯咖啡,你不需要知道咖啡豆怎麼煮出來的,只要點對店家就好。
Pseudocode 與實作範例
在前一章我們透過物流管理系統的案例了解了工廠方法模式的設計原則與解決方案,這一章我們將用另一個例子來加深大家的理解:一個 通知系統 (Notification System)。這個系統支援多種通知方式,例如:
- 📧 Email 通知
- 📱 SMS 簡訊通知
- 🔔 Push 通知
每種通知方式都有不同的傳送邏輯,比如 Email 需要 SMTP 伺服器、SMS 需要電信商 API,而 Push 則需要連接推播服務。目標是設計出一個架構,讓系統可以輕鬆擴展新的通知方式,而不用動到現有程式碼。
我們希望設計一個靈活的架構,讓系統能夠根據需求擴展新的通知方式,而不需要修改現有的程式碼。