Go語言實現GoF設計模式:介面卡模式

2023-12-12 12:00:19

本文分享自華為雲社群《【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)的前提下,總結實現介面卡模式的幾個關鍵點:

  1. 定義 Adapter 結構體/類,這裡是 TableRender 結構體。
  2. 在 Adapter 中聚合 Adaptee,這裡是把 SqlResult 作為 TableRender 的成員變數。
  3. Adapter 實現 Target 介面,這裡是 TableRender 實現了 ConsoleRender 介面。
  4. 在 Target 介面實現中,呼叫 Adaptee 的原有方法實現具體的業務邏輯,這裡是在 TableRender.Render() 呼叫 SqlResult.ToMap() 方法,得到查詢結果,然後再對結果進行渲染。
  5. 在 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 相互共同作業。
  • 老系統的重構。在不改變原有介面的情況下,讓老介面適配到新的介面。

優缺點

優點

  1. 能夠使 Adaptee 和 Target 之間解耦。通過引入新的 Adapter 來適配 Target,Adaptee 無須修改,符合開閉原則
  2. 靈活性好,能夠很方便地通過不同的介面卡來適配不同的介面。

缺點

  1. 增加程式碼複雜度。介面卡模式需要新增介面卡,如果濫用會導致系統的程式碼複雜度增大。

與其他模式的關聯

介面卡模式 和 裝飾者模式代理模式 在 UML 結構上具有一定的相似性。但介面卡模式改變原有物件的介面,但不改變原有功能;而裝飾者模式和代理模式則在不改變介面的情況下,增強原有物件的功能。

文章配圖

可以在 用Keynote畫出手繪風格的配圖 中找到文章的繪圖方法。

參考

[1] 【Go實現】實踐GoF的23種設計模式:SOLID原則, 元閏子

[2] Design Patterns, Chapter 4. Structural Patterns, GoF

[3] 介面卡模式refactoringguru.cn

[4] Gin Web Framework, Gin

 

點選關注,第一時間瞭解華為雲新鮮技術~