🎯 什麼情境該想到我
當你「想用某個東西,但它的介面跟你需要的不合,而你又不能(或不想)改它」的時候——第三方 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 是遺留系統整合的主力,配合 工具-接縫 使用——它讓你不用動舊程式碼就能接上測試與新實作。
🔗 相關工具
- 裝飾者模式 —— 同樣是包裝,但保持介面不變、增加行為
- 代理模式 —— 同樣是包裝,介面也不變,控制存取
- 外觀模式 —— 簡化一整組介面,不是一對一轉換
- 橋接模式 —— 事前設計的分離,Adapter 是事後的補救
- 工具-Go介面小而隱式 —— 為什麼 Go 的 Adapter 特別便宜
- 工具-接縫、工具-打破依賴以便測試 —— 遺留系統整合
- 設計模式