🎯 什麼情境該想到我

當你「要換的不是一個物件,是一整組互相搭配的物件」的時候——
切換「正式環境的一整套外部相依」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 其實互相獨立,那就不該綁在同一個工廠裡——各自注入即可。
  • 和 工廠方法模式 的界線:只建一種東西就不是抽象工廠。別把名字用錯。

🔗 相關工具