访问者模式理解比较困难。可以认为对象开了一扇门,用来接收访问者,然后访问者便可在对象内部操作对象。简单来说,对象对访问者进行了授权。这样做能够实现对象和操作的解耦,职责更加单一。对象只管理自身,操作功能安置在访问者中。
访问者模式:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
UML:
看完访问者模式定义和UML,可能大家会想,这说的是人话吗?一开始我也是这么想的。但是把定义和UML拆分后,就容易理解了。
先看定义:表示一个作用于某对象结构中的各元素的操作。
对象结构中的各元素:就是指一个类和类里的各种成员变量,对应UML中的Element。
操作:就是指访问者,访问者有操作元素的能力。
所以这句话可以解释为:访问者模式,就是访问者可以操作类里的元素。
再看定义:它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
这句话主要讲访问者模式的优点:类在不做任何改动的情况下,能够增加新的操作/解析方式。如元素类是pdf资源文件类,以前只支持抽取文本内容操作,现在增加压缩、提取文件元信息操作,元素类无需感知。
定义没有解释访问者模式是如何实现的,这时候我们可以看UML。
首先我们看Element,这个是元素类,属于被操作的对象。元素类有成员函数Accept,用于接收访问者。关于Element想提两点:
再来看Vistor,有两个成员函数,入参分别对应ConcreteElementA、ConcreteElementB,即Vistor提供了同一种功能,能够操作不同的Element。
通过UML分析,我们可以看出,Element和Vistor是你中有我,我中有你的关系。
虽然分析完了,可能有很多同学会比较懵,这么麻烦的设计模式我用了干嘛!实际情况是,我也没用过访问者模式。但有些场景,访问者模式还是有用的。
设计模式还是为了解耦,实现高内聚、低耦合。
假设有三种文件类型,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,不但不优雅,而且极易出问题。这时候就可以使用访问者模式。
package main
import "fmt"
/**
* @Description: 读文件接口,用于获取到文件
*/
type ReadFile interface {
Read(fileName string)
Accept(v VistorReadFile)
}
/**
* @Description: 读pdf文件类
*/
type ReadPdfFile struct {
}
/**
* @Description: 读取文件
* @receiver p
* @param fileName
*/
func (p *ReadPdfFile) Read(fileName string) {
fmt.Println("读取pdf文件" + fileName)
}
/**
* @Description: 接受访问者类
* @receiver p
* @param v
*/
func (p *ReadPdfFile) Accept(v VistorReadFile) {
v.VistorPdfFile(p)
}
/**
* @Description: 读取txt文件类
*/
type ReadTxtFile struct {
}
/**
* @Description: 读取文件
* @receiver t
* @param fileName
*/
func (t *ReadTxtFile) Read(fileName string) {
fmt.Println("读取txt文件" + fileName)
}
/**
* @Description: 接受访问者类
* @receiver p
* @param v
*/
func (t *ReadTxtFile) Accept(v VistorReadFile) {
v.VistorTxtFile(t)
}
/**
* @Description: 访问者,包含对pdf和txt的操作
*/
type VistorReadFile interface {
VistorPdfFile(p *ReadPdfFile)
VistorTxtFile(t *ReadTxtFile)
}
/**
* @Description: 提取文件类
*/
type ExactFile struct {
}
/**
* @Description: 提取pdf文件
* @receiver e
* @param p
*/
func (e *ExactFile) VistorPdfFile(p *ReadPdfFile) {
fmt.Println("提取pdf文件内容")
}
/**
* @Description: 提取txt文件
* @receiver e
* @param p
*/
func (e *ExactFile) VistorTxtFile(p *ReadTxtFile) {
fmt.Println("提取txt文件内容")
}
/**
* @Description: 压缩文件类
*/
type CompressionFile struct {
}
/**
* @Description: 压缩pdf文件
* @receiver c
* @param p
*/
func (c *CompressionFile) VistorPdfFile(p *ReadPdfFile) {
fmt.Println("压缩pdf文件内容")
}
/**
* @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添加新的实现即可。如果增加新的功能,文件类也无需感知。
原课程中讲解访问者模式的时候用到了继承和函数重载这两个 Go 中没有的特性,接下来的呢,会通过继承实现。
注意由于没有函数重载,所以我们并不知道传递过来的对象是什么类型,这个时候只能采用类型断言的方式来对不同的类型做不同的操作,但是正式由于没有函数重载,所以其实完全可以不用访问者模式直接传入参数就好了。
以前我们经常说不要用写其他语言的方式来写 Go,Go 不需要太多的设计模式,这个就是一个比较鲜明的例子
package visitor
import (
"fmt"
"path"
)
// Visitor 访问者
type Visitor interface {
Visit(IResourceFile) error
}
// IResourceFile IResourceFile
type IResourceFile interface {
Accept(Visitor) error
}
// NewResourceFile NewResourceFile
func NewResourceFile(filepath string) (IResourceFile, error) {
switch path.Ext(filepath) {
case ".ppt":
return &PPTFile{path: filepath}, nil
case ".pdf":
return &PdfFile{path: filepath}, nil
default:
return nil, fmt.Errorf("not found file type: %s", filepath)
}
}
// PdfFile PdfFile
type PdfFile struct {
path string
}
// Accept Accept
func (f *PdfFile) Accept(visitor Visitor) error {
return visitor.Visit(f)
}
// PPTFile PPTFile
type PPTFile struct {
path string
}
// Accept Accept
func (f *PPTFile) Accept(visitor Visitor) error {
return visitor.Visit(f)
}
// Compressor 实现压缩功能
type Compressor struct{}
// Visit 实现访问者模式方法
// 我们可以发现由于没有函数重载,我们只能通过断言来根据不同的类型调用不同函数
// 但是我们即使不采用访问者模式,我们其实也是可以这么操作的
// 并且由于采用了类型断言,所以如果需要操作的对象比较多的话,这个函数其实也会膨胀的比较厉害
// 后续可以考虑按照命名约定使用 generate 自动生成代码
// 或者是使用反射简化
func (c *Compressor) Visit(r IResourceFile) error {
switch f := r.(type) {
case *PPTFile:
return c.VisitPPTFile(f)
case *PdfFile:
return c.VisitPDFFile(f)
default:
return fmt.Errorf("not found resource typr: %#v", r)
}
}
// VisitPPTFile VisitPPTFile
func (c *Compressor) VisitPPTFile(f *PPTFile) error {
fmt.Println("this is ppt file")
return nil
}
// VisitPDFFile VisitPDFFile
func (c *Compressor) VisitPDFFile(f *PdfFile) error {
fmt.Println("this is pdf file")
return nil
}
package visitor
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestCompressor_Visit(t *testing.T) {
tests := []struct {
name string
path string
wantErr string
}{
{
name: "pdf",
path: "./xx.pdf",
},
{
name: "ppt",
path: "./xx.ppt",
},
{
name: "404",
path: "./xx.xx",
wantErr: "not found file type",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := NewResourceFile(tt.path)
if tt.wantErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tt.wantErr)
return
}
require.NoError(t, err)
compressor := &Compressor{}
f.Accept(compressor)
})
}
}
// 不用 Accept 其实也是可以的
func TestCompressor_Visit2(t *testing.T) {
tests := []struct {
name string
path string
wantErr string
}{
{
name: "pdf",
path: "./xx.pdf",
},
{
name: "ppt",
path: "./xx.ppt",
},
{
name: "404",
path: "./xx.xx",
wantErr: "not found file type",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := NewResourceFile(tt.path)
if tt.wantErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tt.wantErr)
return
}
require.NoError(t, err)
compressor := &Compressor{}
compressor.Visit(f)
})
}
}
访问者模式实现了对象和操作的解耦。可以认为访问者模式有两个维度,一是对象和操作解耦,这个比较容易理解,也符合单一职责原则。二是对象给操作开个大门,这个是否需要主要看业务的复杂度。