🎯 什麼情境該想到我

當你「不想讓呼叫端知道它拿到的是哪個具體型別」的時候——依設定選 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 套件完全不認識 postgres

TypeScript 的長相

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" 空白匯入時要有註解說明為什麼。
  • 工廠不該吞掉錯誤。設定錯誤要在建立時就回報,不要回一個「半殘」的物件讓它在執行期才爆。
  • 測試時直接建具體型別,不要繞過工廠——工廠是給生產程式用的入口,不是測試的必經之路。

🔗 相關工具