上一篇:【Go实现】实践GoF的23种设计模式:迭代器模式
简单的分布式应用系统(示例代码工程):https://github.com/ruanrunxue/Practice-Design-Pattern--Go-Implementation
简介
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 packagedb //Table数据表定义 typeTablestruct{ namestring metadatamap[string]int//key为属性名,value属性值的索引,对应到record上存储 recordsmap[interface{}]record iteratorFactoryTableIteratorFactory//默认使用随机迭代器 }
目的是提供类似于关系型数据库的按列查询能力,比如:
上述的按列查询只是等值比较,未来还可能会实现正则表达式匹配等方式,因此我们需要设计出可供未来扩展的接口。这种场景,使用访问者模式正合适。
代码实现
//demo/db/table_visitor.go packagedb //关键点1:定义表查询的访问者抽象接口,允许后续扩展查询方式 typeTableVisitorinterface{ //关键点2:Visit方法以Element作为入参,这里的Element为Table对象 Visit(table*Table)([]interface{},error) } //关键点3:定义Visitor抽象接口的实现对象,这里FieldEqVisitor实现按列等值查询逻辑 typeFieldEqVisitorstruct{ fieldstring valueinterface{} } //关键点4:为FieldEqVisitor定义Visit方法,实现具体的等值查询逻辑 func(f*FieldEqVisitor)Visit(table*Table)([]interface{},error){ result:=make([]interface{},0) idx,ok:=table.metadata[f.field] if!ok{ returnnil,ErrRecordNotFound } for_,r:=rangetable.records{ ifreflect.DeepEqual(r.values[idx],f.value){ result=append(result,r) } } iflen(result)==0{ returnnil,ErrRecordNotFound } returnresult,nil } funcNewFieldEqVisitor(fieldstring,valueinterface{})*FieldEqVisitor{ return&FieldEqVisitor{ field:field, value:value, } } //demo/db/table.go packagedb typeTablestruct{...} //关键点5:为Element定义Accept方法,入参为Visitor接口 func(t*Table)Accept(visitorTableVisitor)([]interface{},error){ returnvisitor.Visit(t) }
客户端可以这么使用:
funcclient(){ 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) iferr!=nil{ t.Error(err) } iflen(result)!=2{ t.Errorf("visitfailed,want2,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 packagedb //关键点1:定义一个访问者函数类型 typeTableVisitorFuncfunc(table*Table)([]interface{},error) //关键点2:定义工厂方法,工厂方法返回的是一个访问者函数,实现了具体的访问逻辑 funcNewFieldEqVisitorFunc(fieldstring,valueinterface{})TableVisitorFunc{ returnfunc(table*Table)([]interface{},error){ result:=make([]interface{},0) idx,ok:=table.metadata[field] if!ok{ returnnil,ErrRecordNotFound } for_,r:=rangetable.records{ ifreflect.DeepEqual(r.values[idx],value){ result=append(result,r) } } iflen(result)==0{ returnnil,ErrRecordNotFound } returnresult,nil } } //关键点3:为Element定义Accept方法,入参为Visitor函数类型 func(t*Table)AcceptFunc(visitorFuncTableVisitorFunc)([]interface{},error){ returnvisitorFunc(t) }
客户端可以这么使用:
funcclient(){ 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")) iferr!=nil{ t.Error(err) } iflen(result)!=2{ t.Errorf("visitfailed,want2,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 packagemodel //ServiceProfileRecord存储在数据库里的类型 typeServiceProfileRecordstruct{ Idstring//服务ID TypeServiceType//服务类型 StatusServiceStatus//服务状态 Ipstring//服务IP Portint//服务端口 RegionIdstring//服务所属regionId Priorityint//服务优先级,范围0~100,值越低,优先级越高 Loadint//服务负载,负载越高表示服务处理的业务压力越大 }
现在,我们要查询符合指定ServiceId和ServiceType的服务记录,可以这么实现一个 Visitor:
//demo/service/registry/model/service_profile.go packagemodel typeServiceProfileVisitorstruct{ svcIdstring svcTypeServiceType } func(s*ServiceProfileVisitor)Visit(table*db.Table)([]interface{},error){ varresult[]interface{} //通过迭代器来遍历Table的所有数据 iter:=table.Iterator() foriter.HasNext(){ profile:=new(ServiceProfileRecord) iferr:=iter.Next(profile);err!=nil{ returnnil,err } //先匹配ServiceId,如果一致则无须匹配ServiceType ifprofile.Id!=""&&profile.Id==s.svcId{ result=append(result,profile) continue } //ServiceId匹配不上,再匹配ServiceType ifprofile.Type!=""&&profile.Type==s.svcType{ result=append(result,profile) } } returnresult,nil }
典型应用场景
k8s 中,kubectl 通过访问者模式来处理用户定义的各类资源。
编译器中,通常使用访问者模式来实现对语法树解析,比如 LLVM。
希望对一个复杂的数据结构执行某些操作,并支持后续扩展。
优缺点
优点
数据结构和操作算法解耦,符合单一职责原则。
支持对数据结构扩展多种操作,具备较强的可扩展性,符合开闭原则。
缺点
访问者模式某种程度上,要求数据结构必须对外暴露其内在实现,否则访问者就无法遍历其中数据(可以结合迭代器模式来解决该问题)。
如果被访问对象内的数据结构变更,可能要更新所有的访问者实现。
与其他模式的关联
访问者模式 经常和迭代器模式一起使用,使得被访问对象无须向外暴露内在数据结构。
也经常和组合模式一起使用,比如在语法树解析中,递归访问和解析树的每个节点(节点组合成树)。
文章配图
可以在用Keynote画出手绘风格的配图中找到文章的绘图方法。
审核编辑:汤梓红
-
代码
+关注
关注
30文章
4788浏览量
68603 -
设计模式
+关注
关注
0文章
53浏览量
8634 -
迭代器
+关注
关注
0文章
43浏览量
4309
原文标题:【Go实现】实践GoF的23种设计模式:访问者模式
文章出处:【微信号:yuanrunzi,微信公众号:元闰子的邀请】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论