簡介
適配器模式(Adapter)是最常用的結(jié)構(gòu)型模式之一,在現(xiàn)實生活中,適配器模式也是處處可見,比如電源插頭轉(zhuǎn)換器,它可以讓英式的插頭工作在中式的插座上。
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.
簡單來說,就是適配器模式讓原本因為接口不匹配而無法一起工作的兩個類/結(jié)構(gòu)體能夠一起工作。
適配器模式所做的就是將一個接口 Adaptee,通過適配器 Adapter 轉(zhuǎn)換成 Client 所期望的另一個接口 Target 來使用,實現(xiàn)原理也很簡單,就是 Adapter 通過實現(xiàn) Target 接口,并在對應的方法中調(diào)用 Adaptee 的接口實現(xiàn)。

UML 結(jié)構(gòu)

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

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

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

代碼實現(xiàn)
// demo/db/sql.go
package db
// Adaptee SQL語句執(zhí)行返回的結(jié)果,并未實現(xiàn)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 調(diào)用ConsoleRender完成對查詢結(jié)果的渲染輸出
func (c *Console) Output(render ConsoleRender) {
fmt.Println(render.Render())
}
// Target接口,控制臺db查詢結(jié)果渲染接口
type ConsoleRender interface {
Render() string
}
// TableRender表格形式的查詢結(jié)果渲染Adapter
// 關鍵點1: 定義Adapter結(jié)構(gòu)體/類
type TableRender struct {
// 關鍵點2: 在Adapter中聚合Adaptee,這里是把SqlResult作為TableRender的成員變量
result *SqlResult
}
// 關鍵點3: 實現(xiàn)Target接口,這里是實現(xiàn)了ConsoleRender接口
func (t *TableRender) Render() string {
// 關鍵點4: 在Target接口實現(xiàn)中,調(diào)用Adaptee的原有方法實現(xiàn)具體的業(yè)務邏輯
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,實現(xiàn)了將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實例,其中創(chuàng)建Adapter實例時需要傳入Adaptee實例
c.Output(NewTableRender(result))
} else {
c.Output(NewErrorRender(err))
}
fmt.Println("> please enter a sql expression:")
fmt.Print("> ")
}
}
在已經(jīng)有了 Target 接口(ConsoleRender)和 Adaptee(SqlResult)的前提下,總結(jié)實現(xiàn)適配器模式的幾個關鍵點:
- 定義 Adapter 結(jié)構(gòu)體/類,這里是 TableRender 結(jié)構(gòu)體。
- 在 Adapter 中聚合 Adaptee,這里是把 SqlResult 作為 TableRender 的成員變量。
- Adapter 實現(xiàn) Target 接口,這里是 TableRender 實現(xiàn)了 ConsoleRender 接口。
- 在 Target 接口實現(xiàn)中,調(diào)用 Adaptee 的原有方法實現(xiàn)具體的業(yè)務邏輯,這里是在 TableRender.Render() 調(diào)用 SqlResult.ToMap() 方法,得到查詢結(jié)果,然后再對結(jié)果進行渲染。
- 在 Client 需要 Target 接口的地方,傳入適配器 Adapter 實例,其中創(chuàng)建 Adapter 實例時傳入 Adaptee 實例。這里是在 NewTableRender() 創(chuàng)建 TableRender 實例時,傳入 SqlResult 作為入?yún)ⅲS后將 TableRender 實例傳入 Console.Output() 方法。
擴展
適配器模式在 Gin 中的運用
Gin 是一個高性能的 Web 框架,它的常見用法如下:
// 用戶自定義的請求處理函數(shù),類型為gin.HandlerFunc
func myGinHandler(c *gin.Context) {
... // 具體處理請求的邏輯
}
func mAIn() {
// 創(chuàng)建默認的route引擎,類型為gin.Engine
r := gin.Default()
// route定義
r.GET("/my-route", myGinHandler)
// route引擎啟動
r.Run()
}
在實際運用場景中,可能存在這種情況。用戶起初的 Web 框架使用了 Go 原生的 .NET/http,使用場景如下:
// 用戶自定義的請求處理函數(shù),類型為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 類型轉(zhuǎn)換成 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)
}
}
使用方法如下:
// 用戶自定義的請求處理函數(shù),類型為http.Handler
func myHttpHandler(w http.ResponseWriter, r *http.Request) {
... // 具體處理請求的邏輯
}
func main() {
// 創(chuàng)建默認的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 風格的適配器模式實現(xiàn),以更為簡潔的 func 替代了 struct。
典型應用場景
- 將一個接口 A 轉(zhuǎn)換成用戶希望的另外一個接口 B,這樣就能使原來不兼容的接口 A 和接口 B 相互協(xié)作。
- 老系統(tǒng)的重構(gòu)。在不改變原有接口的情況下,讓老接口適配到新的接口。
優(yōu)缺點
優(yōu)點
- 能夠使 Adaptee 和 Target 之間解耦。通過引入新的 Adapter 來適配 Target,Adaptee 無須修改,符合開閉原則。
- 靈活性好,能夠很方便地通過不同的適配器來適配不同的接口。
缺點
- 增加代碼復雜度。適配器模式需要新增適配器,如果濫用會導致系統(tǒng)的代碼復雜度增大。
與其他模式的關聯(lián)
適配器模式 和 裝飾者模式、代理模式 在 UML 結(jié)構(gòu)上具有一定的相似性。但適配器模式改變原有對象的接口,但不改變原有功能;而裝飾者模式和代理模式則在不改變接口的情況下,增強原有對象的功能。
文章配圖
可以在 用Keynote畫出手繪風格的配圖 中找到文章的繪圖方法。
參考
[1] 【Go實現(xiàn)】實踐GoF的23種設計模式:SOLID原則, 元閏子
[2] Design Patterns, Chapter 4. Structural Patterns, GoF
[3] 適配器模式, refactoringguru.cn
[4] Gin Web Framework, Gin