概念

访问者模式(Visitor Pattern)是一种行为型设计模式,它允许在不改变被访问对象的类的前提下定义对其进行操作的新操作


访问者模式通常涉及两个核心角色:访问者(Visitor)和被访问元素(Element)

  • 访问者(Visitor)

    定义了对每个被访问元素的操作方法

    每个操作方法都对应着被访问元素的不同类型,可以通过访问者来实现对被访问元素的不同操作

  • 被访问元素(Element)

    定义了一个接受访问者对象的方法,并将自身作为参数传递给访问者对象

    被访问元素可以是单个对象或者对象的集合


访问者模式的核心思想是将对对象的操作(算法)从对象本身中提取出来,并将其封装到不同的访问者对象中,从而实现对对象操作的解耦

这种模式的优点在于,可以在不修改被访问对象的类的情况下定义新的操作,同时也使得新增操作的扩展更加灵活和可控

访问者模式适用于以下情况:

  • 当需要对一个对象结构中的元素进行不同类型的操作,并且希望将操作与对象的数据结构解耦时,可以使用访问者模式
  • 当希望在不修改对象类的情况下定义新的操作,并且希望新增操作的扩展更加灵活和可控时,访问者模式也是一个很好的选择


举个简单的例子,考虑一个文件系统中的不同类型的文件对象(如文本文件、图像文件、音频文件等)

访问者模式可以将对文件对象的操作(如打印文件内容、压缩文件、加密文件等)抽象成访问者对象,并在不修改文件对象类的情况下定义新的操作,从而实现对文件对象的不同操作的解耦


实现条件

  1. 存在一组不同类型的对象

    访问者模式适用于存在一组不同类型的对象,并且这些对象之间存在一定的复杂关系,例如对象之间存在继承关系或者对象之间存在组合关系等

  2. 需要对对象进行不同的操作

    访问者模式适用于需要对一组不同类型的对象进行不同的操作,并且这些操作可能会随着对象的类型而变化的情况

  3. 对象的结构相对稳定

    访问者模式适用于对象的结构相对稳定,并且不太容易发生变化的情况

    因为访问者模式需要将不同类型的对象和操作进行解耦,如果对象的结构经常发生变化,可能会导致访问者对象的变化较大

  4. 需要对对象进行多种操作

    访问者模式适用于需要对一组对象进行多种不同操作的情况,并且这些操作之间可能没有直接关联


优点

  1. 增加新的操作很容易

    访问者模式使得增加新的操作很容易,只需要定义一个新的访问者对象即可,无需修改现有的对象结构

  2. 符合开闭原则

    访问者模式通过将操作封装在访问者对象中,并将访问者对象与对象结构分离,使得可以在不修改现有对象结构的情况下增加新的操作,符合开闭原则

  3. 解耦了对象结构和操作

    访问者模式将对象结构和操作进行了解耦,使得可以独立地改变对象结构或者操作,并且可以相互独立地进行扩展

  4. 支持不同类型的访问者

    访问者模式支持定义不同类型的访问者对象,并且可以在不同的情况下选择合适的访问者对象进行操作,从而提高了系统的灵活性和可扩展性


缺点

  1. 增加新的元素类很困难

    访问者模式使得增加新的元素类变得很困难,因为需要在每个访问者对象中添加对应的操作,可能会导致访问者对象的数量和复杂度增加

  2. 破坏封装性

    访问者模式将操作封装在访问者对象中,可能会破坏对象的封装性,因为访问者对象需要访问对象的内部状态和结构

  3. 可能导致性能问题

    访问者模式需要在访问者对象和元素对象之间建立关联,可能会导致访问者对象过多或者访问者对象过于庞大,从而可能会导致性能问题


实现方式

访问者基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 定义一个Visitor类,提供访问者模式的基本结构。
*/
class Visitor {
/**
* 访问给定的文件对象。
* @param {Object} file 文件对象,需要有getType方法用于获取文件类型。
* @return {any} 返回调用的具体访问方法的结果。
* @throws {Error} 如果找不到对应的访问方法,则抛出错误。
*/
visit(file) {
if (typeof this[file.getType()] === 'function') {
return this[file.getType()](file)
}
throw new Error(`Visitor does not support files of type: ${file.getType()}`)
}
}


