天天看點

實踐GoF的設計模式:通路者模式

摘要:通路者模式的目的是,解耦資料結構和算法,使得系統能夠在不改變現有代碼結構的基礎上,為對象新增一種新的操作。

本文分享自華為雲社群《​​【Go實作】實踐GoF的23種設計模式:通路者模式​​》,作者:元閏子 。

簡介

GoF 對通路者模式(Visitor Pattern)的定義如下:

Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.

通路者模式的目的是,解耦資料結構和算法,使得系統能夠在不改變現有代碼結構的基礎上,為對象新增一種新的操作。

上一篇介紹的疊代器模式也做到了資料結構和算法的解耦,不過它專注于周遊算法。通路者模式,則在周遊的同時,将操作作用到資料結構上,一個常見的應用場景是文法樹的解析。

UML 結構

實踐GoF的設計模式:通路者模式

場景上下文

在 ​​簡單的分布式應用系統​​(示例代碼工程)中,db 子產品用來存儲服務注冊和監控資訊,它是一個 key-value 資料庫。另外,我們給 db 子產品抽象出 Table 對象:

// demo/db/table.go
package db
// Table 資料表定義
type Table struct {
    name            string
    metadata        map[string]int // key為屬性名,value屬性值的索引, 對應到record上存儲
    records         map[interface{}]record
 iteratorFactory TableIteratorFactory // 預設使用随機疊代器
}      

目的是提供類似于關系型資料庫的按列查詢能力,比如:

實踐GoF的設計模式:通路者模式

上述的按列查詢隻是等值比較,未來還可能會實作正規表達式比對等方式,是以我們需要設計出可供未來擴充的接口。這種場景,使用通路者模式正合适。

代碼實作

// demo/db/table_visitor.go
package db
// 關鍵點1: 定義表查詢的通路者抽象接口,允許後續擴充查詢方式
type TableVisitor interface {
 // 關鍵點2: Visit方法以Element作為入參,這裡的Element為Table對象
 Visit(table *Table) ([]interface{}, error)
}
// 關鍵點3: 定義Visitor抽象接口的實作對象,這裡FieldEqVisitor實作按列等值查詢邏輯
type FieldEqVisitor struct {
    field string
    value interface{}
}
// 關鍵點4: 為FieldEqVisitor定義Visit方法,實作具體的等值查詢邏輯
func (f *FieldEqVisitor) Visit(table *Table) ([]interface{}, error) {
 result := make([]interface{}, 0)
 idx, ok := table.metadata[f.field]
 if !ok {
 return nil, ErrRecordNotFound
 }
 for _, r := range table.records {
 if reflect.DeepEqual(r.values[idx], f.value) {
            result = append(result, r)
 }
 }
 if len(result) == 0 {
 return nil, ErrRecordNotFound
 }
 return result, nil
}
func NewFieldEqVisitor(field string, value interface{}) *FieldEqVisitor {
 return &FieldEqVisitor{
        field: field,
        value: value,
 }
}
// demo/db/table.go
package db
type Table struct {...}
// 關鍵點5: 為Element定義Accept方法,入參為Visitor接口
func (t *Table) Accept(visitor TableVisitor) ([]interface{}, error) {
 return visitor.Visit(t)
}      

用戶端可以這麼使用:

func client() {
 table := NewTable("testRegion").WithType(reflect.TypeOf(new(testRegion)))
 table.Insert(1, &testRegion{Id: 1, Name: "beijing"})
 table.Insert(2, &testRegion{Id: 2, Name: "beijing"})
 table.Insert(3, &testRegion{Id: 3, Name: "guangdong"})
 visitor := NewFieldEqVisitor("name", "beijing")
    result, err := table.Accept(visitor)
 if err != nil {
 t.Error(err)
 }
 if len(result) != 2 {
 t.Errorf("visit failed, want 2, got %d", len(result))
 }
}      

總結實作通路者模式的幾個關鍵點:

  1. 定義通路者抽象接口,上述例子為 TableVisitor, 目的是允許後續擴充表查詢方式。
  2. 通路者抽象接口中,Visit 方法以 Element 作為入參,上述例子中, Element 為 Table 對象。
  3. 為 Visitor 抽象接口定義具體的實作對象,上述例子為 FieldEqVisitor。
  4. 在通路者的 Visit 方法中實作具體的業務邏輯,上述例子中 FieldEqVisitor.Visit(...) 實作了按列等值查詢邏輯。
  5. 在被通路者 Element 中定義 Accept 方法,以通路者 Visitor 作為入參。上述例子中為 Table.Accept(...) 方法。

擴充

Go 風格實作

上述實作是典型的面向對象風格,下面以 Go 風格重新實作通路者模式:

