你應該也遇過這種場景:同樣是「發通知」,有時要走 Email,有時改成 SMS,再來產品說想加 Push。大家先用個 switch 撐著,過兩週就變巨無霸。每多一個渠道,測試、例外、重試邏輯都得重寫一次,風險隨之放大。其實不必這麼累。工廠方法的精神是把「選誰上場」變成一個可替換、可擴充的鈕,呼叫端只看同一套介面,至於背後派誰上陣、怎麼初始化、要不要快取,通通由工廠負責。
本文不賣關子,直接用 JS 帶你搭一套好用的工廠:從最小介面開始,逐步加入非同步、裝飾器、故障轉移,最後列出常見坑給你繞開。目標是務實:少改舊碼、多加變體,維護起來不心驚。希望本篇文章能夠幫助到需要的您。
目錄
{tocify} $title={目錄}
為什麼你會需要工廠方法?
當「物件如何被建立」這件事開始變複雜——例如依環境(Web/Node)、平台(iOS/Android PWA)、配置(dev/prod)、或策略(快取/安全/壓縮)而有所不同時,把 “建立物件” 的細節抽離到專門的地方,能讓程式碼更乾淨、可測、可擴充。
Factory Method(工廠方法)就是為此而生:把「建立哪一種產品」的決策,延遲到子類別或組態,客端只依賴抽象介面,不用知道具體類別名稱與 new 的細節。
先釐清名詞:Factory Method vs. 簡單工廠 vs. 抽象工廠
三者常被混用,但定位不同:
簡單工廠(Simple Factory)
一個函式或物件,接參數回傳對應的實例:create('csv') -> CsvExporter。
易懂,但決策通常集中在一個 switch/case 中,不利延展。
工廠方法(Factory Method)
把「建立產品」的步驟定義為一個可被覆寫的方法。
父類別(Creator)提供流程骨架,子類別覆寫 createXxx() 來決定要產哪個實作。
優點:開放/封閉(新增子類別即可擴充);
缺點:若產品矩陣過大,子類別可能爆量。
抽象工廠(Abstract Factory)
建立一族相關產品的一組工廠,例如同時產生 Button、Checkbox、Dialog 的「深色主題工廠」或「蘋果風格工廠」。
跨多產品線時很強,但結構更重。
簡單記憶:
想快速用參數挑類別 → 簡單工廠
想把「怎麼選類別」變成可覆寫、可擴充的決策點 → 工廠方法
想同時建立多個關聯產品 → 抽象工廠
何時該用、何時不必用
適用情境
物件建立邏輯複雜,且會隨場景/環境變化(例:不同部署環境用不同 Logger/HTTP Client)。
想把 new 的細節隔離,讓客端只依賴抽象介面,提升測試性與可替換性。
想遵守 Open/Closed Principle:新增行為時不改舊碼(透過繼承或組態擴充)。
不建議使用
僅是單一且簡單的 new,沒有變體或條件分支。
專案規模小、團隊偏好函數式與工廠函式(可用「函式工廠 + 策略」替代)。
核心結構(以類別版示意)
// Product 介面(JavaScript 沒有 interface,這裡用 JSDoc 說明)
/**
* @typedef {Object} Channel
* @property {(msg: string) => void} deliver
*/
// Concrete Products
class EmailChannel {
deliver(msg) { console.log(`[EMAIL] ${msg}`); }
}
class SmsChannel {
deliver(msg) { console.log(`[SMS] ${msg}`); }
}
// Creator:定義流程骨架,留一個「工廠方法」給子類別覆寫
class NotificationSender {
send(message) {
const channel = this.createChannel(); // <-- 工廠方法
channel.deliver(message);
}
// 工廠方法(預設丟錯,強迫子類別實作)
createChannel() {
throw new Error('You must override createChannel()');
}
}
// Concrete Creators:只覆寫如何建立產品
class EmailNotificationSender extends NotificationSender {
createChannel() { return new EmailChannel(); }
}
class SmsNotificationSender extends NotificationSender {
createChannel() { return new SmsChannel(); }
}
// 用法
new EmailNotificationSender().send('歡迎加入!');
new SmsNotificationSender().send('您的驗證碼是 123456');
重點:send() 的流程固定,但用哪個 Channel 交給 createChannel() 決定。
新增 PushNotificationSender?寫一個子類別覆寫即可,不動舊碼。
實戰 1:通知通道(類別版,含日誌與錯誤處理)
加入更多實務細節:通道可能失敗、要記錄日誌、異常要切換備援。
class Logger {
info(msg) { console.log(`[INFO] ${msg}`); }
error(msg) { console.error(`[ERROR] ${msg}`); }
}
class EmailChannel {
deliver(msg) {
if (!msg) throw new Error('Empty message');
// 真實環境可呼叫第三方 API
console.log(`[EMAIL] ${msg}`);
}
}
class SmsChannel {
deliver(msg) { console.log(`[SMS] ${msg}`); }
}
class NotificationSender {
constructor(logger = new Logger()) {
this.logger = logger;
}
send(message) {
const channel = this.createChannel(); // 工廠方法
try {
channel.deliver(message);
this.logger.info(`sent via ${channel.constructor.name}`);
} catch (err) {
this.logger.error(`send failed: ${err.message}`);
this.onFailover(message, err);
}
}
createChannel() { throw new Error('override me'); }
// 可選的 Template Hook:故障時的備援策略
onFailover(message, err) {
this.logger.info('no failover strategy, dropping message.');
}
}
class EmailNotificationSender extends NotificationSender {
createChannel() { return new EmailChannel(); }
onFailover(message) {
this.logger.info('fallback to SMS');
new SmsNotificationSender(this.logger).send(message);
}
}
class SmsNotificationSender extends NotificationSender {
createChannel() { return new SmsChannel(); }
}
這段示範工廠方法 + 範本方法(Template Method)一起用:
父類別固定流程,子類別只負責「產什麼產品」與「失敗時怎麼補強」。
實戰 2:函式工廠(Functional Style),更貼近 JavaScript 習慣
很多 JS 團隊偏好函式與閉包而非類別。這裡用函式工廠模擬工廠方法概念:把建立邏輯封裝在高階函式中,外部只拿到統一介面。
/**
* @typedef {(data: any) => string} Exporter
*/
/** 產品們:各種匯出器 */
const createCsvExporter = () => /** @type {Exporter} */ (data) =>
Object.values(data).join(',');
const createJsonExporter = () => /** @type {Exporter} */ (data) =>
JSON.stringify(data);
/** 工廠方法概念:用閉包“決定”回傳哪種 Exporter */
const createExporterFactory = (format) => {
/** @type {Record<string, () => Exporter>} */
const registry = {
csv: createCsvExporter,
json: createJsonExporter,
};
const make = registry[format];
if (!make) throw new Error(`Unknown format: ${format}`);
return make(); // 這裡就是“工廠方法”的決策點
};
// 用法
const exporter = createExporterFactory('csv');
console.log(exporter({ id: 1, name: 'Alice' })); // 1,Alice
優點:
更輕量、容易測試(傳不同的 registry 進去即可替換產品)。
不需要繼承,擴充靠「註冊新產品」。
實戰 3:非同步工廠(初始化需要等待的產品)
真實世界裡,很多產品需要非同步初始化(開連線、載入金鑰、Warm-up Cache)。這時工廠方法本身就該是 async。
class DbClient {
constructor(conn) { this.conn = conn; }
async query(sql) { /* ... */ return []; }
}
// 模擬非同步建立連線
const connect = async (dsn) => {
await new Promise(r => setTimeout(r, 50));
return { dsn, connectedAt: Date.now() };
};
class RepositoryCreator {
async createClient() { throw new Error('override'); } // async 工廠方法
async fetchUsers() {
const client = await this.createClient();
return client.query('SELECT * FROM users;');
}
}
class PostgresRepository extends RepositoryCreator {
constructor(dsn) { super(); this.dsn = dsn; this._cached = null; }
async createClient() {
if (this._cached) return this._cached; // 避免重複連線:快取
const conn = await connect(this.dsn);
this._cached = new DbClient(conn);
return this._cached;
}
}
重點:
把等待成本藏在工廠方法,呼叫端只管流程(fetchUsers())。
若建立昂貴,記得快取,避免每次都重連。
可測試性:用工廠方法注入 Test Double
工廠方法能自然注入替身(stub/mock),讓測試不依賴外部資源。
class FakeChannel { constructor() { this.sent = []; } deliver(m) { this.sent.push(m); } }
class TestableSender extends NotificationSender {
constructor(fake, logger) { super(logger); this.fake = fake; }
createChannel() { return this.fake; }
}
// 測試
const fake = new FakeChannel();
const logs = [];
const logger = { info: m => logs.push(m), error: m => logs.push(m) };
new TestableSender(fake, logger).send('hi');
console.assert(fake.sent[0] === 'hi');
擴充策略:避免子類別爆炸(組態式工廠)
當變種很多(上百種),純繼承會讓子類別爆棚。這時可用「註冊表 + 組態」,盡量把差異下放到資料層。
class PaymentChannel {
deliver(amount) { /* ... */ }
}
class StripeChannel extends PaymentChannel {/*...*/}
class PaypalChannel extends PaymentChannel {/*...*/}
class PaymentSender {
constructor(registry) { this.registry = registry; }
send(amount, provider) {
const ChannelCtor = this.registry[provider];
if (!ChannelCtor) throw new Error(`Unknown provider: ${provider}`);
const channel = new ChannelCtor();
channel.deliver(amount);
}
}
// 擴充只需註冊,不改主流程
const sender = new PaymentSender({
stripe: StripeChannel,
paypal: PaypalChannel,
});
常見錯誤與雷點
1. 把所有選擇寫在一個巨大 switch
症狀:新增/修改時必改這支檔,越改越危險。
對策:改用工廠方法(由子類別或註冊表決定),或拆成多個小型策略。
2. 工廠回傳的產品沒有統一介面
症狀:呼叫端寫一堆 if (p.deliver) ... else if (p.send) ...。
對策:先定義最小穩定介面(以 JSDoc/TypeScript 註記),一致命名。
3. 把昂貴物件每次都 new(不快取)
症狀:效能不穩、連線爆量。
對策:在工廠方法加入快取或使用連線池。
4. 把建構細節散落於客端
症狀:客端充滿 new A(new B(new C(...)))。
對策:把巢狀建構集中到工廠,客端只拿介面。
5. 同步/非同步混用
症狀:有時回傳實例,有時回傳 Promise,呼叫端一團亂。
對策:明確規範:若產品需要初始化,就讓工廠方法固定 async,呼叫端用 await。
6. 過度設計
症狀:只有一種產品也硬要上工廠方法。
對策:YAGNI。先用直接 new,等出現第二種變體再抽象。
7. this 綁定踩雷(在 JS 常見)
症狀:把成員方法當回呼傳遞,this 變 undefined。
對策:用箭頭函式或顯式 bind;或回傳純函式產品(閉包)。
class BadFactory {
create() {
return {
// 若回呼以獨立函式呼叫,this 將不是你想的
deliver: this._deliver
};
}
_deliver(msg) { console.log('send', msg, 'ctx=', this); }
}
// 對策:用箭頭函式捕捉詞法 this,或直接回傳無 this 的函式
8. 用字串拼裝類名或動態 eval
症狀:安全風險、難以靜態分析。
對策:用**註冊表(map)**替代 eval,把可用的產品白名單化。
9. 把工廠當成全域單例亂用
症狀:無法在測試或不同流程中替換產品。
對策:顯式傳入工廠/registry(依賴注入),避免硬編碼全域狀態。
10. 區分錯 Simple/Factory Method/Abstract Factory
症狀:想擴充卻發現仍需改原 switch。
對策:要「新增變體不改舊碼」,請用工廠方法(覆寫)或註冊表(資料驅動)。
問題集
Q1:Factory Method 一定要用類別與繼承嗎?
不一定。JavaScript 很適合用函式工廠 + 註冊表實現同樣精神:把決策點集中、把介面統一。
Q2:與策略模式差在哪?
策略模式著重可替換的行為(一組可互換的演算法),而工廠方法著重產品的建立。兩者常一起用:工廠生出不同策略。
Q3:跟 DI 容器、Service Locator 的關係?
DI 容器可視為更通用的「工廠集合」。工廠方法是在程式碼層次定義決策點,DI 是在配置層次管理依賴。小中型專案用工廠方法已足夠。
Q4:在前端框架(React/Vue)還需要嗎?
有時需要。像「依環境建立不同 API Client」、「依 A/B 測試建立不同追蹤器」,用工廠方法能讓 Hook/Composable 更乾淨可測。
進階示例:組態驅動 + 快取 + 非同步
整合上面觀念,做一個可組態的 HTTP Client 工廠:支援重試、逾時、以及在 Node/瀏覽器使用不同底層實作。
// 抽象介面(以 JSDoc 描述)
// /**
// * @typedef {Object} HttpClient
// * @property {(url: string, opts?: RequestInit) => Promise<Response>} fetch
// */
// 產品:瀏覽器 Fetch
const createBrowserClient = (baseUrl) => {
/** @type {import('./types').HttpClient} */
return {
async fetch(path, opts) {
const res = await window.fetch(baseUrl + path, { ...opts });
return res;
}
};
};
// 產品:Node Fetch(這裡以全域 fetch 代表 Node 18+,若無則需帶入 polyfill)
const createNodeClient = (baseUrl) => ({
async fetch(path, opts) {
const res = await fetch(baseUrl + path, { ...opts });
return res;
}
});
// Decorator:加上重試與逾時
const withResilience = (client, { retries = 1, timeoutMs = 5000 } = {}) => {
const timeout = (ms, p) => Promise.race([
p, new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms))
]);
return {
async fetch(url, opts) {
let lastErr;
for (let i = 0; i <= retries; i++) {
try {
return await timeout(timeoutMs, client.fetch(url, opts));
} catch (e) {
lastErr = e;
}
}
throw lastErr;
}
};
};
// Creator:工廠方法(非同步+快取+組態)
class HttpClientFactory {
/**
* @param {{ baseUrl: string; env?: 'browser'|'node'; retries?: number; timeoutMs?: number; }} cfg
*/
constructor(cfg) {
this.cfg = { env: this.detectEnv(), retries: 1, timeoutMs: 5000, ...cfg };
this._cached = null;
}
detectEnv() {
try { return typeof window !== 'undefined' ? 'browser' : 'node'; }
catch { return 'node'; }
}
async createClient() { // 工廠方法
if (this._cached) return this._cached;
const { baseUrl, env, retries, timeoutMs } = this.cfg;
const base =
env === 'browser' ? createBrowserClient(baseUrl) : createNodeClient(baseUrl);
const client = withResilience(base, { retries, timeoutMs });
this._cached = client;
return client;
}
}
// 用法
(async () => {
const factory = new HttpClientFactory({ baseUrl: 'https://api.example.com' });
const http = await factory.createClient();
const res = await http.fetch('/health');
console.log('health status', res.status);
})();
這段展示了工廠方法在真實專案的價值:
抽象產品介面(HttpClient)。
依環境產不同產品。
Decorator 增強能力(重試/逾時)。
工廠方法非同步 + 快取,呼叫端乾淨。
總結
Factory Method 的核心,不是炫技,而是把變化點隔離:
將「建立什麼」的決策收斂到工廠方法;
讓「怎麼使用」的流程維持穩定;
新增變體時不動舊碼,可靠地擴充。
在 JavaScript 世界,你可以用類別繼承實作,也能以函式工廠 + 註冊表達到同樣目的。記住幾個關鍵:穩定介面、一致 async 策略、昂貴資源快取、避免 eval、資料驅動擴充。
做到這些,你的程式碼會更耐久,也更能通過長期維護的考驗。
延伸閱讀推薦:
javaScript設計模式 : Abstract Factory(抽象工廠)
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(策略模式)