访问者子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/**
* PrintVisitor类继承自Visitor类,实现对不同文件类型的打印操作。
*/
class PrintVisitor extends Visitor {
/**
* 打印文本文件。
* @param {Object} file 文本文件对象,需要包含name和content属性。
*/
text(file) {
console.log(`Printing text file ${file.name}:`)
console.log(file.content)
}

/**
* 打印图像文件。
* @param {Object} file 图像文件对象,需要包含name属性。
* 此方法为示例,实际的图像打印逻辑需要根据需求实现。
*/
image(file) {
console.log(`Printing image file ${file.name}`)
// 实际的图像打印逻辑
}

/**
* 打印音频文件。
* @param {Object} file 音频文件对象,需要包含name属性。
* 此方法为示例,实际的音频打印逻辑需要根据需求实现。
*/
audio(file) {
console.log(`Printing audio file ${file.name}`)
// 实际的音频打印逻辑
}
}

/**
* CompressVisitor类继承自Visitor类,实现对不同文件类型的压缩操作。
*/
class CompressVisitor extends Visitor {
/**
* 压缩文本文件。
* @param {Object} file 文本文件对象,需要包含name属性。
*/
text(file) {
console.log(`Compressing text file ${file.name}`)
// 文本文件压缩逻辑
}

/**
* 压缩图像文件。
* @param {Object} file 图像文件对象,需要包含name属性。
* 此方法为示例,实际的图像压缩逻辑需要根据需求实现。
*/
image(file) {
console.log(`Compressing image file ${file.name}`)
// 图像文件压缩逻辑
}

/**
* 压缩音频文件。
* @param {Object} file 音频文件对象,需要包含name属性。
* 此方法为示例,实际的音频压缩逻辑需要根据需求实现。
*/
audio(file) {
console.log(`Compressing audio file ${file.name}`)
// 音频文件压缩逻辑
}
}

/**
* EncryptVisitor类继承自Visitor类,实现对不同文件类型的加密操作。
*/
class EncryptVisitor extends Visitor {
/**
* 加密文本文件。
* @param {Object} file 文本文件对象,需要包含name属性。
*/
text(file) {
console.log(`Encrypting text file ${file.name}`)
// 文本文件加密逻辑
}

/**
* 加密图像文件。
* @param {Object} file 图像文件对象,需要包含name属性。
* 此方法为示例,实际的图像文件加密逻辑需要根据需求实现。
*/
image(file) {
console.log(`Encrypting image file ${file.name}`)
// 图像文件加密逻辑
}

/**
* 加密音频文件。
* @param {Object} file 音频文件对象,需要包含name属性。
* 此方法为示例,实际的音频文件加密逻辑需要根据需求实现。
*/
audio(file) {
console.log(`Encrypting audio file ${file.name}`)
// 音频文件加密逻辑
}
}

// 导出定义的类
export {
Visitor,
PrintVisitor,
CompressVisitor,
EncryptVisitor
}


文件基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { Visitor } from '../VisitorPattern'

/**
* FileSystem类表示文件系统的一个抽象基类。
* 该类提供了基本的文件系统对象功能,包括接受访问者和获取文件类型的方法。
* 该类不应该直接实例化,应由其子类实例化。
*/
class FileSystem {
/**
* 构造函数初始化文件系统对象。
* @param {string} name 文件系统的名称。
* @throws {TypeError} 如果尝试直接构造FileSystem实例,则抛出TypeError。
*/
constructor(name) {
if (new.target === FileSystem) {
throw new TypeError('Cannot construct File instances directly')
}
this.name = name
}

/**
* 接受一个访问者对象并对当前对象进行访问。
* @param {Visitor} visitor 访问者对象。
* @throws {TypeError} 如果传入的参数不是Visitor类型,则抛出TypeError。
*/
accept(visitor) {
if (!(visitor instanceof Visitor)) {
throw new TypeError('Visitor object expected')
}
visitor.visit(this)
}

/**
* 获取文件类型的抽象方法。
* 该方法应由子类实现,以提供具体的文件类型信息。
* @throws {Error} 当调用该方法时,抛出一个错误,提示必须由子类实现该方法。
*/
getType() {
throw new Error("getType method must be implemented by subclasses")
}
}

export default FileSystem


文件子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
* TextFile类继承自FileSystem类,用于处理文本文件的相关操作。
*/
import FileSystem from './File'

