簡介
適配器模式(Adapter)是最常用的結構型模式之一,在現實生活中,適配器模式也是處處可見,比如電源插頭轉換器,它可以讓英式的插頭工作在中式的插座上。
GoF 對它的定義如下:
Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.
簡單來說,就是適配器模式讓原本因為接口不匹配而無法一起工作的兩個類/結構體能夠一起工作。
適配器模式所做的就是將一個接口 Adaptee,通過適配器 Adapter 轉換成 Client 所期望的另一個接口 Target 來使用,實現原理也很簡單,就是 Adapter 通過實現 Target 接口,并在對應的方法中調用 Adaptee 的接口實現。

UML 結構

場景上下文
在 簡單的分布式應用系統(示例代碼工程File not found · GitHub)中,db 模塊用來存儲服務注冊信息和系統監控數據,它是一個 key-value 數據庫。在 訪問者模式 中,我們為它實現了 Table 的按列查詢功能;同時,我們也為它實現了簡單的 SQL 查詢功能(將會在 解釋器模式 中介紹),查詢的結果是 SqlResult 結構體,它提供一個 toMap 方法將結果轉換成 map 。
為了方便用戶使用,我們將實現在終端控制臺上提供人機交互的能力,如下所示,用戶輸入 SQL 語句,后臺返回查詢結果:

終端控制臺的具體實現為 Console,為了提供可擴展的查詢結果顯示樣式,我們設計了 ConsoleRender 接口,但因 SqlResult 并未實現該接口,所以 Console 無法直接渲染 SqlResult 的查詢結果。

為此,我們需要實現一個適配器,讓 Console 能夠通過適配器將 SqlResult 的查詢結果渲染出來。示例中,我們設計了適配器 TableRender,它實現了 ConsoleRender 接口,并以表格的形式渲染出查詢結果,如前文所示。

