Factory Pattern:物件建立的生產線

·11 min

此為《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()
感謝你讀到這裡!如果我的分享讓你有所共鳴或啟發,歡迎請我喝杯珍奶 🧋,給我一點繼續分享的能量: https://anna.bobaboba.me