本文分享自華為雲社群《【Go實現】實踐GoF的23種設計模式:介面卡模式》,作者:元閏子。
簡介
介面卡模式(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 結構
場景上下文
在 簡單的分散式應用系統(示例程式碼工程)中,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