每次專案長大,總會冒出一堆「這頁要深色主題、那支線要換金流供應商、還要保證所有元件彼此相容」的需求。要是你靠到處 if/else 收線,過沒多久就會被自己埋的條件地雷絆倒。
這篇想用淺白的例子聊聊 Abstract Factory(抽象工廠):它不追求炫技,而是幫你把「同一系列的產品」— 像是按鈕、對話框、開關,或是付款、開發票 — 用一個入口統一生出來,確保風格與協定不會混搭走鐘。全文以 JavaScript 為主,從同步 UI 主題到非同步第三方服務,帶你看設計意圖、使用時機、與其他模式的差別,並附上實作細節、測試思路、常見雷點。希望本篇文章能夠幫助到需要的您。
目錄
{tocify} $title={目錄}
什麼是 Abstract Factory?
定義(精簡版)
Abstract Factory 的核心是:用一個「工廠物件」來產生一組相關(或相依)的產品物件,而不讓客戶端(呼叫端)接觸到產品的具體類別。你選定哪個工廠,就得到同一「產品族(Product Family)」下相容的元件組合。
直覺比喻
把它想成「同品牌、同系列的整套家具」:同一系列的桌椅櫃在尺寸與風格上互相對齊。你只決定買哪個系列(挑工廠),不用自己去配對每個零件(個別產品)。
目標
封裝選擇產品族的邏輯(例如:Light/Dark 主題、Vendor A/Vendor B 供應商)。
確保產品間的一致性與相容性(不會把 Dark 的 Button 搭 Light 的 Dialog)。
降低客戶端對具體類別的依賴,讓切換品牌/主題更容易。
何時需要它:適用情境與反指標
適用情境
需要同時建構多個相互關聯的物件(例如 UI 元件、SDK 子模組、資料儲存與快取對應)。
你有多種替換方案(平台、主題、供應商),而且希望用一個開關切換整組行為。
產品之間存在相容性約束:必須來自同一族系才能保證運作。
反指標(不建議使用)
只需要產生單一物件、且不會形成族系。
替換方案極少或基本不變;導入 Abstract Factory 只會增加結構複雜度。
需求仍在快速探索期,結構尚未穩定。
與其他模式的差異
| 模式 | 核心目的 | 何時用 |
|---|---|---|
| Factory Method | 延遲到子類決定要建構哪個單一產品 | 只有一種產品類型,但實作多樣 |
| Abstract Factory | 提供一個工廠介面,產生一族相關產品 | 需要多個相依物件的整套變體 |
| Builder | 將建立步驟分離,組裝同一產品的不同配置 | 強調一步步組裝同一個複雜物件 |
心法:產品數量 × 關聯度高 → Abstract Factory;單一產品但實作不同 → Factory Method;同一產品的建構流程很複雜 → Builder。
範例一(同步版):UI 主題元件工廠(Light/Dark)
此例示範「產品族」的概念:Button、Dialog、Switch 要來自同一主題才有一致視覺與交互體驗。
1. 設計重點
使用 ES6 class 表達產品與工廠。
以 Symbol 當作家族標記,避免不同主題交叉混用。
抽象基底類別提供預設拋錯,提示實作者覆寫。
2. 程式碼
// === 族系防呆:以 Symbol 標記產品所屬的 family ===
const FAMILY = Symbol('theme-family');
// === 抽象產品介面(以慣例表示,JS 無強制介面) ===
class AbstractButton {
render() { throw new Error('AbstractButton.render() must be implemented'); }
}
class AbstractDialog {
open() { throw new Error('AbstractDialog.open() must be implemented'); }
}
class AbstractSwitch {
toggle() { throw new Error('AbstractSwitch.toggle() must be implemented'); }
}
// === 抽象工廠(基底類) ===
class AbstractThemeFactory {
createButton() { throw new Error('createButton() must be implemented'); }
createDialog() { throw new Error('createDialog() must be implemented'); }
createSwitch() { throw new Error('createSwitch() must be implemented'); }
}
// === Light 族系 ===
class LightButton extends AbstractButton {
constructor() {
super();
this[FAMILY] = 'light';
}
render() { return `<button style="background:#fff;color:#222;border:1px solid #ddd">Light Button</button>`; }
}
class LightDialog extends AbstractDialog {
constructor() {
super();
this[FAMILY] = 'light';
}
open() { console.log('[LightDialog] open with soft shadow'); }
}
class LightSwitch extends AbstractSwitch {
constructor() {
super();
this[FAMILY] = 'light';
this.on = false;
}
toggle() {
this.on = !this.on;
console.log(`[LightSwitch] ${this.on ? 'ON' : 'OFF'}`);
}
}
class LightThemeFactory extends AbstractThemeFactory {
createButton() { return new LightButton(); }
createDialog() { return new LightDialog(); }
createSwitch() { return new LightSwitch(); }
}
// === Dark 族系 ===
class DarkButton extends AbstractButton {
constructor() {
super();
this[FAMILY] = 'dark';
}
render() { return `<button style="background:#222;color:#f5f5f5;border:1px solid #444">Dark Button</button>`; }
}
class DarkDialog extends AbstractDialog {
constructor() {
super();
this[FAMILY] = 'dark';
}
open() { console.log('[DarkDialog] open with high-contrast backdrop'); }
}
class DarkSwitch extends AbstractSwitch {
constructor() {
super();
this[FAMILY] = 'dark';
this.on = false;
}
toggle() {
this.on = !this.on;
console.log(`[DarkSwitch] ${this.on ? 'ON' : 'OFF'}`);
}
}
class DarkThemeFactory extends AbstractThemeFactory {
createButton() { return new DarkButton(); }
createDialog() { return new DarkDialog(); }
createSwitch() { return new DarkSwitch(); }
}
// === 工廠選擇器(註冊表) ===
const themeRegistry = new Map([
['light', () => new LightThemeFactory()],
['dark', () => new DarkThemeFactory()],
]);
function getThemeFactory(name = 'light') {
const make = themeRegistry.get(name);
if (!make) throw new Error(`Unknown theme: ${name}`);
return make();
}
// === 族系一致性檢查(可在開發/測試時啟用) ===
function assertSameFamily(...components) {
const fam = components[0]?.[FAMILY];
if (!fam) throw new Error('Component missing FAMILY tag');
const ok = components.every(c => c[FAMILY] === fam);
if (!ok) throw new Error('Mixed product families detected');
}
// === 客戶端使用 ===
const factory = getThemeFactory('dark'); // 換成 'light' 即全站換主題
const btn = factory.createButton();
const dlg = factory.createDialog();
const sw = factory.createSwitch();
assertSameFamily(btn, dlg, sw);
console.log(btn.render());
dlg.open();
sw.toggle();
3. 重點回顧
客戶端只面對工廠與抽象行為,不需要知道具體類別名稱。
切換產品族=更換工廠,零散 if/switch 被收斂到一個入口。
assertSameFamily 是實務上常用的小技巧,避免誤混產品族。
範例二(非同步版):第三方服務供應商工廠(付款+帳單)
實務上,建立產品時常常需要非同步資源(Token、連線、金鑰驗證)。以下示範 Abstract Factory 與 async/await 的結合。
1. 設計重點
產品需要外部設定(如 API Key),在工廠內完成初始化。
工廠方法回傳 Promise,呼叫端以 await 取得已就緒的產品。
以「付款閘道(PaymentGateway)」與「帳單服務(InvoiceService)」為同族產品。
2. 程式碼
class AbstractPaymentGateway {
async charge(cents, customerId) { throw new Error('implement'); }
}
class AbstractInvoiceService {
async createInvoice(payload) { throw new Error('implement'); }
}
class AbstractVendorFactory {
async createGateway() { throw new Error('implement'); }
async createInvoiceService() { throw new Error('implement'); }
}
// === Vendor A ===
class VendorAGateway extends AbstractPaymentGateway {
constructor(api) { super(); this.api = api; this.vendor = 'A'; }
async charge(cents, customerId) {
return this.api.post('/charge', { amount: cents, customerId });
}
}
class VendorAInvoiceService extends AbstractInvoiceService {
constructor(api) { super(); this.api = api; this.vendor = 'A'; }
async createInvoice(payload) {
return this.api.post('/invoice', payload);
}
}
class VendorAFactory extends AbstractVendorFactory {
constructor({ apiKey }) { super(); this.apiKey = apiKey; }
async createGateway() {
const api = await connectA(this.apiKey);
return new VendorAGateway(api);
}
async createInvoiceService() {
const api = await connectA(this.apiKey);
return new VendorAInvoiceService(api);
}
}
// === Vendor B(另一套協定) ===
class VendorBGateway extends AbstractPaymentGateway {
constructor(client) { super(); this.client = client; this.vendor = 'B'; }
async charge(cents, customerId) {
return this.client.send('PAY', { cents, customerId });
}
}
class VendorBInvoiceService extends AbstractInvoiceService {
constructor(client) { super(); this.client = client; this.vendor = 'B'; }
async createInvoice(payload) {
return this.client.send('INVOICE', payload);
}
}
class VendorBFactory extends AbstractVendorFactory {
constructor({ token }) { super(); this.token = token; }
async createGateway() {
const client = await connectB(this.token);
return new VendorBGateway(client);
}
async createInvoiceService() {
const client = await connectB(this.token);
return new VendorBInvoiceService(client);
}
}
// === 模擬連線 ===
async function connectA(apiKey) {
await new Promise(r => setTimeout(r, 10));
return { post: (path, data) => ({ vendor: 'A', path, data, ok: true }) };
}
async function connectB(token) {
await new Promise(r => setTimeout(r, 10));
return { send: (cmd, data) => ({ vendor: 'B', cmd, data, ok: true }) };
}
// === 工廠選擇 ===
function getVendorFactory(kind, credentials) {
switch (kind) {
case 'A': return new VendorAFactory(credentials);
case 'B': return new VendorBFactory(credentials);
default: throw new Error(`Unknown vendor: ${kind}`);
}
}
// === 客戶端使用 ===
(async () => {
const factory = getVendorFactory('B', { token: 'secret' });
const gateway = await factory.createGateway();
const invoice = await factory.createInvoiceService();
const payRes = await gateway.charge(9900, 'cus_123');
const invRes = await invoice.createInvoice({ customerId: 'cus_123', items: [{ name: 'Pro Plan', cents: 9900 }] });
console.log(payRes, invRes);
})();
3. 重點回顧
非同步工廠是 Web/Node.js 常態,請務必將 async/await 納入設計。
把供應商特性封裝在工廠內,讓上層只關心抽象行為(charge、createInvoice)。
進階技巧:註冊表、函式工廠、族系防呆
1. 註冊表(Registry)
用 Map 保存工廠建構器,方便在組態或插件機制下動態擴充:
const registry = new Map();
function registerFactory(key, maker) { registry.set(key, maker); }
function resolveFactory(key) {
const maker = registry.get(key);
if (!maker) throw new Error(`Factory not found: ${key}`);
return maker();
}
2. 函式工廠(無 class 版)
在偏向函數式的程式碼庫,可用閉包與物件工廠達成同等效果:
const FAMILY = Symbol('family');
function createThemeFactory(theme) {
const fam = theme;
return {
createButton() {
return {
[FAMILY]: fam,
render() { return `<button>${fam} button</button>`; }
};
},
createDialog() {
return {
[FAMILY]: fam,
open() { console.log(`[${fam}] dialog open`); }
};
}
};
}
const factory = createThemeFactory('light');
const btn = factory.createButton();
const dlg = factory.createDialog();
3. 族系防呆(品牌化檢查)
前述的 Symbol 標記能有效防止誤混產品族,也利於測試時做靜態檢查:
function assertFamilyMatch(a, b) {
if (a?.[FAMILY] !== b?.[FAMILY]) {
throw new Error('Family mismatch');
}
}
測試與維護:如何寫測試、如何演進
1. 單元測試建議
行為導向:測試抽象能力(例如 render() 是否回傳正確結構),避免綁死具體類別名稱。
族系一致性:新增測試檢驗 FAMILY 是否一致。
非同步工廠:用 await 驗證產物是否就緒,並對 API 呼叫做 mock。
// 假設使用 Node.js assert
import assert from 'node:assert/strict';
const factory = getThemeFactory('light');
const a = factory.createButton();
const b = factory.createDialog();
assert.equal(a[Symbol.for('theme-family')], undefined); // 範例:防止濫用 Symbol.for
// 建議使用 assertSameFamily(見前文)
小提醒:測試不要「嘗試去知道」私有欄位;盡量靠公開行為(方法輸出、副作用)判斷。
2. 維護與演進
新增新族系(例如 HighContrast)時,直接新增一個工廠類別與相對應產品;客戶端不需修改。
若產品清單擴增(新增 createTooltip),抽象工廠介面也要同步擴充,避免族系實作不完整。
把「選擇哪個工廠」的邏輯收斂到單點(環境變數、設定檔、A/B 測試參數)。
常見錯誤與雷點
1. 過度工程
症狀:明明只有一個產品、兩個簡單實作,卻硬上 Abstract Factory。
建議:先用 Factory Method 或單純函式;等「產品族」需求明朗再升級。
2. 把具體類別外漏到客戶端
反例:客戶端寫 new DarkButton()。
後果:切換族系時要改遍全站。請改為呼叫 factory.createButton()。
3. 條件蔓延(if/switch everywhere)
反例:到處 if (theme === 'dark') {...} else {...}。
解法:把條件收斂到工廠選擇器或註冊表。
4. 忽略非同步建立
反例:建立產品需要 token/連線,卻用同步介面回傳半成品。
解法:工廠方法回傳 Promise,呼叫端 await。
5. 產品族混搭
症狀:Dark Button 配 Light Dialog。
解法:導入 FAMILY 標記與 assertSameFamily。可在開發模式啟用。
6. 測試難寫、難換 mock
反例:客戶端內部直接 new VendorAGateway()。
解法:注入 factory 作為依賴,測試時用假工廠替換。
7. 把工廠變成全域單例,導致狀態污染
症狀:測試之間互相干擾。
建議:讓工廠無狀態或以建構參數注入設定;測試結束記得重置。
8. 跨平台差異未隔離(Browser vs Node)
症狀:某族系依賴 DOM API,另一族系在 Node 不可用。
建議:在工廠內部隔離平台差異,或拆成不同發佈目標。
9. 未定義失敗策略
症狀:第三方連線失敗、產品建立中斷。
建議:工廠方法清楚回傳錯誤,並提供重試/降級策略。
10. 版本協議不一致
症狀:同族產品升版後介面微調,舊客戶端壞掉。
建議:以抽象介面版本化(v1/v2),並在工廠層做轉接。
總結
Abstract Factory 的價值在於:把「產品族的切換」與「產品間的一致性」抽離出來,形成可替換、可測試、可擴充的結構。
在 JavaScript/Node.js 的世界,請務必考慮非同步建立,把 async/await 視為工廠介面的一等公民。
搭配註冊表、族系防呆標記與依賴注入,可有效降低條件蔓延與耦合。
初期可從簡(函式工廠、單一族系),待需求成熟再升級為正式的 Abstract Factory。
附錄:極簡對照速記
我有多個相關物件要一起換? → Abstract Factory
我只要換一個物件的實作? → Factory Method
我要一步步組裝同一個複雜物件? → Builder
JS 要不要考慮 async? → 幾乎都要
如何防止混搭? → Symbol 族系標記+測試
延伸閱讀推薦:
javaScript設計模式 : Factory Method (工廠方法)
javaScript設計模式 : Builder(建造者模式)
javaScript設計模式 : Prototype(原型)
javaScript設計模式 : Singleton (單例模式)
javaScript設計模式 : Adapter(轉接器模式)
javaScript設計模式 : Bridge( 橋接模式 )
javaScript設計模式 : Composite(組合模式)
javaScript設計模式 : Decorator(裝飾者)
javaScript設計模式 : Flyweight(享元模式)
javaScript設計模式 : Chain of Responsibility(責任鏈)
javaScript設計模式 : Command Pattern(命令模式)
javaScript設計模式 : Interpreter(直譯器)
javaScript設計模式 : Iterator(迭代器)
javaScript設計模式 : Mediator(仲裁者)
javaScript設計模式 : Observer( 觀察者 )
javaScript設計模式 : Strategy(策略模式)
