摘要:訪問者模式的目的是,解耦數據結構和演算法,使得系統能夠在不改變現有代碼結構的基礎上,為對象新增一種新的操作。 本文分享自華為雲社區《【Go實現】實踐GoF的23種設計模式:訪問者模式》,作者:元閏子 。 簡介 GoF 對訪問者模式(Visitor Pattern)的定義如下: Represent ...
摘要:訪問者模式的目的是,解耦數據結構和演算法,使得系統能夠在不改變現有代碼結構的基礎上,為對象新增一種新的操作。
本文分享自華為雲社區《【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 模式, 酷殼
[5] 訪問者模式, refactoringguru.cn