Go设计模式(24)-访问者模式

398 阅读5分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

访问者模式理解比较困难。可以认为对象开了一扇门,用来接收访问者,然后访问者便可在对象内部操作对象。简单来说,对象对访问者进行了授权。这样做能够实现对象和操作的解耦,职责更加单一。对象只管理自身,操作功能安置在访问者中。

UML类图位置:www.processon.com/view/link/6…

本文代码链接为:github.com/shidawuhen/…

1.定义

1.1 访问者模式

访问者模式:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

UML:

图片

1.2分析

看完访问者模式定义和UML,可能大家会想,这说的是人话吗?一开始我也是这么想的。但是把定义和UML拆分后,就容易理解了。

定义分析

先看定义:表示一个作用于某对象结构中的各元素的操作。

对象结构中的各元素:就是指一个类和类里的各种成员变量,对应UML中的Element。

操作:就是指访问者,访问者有操作元素的能力。

所以这句话可以解释为:访问者模式,就是访问者可以操作类里的元素。

再看定义:它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

这句话主要讲访问者模式的优点:类在不做任何改动的情况下,能够增加新的操作/解析方式。如元素类是pdf资源文件类,以前只支持抽取文本内容操作,现在增加压缩、提取文件元信息操作,元素类无需感知。

UML分析

定义没有解释访问者模式是如何实现的,这时候我们可以看UML。

首先我们看Element,这个是元素类,属于被操作的对象。元素类有成员函数Accept,用于接收访问者。关于Element想提两点:

  1. 成员函数Accept中的入参vistor是接口或者是父类,不是子类

  2. Element可以有一个operator函数,ConcreteElementA和ConcreteElementB可实现该函数。

再来看Vistor,有两个成员函数,入参分别对应ConcreteElementA、ConcreteElementB,即Vistor提供了同一种功能,能够操作不同的Element。

通过UML分析,我们可以看出,Element和Vistor是你中有我,我中有你的关系。

2.应用场景

虽然分析完了,可能有很多同学会比较懵,这么麻烦的设计模式我用了干嘛!实际情况是,我也没用过访问者模式。但有些场景,访问者模式还是有用的。

设计模式还是为了解耦,实现高内聚、低耦合。

假设有三种文件类型,pdf、word、txt,需要对这三种文件做内容提取、压缩、获取文件元信息操作,我们应该如何设计。

我们肯定需要创建pdf、word、txt三个类,实现文件的读取。

然后我们实现内容提取、压缩、获取文件元信息三个类,每个类有三个函数,用来处理不同类型的文件。

现在已经将所有文件读取完毕,需要对文件分别进行内容提取、压缩、获取文件元信息。

我们可以这么实现:

func test() {
   fileList := make([]int, 10)
  
   fmt.Println("------提取文件")
   for _, f := range fileList {
      if "f是pdf" == "true" {
         fmt.Println("调用pdf提取接口")
      }else "f是txt" == "true" {
         fmt.Println("调用txt提取接口")
      }
   }
   fmt.Println("------压缩文件")
   for _, f := range fileList {
      if "f是pdf" == "true" {
         fmt.Println("调用pdf压缩接口")
      }else "f是txt" == "true" {
         fmt.Println("调用txt压缩接口")
      }
   }
}

如果这样实现,当增加新文件类型或者新功能时,都要修改一堆if-else,不但不优雅,而且极易出问题。这时候就可以使用访问者模式。

3.代码实现

package main

import "fmt"

/**
 * @Author: Jason Pang
 * @Description: 读文件接口,用于获取到文件
 */
type ReadFile interface {
   Read(fileName string)
   Accept(v VistorReadFile)
}

/**
 * @Author: Jason Pang
 * @Description: 读pdf文件类
 */
type ReadPdfFile struct {
}

/**
 * @Author: Jason Pang
 * @Description: 读取文件
 * @receiver p
 * @param fileName
 */