// demo/db/table_visitor_func.go
package db
// 關鍵點1: 定義一個通路者函數類型
type TableVisitorFunc func(table *Table) ([]interface{}, error)
// 關鍵點2: 定義工廠方法,工廠方法傳回的是一個通路者函數,實作了具體的通路邏輯
func NewFieldEqVisitorFunc(field string, value interface{}) TableVisitorFunc {
 return func(table *Table) ([]interface{}, error) {
 result := make([]interface{}, 0)
 idx, ok := table.metadata[field]
 if !ok {
 return nil, ErrRecordNotFound
 }
 for _, r := range table.records {
 if reflect.DeepEqual(r.values[idx], value) {
                result = append(result, r)
 }
 }
 if len(result) == 0 {
 return nil, ErrRecordNotFound
 }
 return result, nil
 }
}
// 關鍵點3: 為Element定義Accept方法,入參為Visitor函數類型
func (t *Table) AcceptFunc(visitorFunc TableVisitorFunc) ([]interface{}, error) {
 return visitorFunc(t)
}      

用戶端可以這麼使用:

func client() {
 table := NewTable("testRegion").WithType(reflect.TypeOf(new(testRegion)))
 table.Insert(1, &testRegion{Id: 1, Name: "beijing"})
 table.Insert(2, &testRegion{Id: 2, Name: "beijing"})
 table.Insert(3, &testRegion{Id: 3, Name: "guangdong"})
    result, err := table.AcceptFunc(NewFieldEqVisitorFunc("name", "beijing"))
 if err != nil {
 t.Error(err)
 }
 if len(result) != 2 {
 t.Errorf("visit failed, want 2, got %d", len(result))
 }
}      

Go 風格的實作,利用了函數閉包的特點,更加簡潔了。

總結幾個實作關鍵點:

  1. 定義一個通路者函數類型,函數簽名以 Element 作為入參,上述例子為 TableVisitorFunc 類型。
  2. 定義一個工廠方法,工廠方法傳回的是具體的通路通路者函數,上述例子為 NewFieldEqVisitorFunc 方法。這裡利用了函數閉包的特性,在通路者函數中直接引用工廠方法的入參,與 FieldEqVisitor 中持有兩個成員屬性的效果一樣。
  3. 為 Element 定義 Accept 方法,入參為 Visitor 函數類型 ,上述例子是 Table.AcceptFunc(...) 方法。

與疊代器模式結合

通路者模式經常與疊代器模式一起使用。比如上述例子中,如果你定義的 Visitor 實作不在 db 包内,那麼就無法直接通路 Table 的資料,這時就需要通過 Table 提供的疊代器來實作。

在 ​​簡單的分布式應用系統​​(示例代碼工程)中,db 子產品存儲的服務注冊資訊如下:

// demo/service/registry/model/service_profile.go
package model
// ServiceProfileRecord 存儲在資料庫裡的類型
type ServiceProfileRecord struct {
    Id       string // 服務ID
    Type     ServiceType // 服務類型
    Status   ServiceStatus // 服務狀态
    Ip       string // 服務IP
    Port     int // 服務端口
 RegionId string // 服務所屬regionId
    Priority int // 服務優先級,範圍0~100,值越低,優先級越高
    Load     int // 服務負載,負載越高表示服務處理的業務壓力越大
}      

現在,我們要查詢符合指定 ServiceId 和 ServiceType 的服務記錄,可以這麼實作一個 Visitor:

// demo/service/registry/model/service_profile.go
package model
type ServiceProfileVisitor struct {
 svcId string
 svcType ServiceType
}
func (s *ServiceProfileVisitor) Visit(table *db.Table) ([]interface{}, error) {
 var result []interface{}
 // 通過疊代器來周遊Table的所有資料
 iter := table.Iterator()
 for iter.HasNext() {
 profile := new(ServiceProfileRecord)
 if err := iter.Next(profile); err != nil {
 return nil, err
 }
 // 先比對ServiceId,如果一緻則無須比對ServiceType
 if profile.Id != "" && profile.Id == s.svcId {
            result = append(result, profile)
 continue
 }
 // ServiceId比對不上,再比對ServiceType
 if profile.Type != "" && profile.Type == s.svcType {
            result = append(result, profile)
 }
 }
 return result, nil
}      

典型應用場景

  • k8s 中,kubectl 通過通路者模式來處理使用者定義的各類資源。
  • 編譯器中,通常使用通路者模式來實作對文法樹解析,比如 LLVM。
  • 希望對一個複雜的資料結構執行某些操作,并支援後續擴充。

優缺點

優點

  • 資料結構和操作算法解耦,符合單一職責原則。
  • 支援對資料結構擴充多種操作,具備較強的可擴充性,符合開閉原則。

缺點

  • 通路者模式某種程度上,要求資料結構必須對外暴露其内在實作,否則通路者就無法周遊其中資料(可以結合疊代器模式來解決該問題)。
  • 如果被通路對象内的資料結構變更,可能要更新所有的通路者實作。

與其他模式的關聯

  • 通路者模式 經常和疊代器模式一起使用,使得被通路對象無須向外暴露内在資料結構。
  • 也經常群組合模式一起使用,比如在文法樹解析中,遞歸通路和解析樹的每個節點(節點組合成樹)。

文章配圖

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

參考

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

[2] ​​【Go實作】實踐GoF的23種設計模式:疊代器模式​​, 元閏子

[3] Design Patterns, Chapter 5. Behavioral Patterns, GoF

[4] GO 程式設計模式:K8S VISITOR 模式, 酷殼

繼續閱讀