🎯 什麼情境該想到我
當你「要換的不是一個物件,是一整組互相搭配的物件」的時候——
切換「正式環境的一整套外部相依」vs「測試用的一整套假實作」、跨雲廠商的一整族服務。
⚙️ 怎麼用
意圖與和工廠方法的差別
提供一個介面,用來建立一系列「相關或相依」的物件,而不指定它們的具體類別。
| 工廠方法模式 | 抽象工廠 | |
|---|---|---|
| 建立幾種東西 | 一種 | 一族(多種) |
| 關注點 | 「這個東西該是哪個實作」 | ★ 「這一組東西要互相搭配」 |
| 換的粒度 | 換一個實作 | 換一整套 |
★ 核心價值是「一致性保證」:
用了 PostgreSQL 的工廠,就不可能拿到 MySQL 的 QueryBuilder。
它在型別層面防止你混搭不相容的實作。
★ Go 的長相:一個帶多個 New 方法的介面
// ★ 抽象工廠 = 「一個介面,多個建立方法」
type Platform interface {
Storage() Storage
Queue() Queue
Secrets() SecretStore
}
// ──── 一整族:AWS ────
type awsPlatform struct{ cfg aws.Config }
func (p awsPlatform) Storage() Storage { return &s3Storage{p.cfg} }
func (p awsPlatform) Queue() Queue { return &sqsQueue{p.cfg} }
func (p awsPlatform) Secrets() SecretStore { return &smSecrets{p.cfg} }
// ──── 另一整族:本機開發 ────
type localPlatform struct{ dir string }
func (p localPlatform) Storage() Storage { return &fsStorage{p.dir} }
func (p localPlatform) Queue() Queue { return &memQueue{} }
func (p localPlatform) Secrets() SecretStore { return &envSecrets{} }
func NewPlatform(env string) (Platform, error) {
switch env {
case "aws": return awsPlatform{loadAWS()}, nil
case "local": return localPlatform{"/tmp/dev"}, nil
}
return nil, fmt.Errorf("unknown env %q", env)
}
// 使用端完全不知道自己在哪個環境
func run(p Platform) {
p.Storage().Put(ctx, "k", v)
p.Queue().Publish(ctx, msg)
}★ 但在 Go 裡,更常見的做法是「一個 struct 塞好相依」
type Deps struct {
Storage Storage
Queue Queue
Secrets SecretStore
}
func wireAWS(cfg aws.Config) Deps {
return Deps{Storage: &s3Storage{cfg}, Queue: &sqsQueue{cfg}, Secrets: &smSecrets{cfg}}
}
func wireLocal(dir string) Deps {
return Deps{Storage: &fsStorage{dir}, Queue: &memQueue{}, Secrets: &envSecrets{}}
}★ 兩者的差別:
- 抽象工廠(介面):建立是延遲的,每次呼叫
p.Storage()才建 → 適合「有些相依可能用不到,建立很貴」- struct 組裝(wiring):建立是立即的,一次全部備好 → 更簡單、更透明、Go 社群更常用
多數情況下 struct 組裝就夠了。 抽象工廠的介面只在「族的成員很多、且建立成本高或需要延遲」時才划算。
(google/wire、uber-go/fx這類 DI 工具做的就是自動化這件 wiring。)
TypeScript 的長相
type Platform = {
storage(): Storage;
queue(): Queue;
secrets(): SecretStore;
};
// ★ TS 裡通常也是直接用物件字面量組裝,不需要 class
export const localPlatform: Platform = {
storage: () => new FsStorage("/tmp/dev"),
queue: () => new MemQueue(),
secrets: () => new EnvSecrets(),
};生態中的真實案例
| 案例 | 說明 |
|---|---|
| 測試替身的一整套 | ★ 實務上最常見也最值得的用法:整組換成 in-memory 實作,測試不碰網路 |
| 跨雲抽象層 | AWS / GCP / Azure 各一族(但要小心,見下方警告) |
| GUI 工具包 | GoF 的原始範例(Windows/Mac 的一整族 widget)——現在幾乎沒人這樣做了 |
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
- ★ 最大的風險:加一個新成員要改所有族。
Platform介面加一個Cache(),所有實作都得跟著加。族越多、成員越多,這個成本越痛。
→ 這是開閉原則的反向:對「加新族」開放,對「加新成員」封閉。族少成員多時它是錯的選擇。 - ★ 「跨雲抽象層」常常是過度設計。
真正要遷雲的機率很低,而抽象層會把你限制在「所有雲的交集功能」上——失去各家最有價值的特性。
務實的做法是:只抽象你真的用到的那一小塊,並接受未來要改。 - 只有兩族(正式 / 測試)時,直接用 struct wiring 就好,不需要介面。
- 不要為了「以後可能要換」而抽象。YAGNI —— 真的要換時再抽,那時你才知道正確的邊界在哪。
- 族之間的成員必須真的有搭配關係。如果 Storage 和 Queue 其實互相獨立,那就不該綁在同一個工廠裡——各自注入即可。
- 和 工廠方法模式 的界線:只建一種東西就不是抽象工廠。別把名字用錯。