🎯 什麼情境該想到我
當你「不想讓呼叫端知道它拿到的是哪個具體型別」的時候——依設定選 driver、依副檔名選解析器、依環境選實作。
⚙️ 怎麼用
意圖與經典結構
定義一個建立物件的介面,但讓子類別決定要實例化哪一個類別。
Creator(抽象) Product(抽象)
+ factoryMethod() : Product ▲
+ someOperation() │
▲ │
│ ConcreteProduct
ConcreteCreator
(覆寫 factoryMethod)
★ 經典 GoF 的定義靠「繼承 + 覆寫」 —— 這正是它在 Go 裡長得完全不一樣的原因。
★ Go 的長相:沒有繼承,所以它就是「一個回傳介面的函式」
type Storage interface {
Get(ctx context.Context, key string) ([]byte, error)
Put(ctx context.Context, key string, v []byte) error
}
// ★ 這就是 Go 的工廠方法 —— 一個回傳介面的普通函式
func NewStorage(cfg Config) (Storage, error) {
switch cfg.Kind {
case "s3": return newS3(cfg)
case "local": return newLocal(cfg)
case "memory":return newMemory(), nil
default: return nil, fmt.Errorf("unknown storage kind %q", cfg.Kind)
}
}★ Go 版本少掉了整個「Creator 類別階層」。
GoF 需要 Creator/ConcreteCreator 兩層,是因為 C++/Java 沒有頭等函式——
要把「建立邏輯」當成可替換的東西傳遞,只能用「可覆寫的方法」包起來。
Go 有頭等函式,所以一個func就夠了。
註冊表版本(外掛式,避免 switch 越長越大):
var registry = map[string]func(Config) (Storage, error){}
func Register(kind string, f func(Config) (Storage, error)) {
registry[kind] = f
}
func NewStorage(cfg Config) (Storage, error) {
f, ok := registry[cfg.Kind]
if !ok {
return nil, fmt.Errorf("unknown storage kind %q", cfg.Kind)
}
return f(cfg)
}
// 各實作在自己的檔案裡 init() 註冊 —— ★ 加新實作不用改 NewStorage
func init() { Register("s3", newS3) }★ 這是 工具-開閉原則 的直接體現,也是 database/sql 的做法:
sql.Register("postgres", &pq.Driver{}) // driver 自己註冊
db, _ := sql.Open("postgres", dsn) // ★ sql 套件完全不認識 postgresTypeScript 的長相
type Storage = {
get(key: string): Promise<Uint8Array | null>;
put(key: string, v: Uint8Array): Promise<void>;
};
// 用 discriminated union 讓 TS 幫你檢查窮盡性
type Config =
| { kind: "s3"; bucket: string }
| { kind: "local"; dir: string }
| { kind: "memory" };
export function createStorage(cfg: Config): Storage {
switch (cfg.kind) {
case "s3": return new S3Storage(cfg.bucket);
case "local": return new LocalStorage(cfg.dir);
case "memory": return new MemoryStorage();
default: {
// ★ 加了新的 kind 卻忘記處理 → 這一行編譯期就報錯
const _exhaustive: never = cfg;
return _exhaustive;
}
}
}★
never的窮盡性檢查是 TS 相對 Go 的一個實質優勢(見 工具-判別聯集)。
Go 的switch漏一個 case 只會在執行期走到default。
生態中的真實案例
| 案例 | 說明 |
|---|---|
sql.Open(driverName, dsn) | ★ 註冊表式工廠的教科書範例 |
image.Decode(r) | 依檔案內容的 magic bytes 選解碼器(各格式 init() 註冊) |
crypto.Hash.New() | 依演算法回傳 hash.Hash |
Node 的 createServer / driver 套件 | 同樣是回傳介面的函式 |
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
- ★ 只有一種實作時不要抽介面。Go 社群的共識是 “accept interfaces, return structs” ——
工廠回傳具體型別,讓「呼叫端」自己定義它需要的介面。
過早抽介面會產生「只有一個實作的介面」,純粹是噪音(見 工具-Go介面小而隱式)。 - ★ 區分「工廠方法」與「簡單工廠」。
NewStorage(cfg)嚴格說是簡單工廠(不是 GoF 的工廠方法)。
GoF 的工廠方法必須有 Creator 階層——而那個階層在 Go 裡不存在也不需要。面試時值得點出這個差異。 - switch 越長越大就改註冊表,否則每加一個實作都要改核心檔案(違反 工具-開閉原則)。
init()註冊有副作用:套件被 import 就會註冊,用_ "github.com/lib/pq"空白匯入時要有註解說明為什麼。- 工廠不該吞掉錯誤。設定錯誤要在建立時就回報,不要回一個「半殘」的物件讓它在執行期才爆。
- 測試時直接建具體型別,不要繞過工廠——工廠是給生產程式用的入口,不是測試的必經之路。