代碼實現
// demo/db/sql.go
package db
// Adaptee SQL語句執行返回的結果,并未實現Target接口
type SqlResult struct {
fields []string
vals []interface{}
}
func (s *SqlResult) Add(field string, record interface{}) {
s.fields = Append(s.fields, field)
s.vals = append(s.vals, record)
}
func (s *SqlResult) ToMap() map[string]interface{} {
results := make(map[string]interface{})
for i, f := range s.fields {
results[f] = s.vals[i]
}
return results
}
// demo/db/console.go
package db
// Client 終端控制臺
type Console struct {
db Db
}
// Output 調用ConsoleRender完成對查詢結果的渲染輸出
func (c *Console) Output(render ConsoleRender) {
fmt.Println(render.Render())
}
// Target接口,控制臺db查詢結果渲染接口
type ConsoleRender interface {
Render() string
}
// TableRender表格形式的查詢結果渲染Adapter
// 關鍵點1: 定義Adapter結構體/類
type TableRender struct {
// 關鍵點2: 在Adapter中聚合Adaptee,這里是把SqlResult作為TableRender的成員變量
result *SqlResult
}
// 關鍵點3: 實現Target接口,這里是實現了ConsoleRender接口
func (t *TableRender) Render() string {
// 關鍵點4: 在Target接口實現中,調用Adaptee的原有方法實現具體的業務邏輯
vals := t.result.ToMap()
var header []string
var data []string
for key, val := range vals {
header = append(header, key)
data = append(data, fmt.Sprintf("%v", val))
}
builder := &strings.Builder{}
table := tablewriter.NewWriter(builder)
table.SetHeader(header)
table.Append(data)
table.Render()
return builder.String()
}
// 這里是另一個Adapter,實現了將error渲染的功能
type ErrorRender struct {
err error
}
func (e *ErrorRender) Render() string {
return e.err.Error()
}
客戶端這么使用:
func (c *Console) Start() {
fmt.Println("welcome to Demo DB, enter exit to end!")
fmt.Println("> please enter a sql expression:")
fmt.Print("> ")
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
sql := scanner.Text()
if sql == "exit" {
break
}
result, err := c.db.ExecSql(sql)
if err == nil {
// 關鍵點5:在需要Target接口的地方,傳入適配器Adapter實例,其中創建Adapter實例時需要傳入Adaptee實例
c.Output(NewTableRender(result))
} else {
c.Output(NewErrorRender(err))
}
fmt.Println("> please enter a sql expression:")
fmt.Print("> ")
}
}
在已經有了 Target 接口(ConsoleRender)和 Adaptee(SqlResult)的前提下,總結實現適配器模式的幾個關鍵點:
- 定義 Adapter 結構體/類,這里是 TableRender 結構體。
- 在 Adapter 中聚合 Adaptee,這里是把 SqlResult 作為 TableRender 的成員變量。
- Adapter 實現 Target 接口,這里是 TableRender 實現了 ConsoleRender 接口。
- 在 Target 接口實現中,調用 Adaptee 的原有方法實現具體的業務邏輯,這里是在 TableRender.Render() 調用 SqlResult.ToMap() 方法,得到查詢結果,然后再對結果進行渲染。
- 在 Client 需要 Target 接口的地方,傳入適配器 Adapter 實例,其中創建 Adapter 實例時傳入 Adaptee 實例。這里是在 NewTableRender() 創建 TableRender 實例時,傳入 SqlResult 作為入參,隨后將 TableRender 實例傳入 Console.Output() 方法。
擴展
適配器模式在 Gin 中的運用
Gin 是一個高性能的 Web 框架,它的常見用法如下:
// 用戶自定義的請求處理函數,類型為gin.HandlerFunc
func myGinHandler(c *gin.Context) {
... // 具體處理請求的邏輯
}
func mAIn() {
// 創建默認的route引擎,類型為gin.Engine
r := gin.Default()
// route定義
r.GET("/my-route", myGinHandler)
// route引擎啟動
r.Run()
}
在實際運用場景中,可能存在這種情況。用戶起初的 Web 框架使用了 Go 原生的 .NET/http,使用場景如下:
// 用戶自定義的請求處理函數,類型為http.Handler
func myHttpHandler(w http.ResponseWriter, r *http.Request) {
... // 具體處理請求的邏輯
}
func main() {
// route定義
http.HandleFunc("/my-route", myHttpHandler)
// route啟動
http.ListenAndServe(":8080", nil)
}
因性能問題,當前客戶準備切換至 Gin 框架,顯然,myHttpHandler 因接口不兼容,不能直接注冊到 gin.Default() 上。為了方便用戶,Gin 框架提供了一個適配器 gin.WrapH,可以將 http.Handler 類型轉換成 gin.HandlerFunc 類型,它的定義如下:
// WrapH is a helper function for wrapping http.Handler and returns a Gin middleware.
func WrapH(h http.Handler) HandlerFunc {
return func(c *Context) {
h.ServeHTTP(c.Writer, c.Request)
}
}
使用方法如下:
// 用戶自定義的請求處理函數,類型為http.Handler
func myHttpHandler(w http.ResponseWriter, r *http.Request) {
... // 具體處理請求的邏輯
}
func main() {
// 創建默認的route引擎
r := gin.Default()
// route定義
r.GET("/my-route", gin.WrapH(myHttpHandler))
// route引擎啟動
r.Run()
}
在這個例子中,gin.Engine 就是 Client,gin.HandlerFunc 是 Target 接口,http.Handler 是 Adaptee,gin.WrapH 是 Adapter。這是一個 Go 風格的適配器模式實現,以更為簡潔的 func 替代了 struct。
典型應用場景
- 將一個接口 A 轉換成用戶希望的另外一個接口 B,這樣就能使原來不兼容的接口 A 和接口 B 相互協作。
- 老系統的重構。在不改變原有接口的情況下,讓老接口適配到新的接口。
優缺點
優點
- 能夠使 Adaptee 和 Target 之間解耦。通過引入新的 Adapter 來適配 Target,Adaptee 無須修改,符合開閉原則。
- 靈活性好,能夠很方便地通過不同的適配器來適配不同的接口。
缺點
- 增加代碼復雜度。適配器模式需要新增適配器,如果濫用會導致系統的代碼復雜度增大。
與其他模式的關聯
適配器模式 和 裝飾者模式、代理模式 在 UML 結構上具有一定的相似性。但適配器模式改變原有對象的接口,但不改變原有功能;而裝飾者模式和代理模式則在不改變接口的情況下,增強原有對象的功能。
文章配圖
可以在 用Keynote畫出手繪風格的配圖 中找到文章的繪圖方法。
參考
[1] 【Go實現】實踐GoF的23種設計模式:SOLID原則, 元閏子
[2] Design Patterns, Chapter 4. Structural Patterns, GoF
[3] 適配器模式, refactoringguru.cn
[4] Gin Web Framework, Gin