上一篇:【Go完成】实践GoF的23种规划形式:迭代器形式
简单的分布式运用体系(示例代码工程):github.com/ruanrunxue/…
简介
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
更多文章请重视微信大众号:元闰子的邀请