本文分享自華為雲社群《【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
的介面實現。
在 簡單的分散式應用系統(範例程式碼工程)中,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
)的前提下,總結實現介面卡模式的幾個關鍵點:
TableRender
結構體。SqlResult
作為 TableRender
的成員變數。TableRender
實現了 ConsoleRender
介面。TableRender.Render()
呼叫 SqlResult.ToMap()
方法,得到查詢結果,然後再對結果進行渲染。NewTableRender()
建立 TableRender
範例時,傳入 SqlResult
作為入參,隨後將 TableRender
範例傳入 Console.Output()
方法。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
。
介面卡模式 和 裝飾者模式、代理模式 在 UML 結構上具有一定的相似性。但介面卡模式改變原有物件的介面,但不改變原有功能;而裝飾者模式和代理模式則在不改變介面的情況下,增強原有物件的功能。
可以在 用Keynote畫出手繪風格的配圖 中找到文章的繪圖方法。
[1] 【Go實現】實踐GoF的23種設計模式:SOLID原則, 元閏子
[2] Design Patterns, Chapter 4. Structural Patterns, GoF
[3] 介面卡模式, refactoringguru.cn
[4] Gin Web Framework, Gin