Factory Pattern:物件建立的生產線
此為《Learning JavaScript Design Patterns, 2nd Edition》學習筆記,對應第 7 章(JavaScript Design Patterns - The Factory Pattern)
模式用途
- 分類:建立型
- 解決什麼問題:如何提供一個統一入口來建立不同類型的物件
從通知系統開始
在前端開發裡,通知幾乎是標配:成功提示、錯誤警告、確認對話框⋯⋯雖然都叫「通知」,但依情境不同,可能會用 Toast、Banner 或 Modal 呈現。
剛開始最直覺的做法,是在各處直接 new Toast()
、new Banner()
。但專案一大,維護就會開始吃力。Factory Pattern 想處理的,就是這件事:把「怎麼建立物件」集中管理,讓程式碼更好維護,也更好擴充。
接下來會用通知系統作為例子,從最原始的寫法出發,一步步走到完整的 Factory Pattern。
一步步演進
1. 零散的 new
與分支
一開始最直覺的方式就是在各處直接 new
,再加 if/else
判斷。短期好寫,長期難維護。
// 零散的分支與 new
if (type === 'error') {
new Modal({ title: '錯誤', content: msg }).show()
}
else if (inline) {
new Banner({ content: msg }).show()
}
else {
new Toast({ content: msg, duration: 3000 }).show()
}
痛點:
new
散落:預設(duration
、樣式)改一次要改多處- 分支分散:新增一種通知就再加一個
if/else
- 細節外漏:呼叫端到處帶樣式、錯誤處理,難保一致
2. 把建立邏輯集中起來
先把 new
和分支收進同一個入口。呼叫端只描述「要哪一種通知」。
export function createNotifier(kind, opts = {}) {
switch (kind) {
case 'toast': return new Toast(opts)
case 'banner': return new Banner(opts)
case 'modal': return new Modal(opts)
default: throw new Error(`Unknown notifier: ${kind}`)
}
}
用法:
createNotifier('toast', { content: msg, duration: 3000 }).show()
這一步就把重複設定與分支收斂到一個地方,之後要改預設值也只需要改這裡。這一步就是基本的 Factory Pattern:提供統一入口來建立不同類型的物件。
什麼是 API contract 與 duck typing?
書中在 Factory Pattern 使用時機的段落內,提到了兩個我第一次聽到的術語:API contract(API 契約)和 duck typing。一開始我也不太懂,後來用通知系統的例子來理解就清楚了。
API Contract 簡單說就是「大家都要遵守的規則」。在通知系統的例子中,這個規則就是:
// 不管是哪種通知,都要有顯示的方法
createNotifier('toast', { content: msg }).show()
createNotifier('banner', { content: msg }).show()
createNotifier('modal', { content: msg }).show()
所有從工廠出來的通知物件,都要有「顯示自己」的能力,就滿足了契約。
Duck typing 這個名字很有趣,來自一句話:「如果它走路像鴨子、叫聲像鴨子,那它就是鴨子」🦆。用在程式上就是:
// 不管是什麼類型,只要有 show() 方法就行
function displayNotification(type, content) {
const notifier = createNotifier(type, { content })
notifier.show() // 只要能 show(),就當作它是「可顯示的通知」
}
這樣不用糾結物件的具體類型,只要它「能做我們需要的事」就夠了。
3. 讓工廠可以擴充
當通知種類變多(Snackbar、Message⋯⋯),但不想每次都改 switch
。改成用「動態註冊表」,新增種類用註冊,不動工廠函式的邏輯。
const registry = {}
export function registerNotifier(kind, factoryFn) {
registry[kind] = factoryFn
}
export function createNotifier(kind, opts = {}) {
const fn = registry[kind]
if (!fn)
throw new Error(`Unknown: ${kind}`)
return fn(opts)
}
使用:
registerNotifier('snackbar', opts => new Snackbar(opts))
createNotifier('snackbar', { content: '已儲存' }).show()
這樣新增不會碰到核心檔案,維護風險小很多。
4. 連選擇邏輯也一起管
既然建立集中,連「要選哪一種通知」也可以進一步一起集中。呼叫端只描述情境,至於要用誰,交給入口 notify(...)
決定。
export function notify({ type, inline, msg }) {
if (type === 'error') {
return createNotifier('modal', { title: '錯誤', content: msg }).show()
}
const kind = inline ? 'banner' : 'toast'
return createNotifier(kind, { content: msg, duration: 3000 }).show()
}
之後規則變動(例如錯誤訊息改成跳出 Toast,而不是 Modal),只要改這裡就好。
5. 當需要整組一致時
先看一個對比例子,感受一下「整組一致」的差別:
// ❌ 沒有統一規範:風格不一致
createToast({ theme: 'light', density: 'compact' })
createModal({ theme: 'dark', density: 'comfortable' })
createBanner({ theme: 'light', density: 'comfortable' })
// 結果:深淺主題不一致、排版不一,使用者體驗不好
// ✅ 有統一規範:風格一致
const kit = createNotificationFactory('dark-comfortable')
kit.createToast() // 都是深色主題與舒適排版
kit.createModal() // 都是深色主題與舒適排版
kit.createBanner() // 都是深色主題與舒適排版
// 結果:整組同步,使用者體驗一致
而這是 Abstract Factory 要解決的問題:當需要一整組相關且一致的物件時,如何確保它們能統一管理、統一切換。
如何定義物件可以視為是一組的?
書中這段話點出了 Abstract Factory 的核心:
…Abstract Factory pattern, which aims to encapsulate a group of individual factories with a common goal.
我讀到這一句時,卡在「究竟是按照什麼標準來定義這些物件是一組的?」這個問題很久,後來才理解到判斷標準其實就是「是否有共同目的」。只要這些工廠有相同目的,就可以把它們視為一組。
如果用上面的實際例子來理解的話:
- 個別的工廠(individual factories):
createToast()
、createModal()
、createBanner()
這些各自負責建立不同物件的方法 - 共同目的(common goal):確保這三種通知都遵循同一套設計規範(深色主題與舒適排版)
關於 common goal 的翻譯把 common goal 譯作「共同目的」,而非「共同目標」。是因為用「目的」來理解程式碼對我個人來說更直觀。
完整實作
以下是完整的 Abstract Factory 實作:
export class LightCompactFactory {
createToast() { return new Toast({ theme: 'light', density: 'compact' }) }
createBanner() { return new Banner({ theme: 'light', density: 'compact' }) }
createModal() { return new Modal({ theme: 'light', density: 'compact' }) }
}
export class DarkComfortableFactory {
createToast() { return new Toast({ theme: 'dark', density: 'comfortable' }) }
createBanner() { return new Banner({ theme: 'dark', density: 'comfortable' }) }
createModal() { return new Modal({ theme: 'dark', density: 'comfortable' }) }
}
export function createNotificationFactory(key) {
switch (key) {
case 'light-compact': return new LightCompactFactory()
case 'dark-comfortable': return new DarkComfortableFactory()
}
}
使用:
const kit = createNotificationFactory('dark-comfortable')
kit.createToast().show()
kit.createModal().show() // 同風格,不混搭
重點整理:五步學習卡
1) 問題來源
建立多種相似物件時:
- new 與分支四散:建立邏輯散落各處,預設重複
- 擴充困難:新增品項要改多處
switch/if-else
- 風格混搭:一組相關產品缺乏統一規範
👉 需要集中建立邏輯,並在必要時確保整組一致。
2) 白話描述
Factory Pattern 就像「物件的生產線」:
- 有一個統一入口,告訴它「我要哪種產品」就能拿到對的物件
- 可以註冊新的生產方式,不用改動原本的生產線核心
- 需要整組風格一致時,選一組有共同目的的工廠,所有產品就會同步
3) 專業術語
在 JavaScript 裡,Factory Pattern 透過函式來集中物件建立邏輯。從基本的工廠函式,到可擴充的登錄式工廠,再到 Abstract Factory 的一組共同目的工廠管理,都是為了讓物件建立更有組織、更好維護。
4) 視覺化
5) 程式碼與適用情境
適用情境
- 專案中存在多種「相似但不同」的產品/物件(多變體、多平台)
- 希望統一建立邏輯與預設,集中維護(可測、可控)
- 一組相關產品需要一致規範,且能一次切換(主題/平台)
不適用情境
- 物件種類少且變化不大,直接寫即可
- 沒有共同規範,不需要一致性或同步切換
- 建立邏輯極簡,抽出工廠只會增加複雜度
集中建立
export function createNotifier(kind, opts = {}) {
switch (kind) {
case 'toast': return new Toast(opts)
case 'banner': return new Banner(opts)
case 'modal': return new Modal(opts)
default: throw new Error(`Unknown: ${kind}`)
}
}
登錄式
const registry = {}
export const register = (kind, factory) => (registry[kind] = factory)
export function create(kind, opts = {}) {
const factory = registry[kind]
if (!factory)
throw new Error(`Unknown: ${kind}`)
return factory(opts)
}
抽象工廠
export class LightCompactFactory {
createToast() { return new Toast({ theme: 'light', density: 'compact' }) }
createBanner() { return new Banner({ theme: 'light', density: 'compact' }) }
createModal() { return new Modal({ theme: 'light', density: 'compact' }) }
}
export class DarkComfortableFactory {
createToast() { return new Toast({ theme: 'dark', density: 'comfortable' }) }
createBanner() { return new Banner({ theme: 'dark', density: 'comfortable' }) }
createModal() { return new Modal({ theme: 'dark', density: 'comfortable' }) }
}
export function createNotificationFactory(key) {
switch (key) {
case 'light-compact': return new LightCompactFactory()
case 'dark-comfortable': return new DarkComfortableFactory()
}
}
const kit = createNotificationFactory('dark-comfortable')
kit.createToast().show()