0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

GoF设计模式之访问者模式

元闰子的邀请 来源:元闰子的邀请 作者:元闰子的邀请 2022-10-08 11:05 次阅读

上一篇:【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 结构

c3487bdc-4571-11ed-96c9-dac502259ad0.jpg

场景上下文

在简单的分布式应用系统(示例代码工程)中,db 模块用来存储服务注册和监控信息,它是一个 key-value 数据库。另外,我们给 db 模块抽象出Table对象:

//demo/db/table.go
packagedb

//Table数据表定义
typeTablestruct{
namestring
metadatamap[string]int//key为属性名,value属性值的索引,对应到record上存储
recordsmap[interface{}]record
iteratorFactoryTableIteratorFactory//默认使用随机迭代器
}

目的是提供类似于关系型数据库的按列查询能力,比如:

c399aa70-4571-11ed-96c9-dac502259ad0.jpg

上述的按列查询只是等值比较,未来还可能会实现正则表达式匹配等方式,因此我们需要设计出可供未来扩展的接口。这种场景,使用访问者模式正合适。

代码实现

//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,微信公众号:元闰子的邀请】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Command模式与动态语言

    Gof的设计模式中,有一个模式引起的争议比较大,有很多人甚至认为这个模式应该排除在OO模式之外,原因在于它不具有OO的特性
    发表于 06-22 10:20 1013次阅读
    Command<b class='flag-5'>模式</b>与动态语言

    Java设计模式分析观察

    观察模式的流程跟报纸订阅方式一致,即:观察模式=出版+订阅,只是名称不一样,出版
    发表于 09-26 17:36 0次下载

    在 Java8 环境下实现观察模式的实例分析

    观察(Observer)模式又名发布-订阅(Publish/Subscribe)模式,是四人组(GoF,即 Erich Gamma、Richard Helm、Ralph Johnso
    发表于 10-12 16:09 0次下载
    在 Java8 环境下实现观察<b class='flag-5'>者</b><b class='flag-5'>模式</b>的实例分析

    Java设计模式(二十一):中介模式

    中介模式(Mediator Pattern) 中介模式(Mediator Pattern): 属于对象的行为模式。又叫调停
    发表于 01-24 11:28 311次阅读

    嵌入式软件设计设计模式

    文章目录前言1.设计模式适配器模式2.设计模式单例模式3.设计
    发表于 10-21 11:07 9次下载
    嵌入式软件设计<b class='flag-5'>之</b>设计<b class='flag-5'>模式</b>

    嵌入式软件设计模式 好文值得收藏

    本文引用自本人公众号文章: 嵌入式开发中的两点编程思想   C语言也很讲究设计模式?一文讲透   包含如下: 01)C语言和设计模式(继承、封装、多态)   02)C语言和设计模式访问者
    的头像 发表于 06-20 09:09 1967次阅读

    GoF给装饰模式的定义

    的源码,就会发现 middleware 功能的实现用的就是装饰模式(Decorator Pattern)。
    的头像 发表于 06-29 10:22 814次阅读

    GoF设计模式观察模式

    现在有 2 个服务,Service A 和 Service B,通过 REST 接口通信;Service A 在某个业务场景下调用 Service B 的接口完成一个计算密集型任务,假设接口为 http://service_b/api/v1/domain;该任务运行时间很长,但 Service A 不想一直阻塞在接口调用上。为了满足 Service A 的要求,通常有 2 种方案:
    的头像 发表于 07-25 11:32 1026次阅读

    GoF设计模式代理模式

    它是一个使用率非常高的设计模式,在现实生活中,也是很常见。比如,演唱会门票黄牛。假设你需要看一场演唱会,但官网上门票已经售罄,于是就当天到现场通过黄牛高价买了一张。在这个例子中,黄牛就相当于演唱会门票的代理,在正式渠道无法购买门票的情况下,你通过代理完成了该目标。
    的头像 发表于 10-17 09:45 938次阅读

    设计模式访问者设计模式

    访问者设计模式是一种行为型设计模式,用于将算法与对象结构分离。它允许你在不改变对象结构的前提下定义新的操作。
    的头像 发表于 06-06 11:25 818次阅读

    设计模式行为型:访问者模式

    访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。
    的头像 发表于 06-07 15:11 755次阅读
    设计<b class='flag-5'>模式</b>行为型:<b class='flag-5'>访问者</b><b class='flag-5'>模式</b>

    设计模式创造性:建造模式

    建造模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
    的头像 发表于 06-09 16:25 732次阅读
    设计<b class='flag-5'>模式</b>创造性:建造<b class='flag-5'>者</b><b class='flag-5'>模式</b>

    UVM设计模式访问者模式

    Visitor Pattern: 允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。换言之,如果component的数据结构是比较稳定的,但其是易于变化的,那么使用访问者模式是个不错的选择。
    的头像 发表于 08-11 09:28 768次阅读
    UVM设计<b class='flag-5'>模式</b><b class='flag-5'>之</b><b class='flag-5'>访问者</b><b class='flag-5'>模式</b>

    实践GoF的23种设计模式:备忘录模式

    相对于代理模式、工厂模式等设计模式,备忘录模式(Memento)在我们日常开发中出镜率并不高,除了应用场景的限制之外,另一个原因,可能是备忘录模式
    的头像 发表于 11-25 09:05 549次阅读
    实践<b class='flag-5'>GoF</b>的23种设计<b class='flag-5'>模式</b>:备忘录<b class='flag-5'>模式</b>

    实践GoF的23种设计模式:解释器模式

    解释器模式(Interpreter Pattern)应该是 GoF 的 23 种设计模式中使用频率最少的一种了,它的应用场景较为局限。
    的头像 发表于 04-01 11:01 707次阅读
    实践<b class='flag-5'>GoF</b>的23种设计<b class='flag-5'>模式</b>:解释器<b class='flag-5'>模式</b>