🎯 什麼情境該想到我

當你「想用某個東西,但它的介面跟你需要的不合,而你又不能(或不想)改它」的時候——第三方 SDK、遺留程式碼、標準庫。
★ Go 的隱式介面滿足讓這個模式幾乎免費,這是 Go 相對 Java 最舒服的地方之一。

⚙️ 怎麼用

意圖

把一個類別的介面轉換成客戶端期望的另一個介面,讓原本不相容的東西能一起工作。

客戶端 ──需要──▶ Target(你要的介面)
                      ▲
                      │ 實作
                  Adapter ──持有──▶ Adaptee(人家的介面)

★ Go 的長相:隱式介面讓 Adapter 幾乎不用寫

// 你要的介面
type Notifier interface {
    Notify(ctx context.Context, msg string) error
}
 
// 第三方 SDK 的介面(你改不了)
type SlackClient struct{}
func (c *SlackClient) PostMessage(channel, text string, opts ...slack.Opt) (string, error)
 
// ★ Adapter:一個小 struct,把簽章轉一下
type slackNotifier struct {
    client  *SlackClient
    channel string
}
 
func (s slackNotifier) Notify(ctx context.Context, msg string) error {
    _, err := s.client.PostMessage(s.channel, msg)
    return err
}

★ Java 需要 implements Notifier 明確宣告;Go 不用。
這代表你可以為任何型別(包括別人的型別)寫 adapter,而對方完全不需要知道你的介面存在。
這就是 Go 的 “accept interfaces, return structs” 能成立的原因(見 工具-Go介面小而隱式)。

★ 函式型別 Adapter:Go 標準庫最漂亮的一招

// 標準庫的定義
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
 
// ★ 這就是一個 Adapter —— 把「函式」適配成「介面」
type HandlerFunc func(ResponseWriter, *Request)
 
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)                    // 只是呼叫自己
}
 
// 於是任何符合簽章的函式都能當 Handler 用
http.Handle("/x", http.HandlerFunc(myFunc))

★ 這個「XxxFunc 適配器」是 Go 的慣用法,值得學起來自己用:
http.HandlerFunc、sort.SliceStable 的比較函式、io.WriterFunc(社群常見)、
grpc.UnaryServerInterceptor。

它讓「一個方法的介面」和「一個函式」可以互換——
呼叫端想用函式就用函式、想用有狀態的 struct 就用 struct,兩邊都不用改。

自己定義的範例:

type Notifier interface{ Notify(context.Context, string) error }
 
type NotifierFunc func(context.Context, string) error
func (f NotifierFunc) Notify(ctx context.Context, m string) error { return f(ctx, m) }
 
// 測試時:
var n Notifier = NotifierFunc(func(ctx context.Context, m string) error {
    captured = append(captured, m)
    return nil
})

★ 這讓「寫測試替身」從「定義一個 struct + 實作所有方法」變成「寫一個匿名函式」。

標準庫裡的其他 Adapter

案例適配什麼
io.NopCloser(r)把 io.Reader 適配成 io.ReadCloser(Close 什麼都不做)
bufio.NewReader(r)同時是 Adapter 與 裝飾者模式(加了緩衝行為)
strings.NewReader(s)把 string 適配成 io.Reader
http.FileServer(http.Dir("."))把檔案系統適配成 Handler

TypeScript 的長相

// 結構型別(structural typing)讓 TS 也有隱式滿足
type Notifier = { notify(msg: string): Promise<void> };
 
// ★ 直接用物件字面量當 adapter,連 class 都不用
const slackNotifier: Notifier = {
  notify: (msg) => slack.postMessage(CHANNEL, msg).then(() => {}),
};

★ TS 和 Go 一樣是結構型別,所以 Adapter 同樣便宜。
差別是 TS 連 class 都不需要——一個物件字面量就是 adapter。

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • ★ 不要把 Adapter 寫成 外觀模式。
    Adapter 是一對一的介面轉換(不改變功能);Facade 是把一整組複雜的東西簡化成一個入口。
    Adapter 開始「幫忙做很多事」的時候,它已經變成 Facade 或領域服務了。
  • ★ 介面不合,有時該改的是你的介面。
    如果每個實作都要寫一層厚厚的 adapter,通常代表你的 Target 介面設計得不對(太大、抽象層次錯)。
    → Go 的慣例是介面小、由呼叫端定義(見 工具-Go介面小而隱式),這樣自然不需要厚 adapter。
  • 語義不能硬轉。對方的 API 沒有「取消」的概念,你的介面卻有 ctx context.Context——
    adapter 能讓它編譯過,但取消不會真的生效。這種「假裝支援」比不支援更危險,要明確註記。
  • 錯誤轉換要小心:第三方的錯誤型別包進你的介面時,要決定是包裝(%w,保留可 errors.Is 判斷)還是轉成領域錯誤(見 工具-Go現代錯誤處理)。
  • 雙向 adapter(兩邊都能轉)通常是設計問題的訊號。
  • Adapter 是遺留系統整合的主力,配合 工具-接縫 使用——它讓你不用動舊程式碼就能接上測試與新實作。

🔗 相關工具