摘要:访问者模式的目的是,解耦数据结构和算法,使得系统能够在不改变现有代码结构的基础上,为对象新增一种新的操作。

本文分享自华为云社区《【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))
}
}

总结实现访问者模式的几个关键点:

  1. 定义访问者抽象接口,上述例子为 TableVisitor, 目的是允许后续扩展表查询方式。
  2. 访问者抽象接口中,Visit 方法以 Element 作为入参,上述例子中, Element 为 Table 对象。
  3. 为 Visitor 抽象接口定义具体的实现对象,上述例子为 FieldEqVisitor。
  4. 在访问者的 Visit 方法中实现具体的业务逻辑,上述例子中 FieldEqVisitor.Visit(...) 实现了按列等值查询逻辑。
  5. 在被访问者 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 风格的实现,利用了函数闭包的特点,更加简洁了。

总结几个实现关键点:

  1. 定义一个访问者函数类型,函数签名以 Element 作为入参,上述例子为 TableVisitorFunc 类型。
  2. 定义一个工厂方法,工厂方法返回的是具体的访问访问者函数,上述例子为 NewFieldEqVisitorFunc 方法。这里利用了函数闭包的特性,在访问者函数中直接引用工厂方法的入参,与 FieldEqVisitor 中持有两个成员属性的效果一样。
  3. 为 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

 

点击关注,第一时间了解华为云新鲜技术~

实践GoF的设计模式:访问者模式的更多相关文章

  1. .NET设计模式访问者模式

    一.访问者模式的定义: 表示一个作用于某对象结构中的各元素的操作.它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作. 二.访问者模式的结构和角色: 1.Visitor 抽象访问者角色,为该 ...

  2. 实践GoF的设计模式:迭代器模式

    摘要:迭代器模式主要用在访问对象集合的场景,能够向客户端隐藏集合的实现细节. 本文分享自华为云社区<[Go实现]实践GoF的23种设计模式:迭代器模式>,作者:元闰子. 简介 有时会遇到这 ...

  3. 深入浅出设计模式——访问者模式(Visitor Pattern)

    模式动机 对于系统中的某些对象,它们存储在同一个集合中,且具有不同的类型,而且对于该集合中的对象,可以接受一类称为访问者的对象来访问,而且不同的访问者其访问方式有所不同,访问者模式为解决这类问题而诞生 ...

  4. C++设计模式——访问者模式

    访问者模式 在GOF的<设计模式:可复用面向对象软件的基础>一书中对访问者模式是这样说的:表示一个作用于某对象结构中的各元素的操作.它使你可以在不改变各元素的类的前提下定义作用于这些元素的 ...

  5. C#设计模式-访问者模式

    一. 访问者(Vistor)模式 访问者模式是封装一些施加于某种数据结构之上的操作.一旦这些操作需要修改的话,接受这个操作的数据结构则可以保存不变.访问者模式适用于数据结构相对稳定的系统, 它把数据结 ...

  6. JAVA 设计模式 访问者模式

    用途 访问者模式 (Visitor) 表示一个作用于某对象结构中的各元素的操作. 它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作. 访问者模式是一种行为型模式. 用途

  7. java设计模式---访问者模式

      Java深入到一定程度,就不可避免的碰到设计模式这一概念,了解设计模式,将使自 己对java中的接口或抽象类应用有更深的理解.设计模式在java的中型系统中应用广 泛,遵循一定的编程模式,才能使自 ...

  8. 设计模式 -- 访问者模式(Visitor)

    写在前面的话:读书破万卷,编码如有神--------------------------------------------------------------------主要内容包括: 初识访问者模 ...

  9. Java设计模式—访问者模式

    原文地址:http://www.cnblogs.com/java-my-life/archive/2012/06/14/2545381.html 总结的太棒啦,导致自己看了都不想总结了...... 在 ...

  10. C#设计模式——访问者模式(Visitor Pattern)

    一.概述由于需求的改变,某些类常常需要增加新的功能,但由于种种原因这些类层次必须保持稳定,不允许开发人员随意修改.对此,访问者模式可以在不更改类层次结构的前提下透明的为各个类动态添加新的功能.二.访问 ...

随机推荐

  1. db-cdc之mysql 深入了解并使用binlog

    1.什么是binlog? 2.binlog可以用来干什么? 3.怎么样使用binlog? binlog是记录所有数据库表结构变更(例如CREATE.ALTER TABLE-)以及表数据修改(INSER ...

  2. 服务器常见问题排查(一)——cpu占用高、上下文频繁切换、频繁GC

    一般而言cpu异常往往还是比较好定位的.原因包括业务逻辑问题(死循环).频繁gc以及上下文切换过多.而最常见的往往是业务逻辑(或者框架逻辑)导致的,可以使用jstack来分析对应的堆栈情况. 使用js ...

  3. Kafka 在分布式系统中的 7 大应用场景

    Kafka 介绍 Kafka 是一个开源的分布式流式平台,它可以处理大量的实时数据,并提供高吞吐量,低延迟,高可靠性和高可扩展性.Kafka 的核心组件包括生产者(Producer),消费者(Cons ...

  4. 二叉树、平衡二叉树、红黑树、B树、B+树

    几种树的主要区别: 红黑树为二叉自平衡搜索树,深度大,多用于内存排序: B树为多路(多叉)搜索树,深度低,搜索数据时磁盘IO较少,多用于索引外存数据,只支持随机访问,不支持顺序访问: B+树是对B树的 ...

  5. Ubuntu 编辑文件、安装、删除软件等常用命令(持续更新)

    一.编辑文件 1. sudo vi 文件名,进入文件页面,如图: 2. 按 i 键或者 o 键,进入编辑,左下角出现---------INSERT---------信息 3. 输入完毕后,按ESC退出 ...

  6. [Python急救站课程]整数数列求和

    整数数列求和程序: n = input("请输入整数N: ") sum = 0 for i in range(int(n)): # range用于计数整数.for表示循环 sum ...

  7. Petals

    ------------恢复内容开始------------ 打开发现一堆地址冒红 滑倒后面发现E8,根据经验应该是花指令考点 然后D-->nop-->C-->P得到正常结果 然后第 ...

  8. (Good topic)单词的压缩编码(leetcode3.28每日打卡)

    给定一个单词列表,我们将这个列表编码成一个索引字符串 S 与一个索引列表 A. 例如,如果这个列表是 ["time", "me", "bell&quo ...

  9. 递归+记忆化递归+DP:斐波那契数列

    递归:算法复杂度O(2^N) 1 int fib(int n) 2 { 3 if (n == 0) 4 { 5 return 0; 6 } 7 if (n == 1) 8 { 9 return 1; ...

  10. Centos8.4离线安装JDK+Tomcat+MySQL8.0+Nginx

    一.安装JDK 注:以下命令环境在Xshell中进行. 1.查询出系统自带的OpenJDK及版本 rpm -qa | grep jdk 2.如果显示已安装openjdk则对其进行卸载. 输入:rpm ...