/**
* 创建一个TextFile实例。
* @param {string} name 文件名。
* @param {string} content 文件内容。
*/
class TextFile extends FileSystem {
constructor(name, content) {
super(name) // 调用父类的构造函数
this.content = content // 设置文件内容
}

/**
* 获取文件类型。
* @returns {string} 返回文件类型,此处固定为'text'。
*/
getType() {
return 'text'
}
}

export default TextFile



/**
* ImageFile类继承自FileSystem类,用于处理图像文件相关操作。
*/
import FileSystem from './File'

/**
* 创建一个ImageFile实例。
* @param {string} name 文件名。
* @param {Blob|Uint8Array} data 文件数据。
*/
class ImageFile extends FileSystem {
constructor(name, data) {
super(name) // 调用父类构造函数
this.data = data // 存储文件数据
}

/**
* 获取文件类型。
* @return {string} 返回文件类型,此处固定为'image'。
*/
getType() {
return 'image'
}
}

export default ImageFile



/**
* 导入文件系统基础类
*/
import FileSystem from './File'

/**
* AudioFile 类,继承自 FileSystem,用于表示音频文件。
*/
class AudioFile extends FileSystem {
/**
* 构造函数,创建一个 AudioFile 实例。
* @param {string} name - 音频文件的名称。
* @param {string|Blob|MediaSource} audio - 音频数据,可以是文件路径、Blob对象或MediaSource对象。
*/
constructor(name, audio) {
super(name)
this.audio = audio
}

/**
* 获取文件类型,此处固定返回 'audio'。
* @return {string} 返回字符串 'audio',表示此类处理的是音频文件。
*/
getType() {
return 'audio'
}
}

/**
* 导出 AudioFile 类作为默认导出,便于其他模块使用。
*/
export default AudioFile


使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* 导入各种文件类型的子类和访问者模式的相关类。
*/
import TextFile from '../SubClass/TextFile'
import ImageFile from '../SubClass/ImageFile'
import AudioFile from '../SubClass/AudioFile'
import { CompressVisitor, EncryptVisitor, PrintVisitor } from '../VisitorPattern'

// 创建不同类型的文件实例
const textFile = new TextFile("document.txt", "This is the content of the text file.")
const imageFile = new ImageFile("photo.jpg", "Image data...")
const audioFile = new AudioFile("music.mp3", "Audio data...")

// 创建不同功能的访问者实例
const printVisitor = new PrintVisitor()
const compressVisitor = new CompressVisitor()
const encryptVisitor = new EncryptVisitor()


// 对所有文件进行打印访问
// 输出:
// Printing text file document.txt:
// This is the content of the text file.
textFile.accept(printVisitor)

// 输出:
// Printing image file photo.jpg
imageFile.accept(printVisitor)

// 输出:
// Printing audio file music.mp3
audioFile.accept(printVisitor)


// 对所有文件进行压缩访问
// 输出:
// Compressing text file document.txt
textFile.accept(compressVisitor)

// 输出:
// Compressing image file photo.jpg
imageFile.accept(compressVisitor)

// 输出:
// Compressing audio file music.mp3
audioFile.accept(compressVisitor)


// 对所有文件进行加密访问
// 输出:
// Encrypting text file document.txt
textFile.accept(encryptVisitor)

// 输出:
// Encrypting image file photo.jpg
imageFile.accept(encryptVisitor)

// 输出:
// Encrypting audio file music.mp3
audioFile.accept(encryptVisitor)


场景

  1. 编译器设计

    编译器可以使用访问者模式来遍历语法树并执行不同的操作,例如语法分析、类型检查、代码生成等

  2. 文件处理

    像你之前提到的文件系统中的不同类型的文件对象,可以使用访问者模式来实现文件的不同操作,如打印、压缩、加密等

  3. UI 组件

    在图形界面开发中,如果有不同类型的 UI 组件(如按钮、文本框、下拉菜单等),可以使用访问者模式来执行不同的操作,如渲染、验证、序列化等

  4. 数据结构操作

    当需要对数据结构执行多种操作时,例如遍历树、图等,可以使用访问者模式来实现这些操作,而不必修改数据结构本身

  5. 游戏开发

    在游戏开发中,访问者模式可以用于实现不同类型的游戏对象的行为,如处理碰撞、更新状态等

  6. 医疗信息系统

    在医疗信息系统中,可以使用访问者模式来执行不同的操作,如数据采集、统计分析、报表生成等


源代码