func (p *ReadPdfFile) Read(fileName string) {
   fmt.Println("读取pdf文件" + fileName)
}

/**
 * @Author: Jason Pang
 * @Description: 接受访问者类
 * @receiver p
 * @param v
 */
func (p *ReadPdfFile) Accept(v VistorReadFile) {
   v.VistorPdfFile(p)
}

/**
 * @Author: Jason Pang
 * @Description: 读取txt文件类
 */
type ReadTxtFile struct {
}

/**
 * @Author: Jason Pang
 * @Description: 读取文件
 * @receiver t
 * @param fileName
 */
func (t *ReadTxtFile) Read(fileName string) {
   fmt.Println("读取txt文件" + fileName)
}

/**
 * @Author: Jason Pang
 * @Description: 接受访问者类
 * @receiver p
 * @param v
 */
func (t *ReadTxtFile) Accept(v VistorReadFile) {
   v.VistorTxtFile(t)
}

/**
 * @Author: Jason Pang
 * @Description: 访问者,包含对pdf和txt的操作
 */
type VistorReadFile interface {
   VistorPdfFile(p *ReadPdfFile)
   VistorTxtFile(t *ReadTxtFile)
}

/**
 * @Author: Jason Pang
 * @Description: 提取文件类
 */
type ExactFile struct {
}

/**
 * @Author: Jason Pang
 * @Description: 提取pdf文件
 * @receiver e
 * @param p
 */
func (e *ExactFile) VistorPdfFile(p *ReadPdfFile) {
   fmt.Println("提取pdf文件内容")
}

/**
 * @Author: Jason Pang
 * @Description: 提取txt文件
 * @receiver e
 * @param p
 */
func (e *ExactFile) VistorTxtFile(p *ReadTxtFile) {
   fmt.Println("提取txt文件内容")
}

/**
 * @Author: Jason Pang
 * @Description: 压缩文件类
 */
type CompressionFile struct {
}

/**
 * @Author: Jason Pang
 * @Description: 压缩pdf文件
 * @receiver c
 * @param p
 */
func (c *CompressionFile) VistorPdfFile(p *ReadPdfFile) {
   fmt.Println("压缩pdf文件内容")
}

/**
 * @Author: Jason Pang
 * @Description: 压缩txt文件
 * @receiver c
 * @param p
 */
func (c *CompressionFile) VistorTxtFile(p *ReadTxtFile) {
   fmt.Println("压缩txt文件内容")
}

func main() {
   filesList := []ReadFile{
      &ReadPdfFile{},
      &ReadTxtFile{},
      &ReadPdfFile{},
      &ReadTxtFile{},
   }
   //提取文件
   fmt.Println("--------------------------提取文件")
   extract := ExactFile{}
   for _, f := range filesList {
      f.Accept(&extract)
   }
   //压缩文件
   fmt.Println("--------------------------压缩文件")
   compress := CompressionFile{}
   for _, f := range filesList {
      f.Accept(&compress)
   }
}

输出:

➜ myproject go run main.go

--------------------------提取文件

提取pdf文件内容

提取txt文件内容

提取pdf文件内容

提取txt文件内容

--------------------------压缩文件

压缩pdf文件内容

压缩txt文件内容

压缩pdf文件内容

压缩txt文件内容

这种写法,如果增加新的文件类型,main中代码无需改动,只需要vistor添加新的实现即可。如果增加新的功能,文件类也无需感知。

总结

访问者模式实现了对象和操作的解耦。可以认为访问者模式有两个维度,一是对象和操作解耦,这个比较容易理解,也符合单一职责原则。二是对象给操作开个大门,这个是否需要主要看业务的复杂度。

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客为:shidawuhen.github.io/

往期文章回顾:

  1. 设计模式

  2. 招聘

  3. 思考

  4. 存储

  5. 算法系列

  6. 读书笔记

  7. 小工具

  8. 架构

  9. 网络

  10. Go语言