摘要:通路者模式的目的是,解耦資料結構和算法,使得系統能夠在不改變現有代碼結構的基礎上,為對象新增一種新的操作。
本文分享自華為雲社群《【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 結構
場景上下文
在 簡單的分布式應用系統(示例代碼工程)中,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 // 預設使用随機疊代器
}
目的是提供類似于關系型資料庫的按列查詢能力,比如:
上述的按列查詢隻是等值比較,未來還可能會實作正規表達式比對等方式,是以我們需要設計出可供未來擴充的接口。這種場景,使用通路者模式正合适。
代碼實作
// 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))
}
}
總結實作通路者模式的幾個關鍵點:
- 定義通路者抽象接口,上述例子為 TableVisitor, 目的是允許後續擴充表查詢方式。
- 通路者抽象接口中,Visit 方法以 Element 作為入參,上述例子中, Element 為 Table 對象。
- 為 Visitor 抽象接口定義具體的實作對象,上述例子為 FieldEqVisitor。
- 在通路者的 Visit 方法中實作具體的業務邏輯,上述例子中 FieldEqVisitor.Visit(...) 實作了按列等值查詢邏輯。
- 在被通路者 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 風格的實作,利用了函數閉包的特點,更加簡潔了。
總結幾個實作關鍵點:
- 定義一個通路者函數類型,函數簽名以 Element 作為入參,上述例子為 TableVisitorFunc 類型。
- 定義一個工廠方法,工廠方法傳回的是具體的通路通路者函數,上述例子為 NewFieldEqVisitorFunc 方法。這裡利用了函數閉包的特性,在通路者函數中直接引用工廠方法的入參,與 FieldEqVisitor 中持有兩個成員屬性的效果一樣。
- 為 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 模式, 酷殼