JavaScript 访问者模式:打造高扩展性的对象结构

embedded/2024/12/22 0:51:39/

一. 前言

在面向对象编程中,访问者模式Visitor Pattern)是一种行为设计模式,它允许我们向现有的类结构添加新的操作,而无需修改这些类。这对于需要对类层次结构中的元素进行复杂算法处理的场景非常有用。

本文将详细介绍访问者模式的概念、实现方式及其在 JavaScript 中的一个应用小案例,让我们更加快速的了解它。

二. 什么是访问者模式

1. 定义

访问者模式定义了一个访问者的接口,该接口可以处理一个对象结构中的各个元素,而无需改变各元素的类。使用访问者模式可以让用户在不修改现有对象结构的情况下定义新的操作。

2. 核心角色

  • Object Structure(对象结构):是一个包含一个或多个 Element 对象的集合,同时提供一个接受操作 accept(),以接收一个访问者对象。

  • Element(元素):是对象结构中每个对象的类,它提供一个 accept()方法以接收访问者。

  • Visitor(访问者):是一个接口,为 Element 类中的每一个具体类声明一个 Visit 方法。

  • Concrete Visitor(具体访问者):实现了 Visitor 接口中声明的各个 Visit 方法,每个方法实现了对应 Element 类型的业务逻辑。

3. UML

三. 实现方式

访问者模式的实现方式依赖于具体的应用场景和技术栈。在 JavaScript 中,实现访问者模式可以通过多种方式,其中最常见的方法是使用对象组合和多态来完成。

假设我们有一个简单的表达式树,其中包含两种节点类型:加法节点和数字节点。我们将使用访问者模式来计算表达式的值。

  1. 定义元素接口:首先,需要定义一个抽象的元素接口,该接口至少包含一个 accept 方法,此方法接受一个访问者作为参数。

// 元素接口
class Element {accept(visitor) {throw new Error('Method not implemented.')}
}
  1. 具体元素实现:为每种具体的元素类型实现该接口。每个具体元素类都必须实现 accept 方法,并调用访问者对应的访问方法。

// 具体元素 - 加法节点
class AddNode extends Element {constructor(left, right) {super()this.left = leftthis.right = right}accept(visitor) {return visitor.visitAddNode(this)}
}// 具体元素 - 数字节点
class NumberNode extends Element {constructor(value) {super()this.value = value}accept(visitor) {return visitor.visitNumberNode(this)}
}
  1. 定义访问者接口:定义一个访问者接口,该接口为每一种具体的元素类型声明一个访问方法。

// 访问者接口
class Visitor {visitAddNode(node) {throw new Error('Method not implemented.')}visitNumberNode(node) {throw new Error('Method not implemented.')}
}
  1. 具体访问者实现:为每种访问者类型实现具体的访问逻辑,每个具体访问者类都要实现所有访问方法。

// 具体访问者 - 表达式求值
class Evaluator extends Visitor {visitAddNode(node) {return this.visit(node.left) + this.visit(node.right)}visitNumberNode(node) {return node.value}visit(node) {return node.accept(this)}
}
  1. 对象结构:定义一个对象结构,它包含元素的集合,并提供方法来迭代这些元素,允许访问者访问它们。

// 对象结构
class ExpressionTree {constructor(root) {this.root = root}evaluate() {const evaluator = new Evaluator()return evaluator.visit(this.root)}
}
  1. 具体使用:最后,编写相应代码,实例化具体元素和访问者,并通过对象结构让访问者访问所有的元素。

// 使用
const tree = new ExpressionTree(new AddNode(new NumberNode(1), new AddNode(new NumberNode(2), new NumberNode(3))))console.log(tree.evaluate()) // 输出: 6

解释一下,在以上这几个步骤中:

  • AddNodeNumberNode 是具体元素,它们都实现了 accept 方法,调用访问者对应的 visitAddNodevisitNumberNode 方法。

  • Evaluator 是一个具体访问者,它实现了访问者接口中定义的 visitAddNodevisitNumberNode 方法,用于计算表达式的值。

  • ExpressionTree 是对象结构,它持有表达式的根节点,并提供 evaluate 方法来遍历树结构,执行计算。

这种方法的优点在于它允许我们在不修改现有元素类的情况下引入新的访问者类(例如,添加一个新的访问者来打印表达式的字符串表示形式)。这样就可以很容易地扩展系统的功能。

四. 实战应用

通过以上我们已经学习到的实现方式,接下来我们基于访问者模式来设计一个对象类型校验的方式。主要原理是:借用对象的 toString 方法。

我们使用访问者模式来创建一个类型检查器,它可以检查给定对象的类型,并根据对象的类型执行特定的操作。这样可以方便地扩展类型检查逻辑,而无需修改现有代码。

1. 定义元素接口

首先,我们需要定义一个抽象的元素基类,该类包含一个 accept 方法,用于接收访问者。

class Element {accept(visitor) {throw new Error('Method not implemented.')}
}

2. 具体元素

然后,我们定义具体的元素类,每个类都实现 accept 方法,并调用访问者对应的访问方法。

class StringElement extends Element {constructor(value) {super()this.value = value}accept(visitor) {visitor.visit(this)}
}class NumberElement extends Element {constructor(value) {super()this.value = value}accept(visitor) {visitor.visit(this)}
}class BooleanElement extends Element {constructor(value) {super()this.value = value}accept(visitor) {visitor.visit(this)}
}class ArrayElement extends Element {constructor(value) {super()this.value = value}accept(visitor) {visitor.visit(this)}
}class ObjectElement extends Element {constructor(value) {super()this.value = value}accept(visitor) {visitor.visit(this)}
}// 可以继续添加其他类型的元素

3. 定义访问者接口

接着,我们定义一个访问者接口,它为每一种具体的元素类型声明一个访问方法。

class Visitor {visit(element) {throw new Error('Method not implemented.')}
}

4. 具体访问者

然后,我们实现具体的访问者类,每个类都会实现访问者接口中的方法,并提供具体的功能实现。

class TypeChecker extends Visitor {visit(element) {const type = this.getType(element)console.log(`Type of '${element.value}' is ${type}.`)}getType(element) {const toString = Object.prototype.toString.call(element.value)switch (toString) {case '[object String]':return 'String'case '[object Number]':return 'Number'case '[object Boolean]':return 'Boolean'case '[object Array]':return 'Array'case '[object Object]':return 'Object'default:return 'Unknown'}}
}

5. 客户端代码

最后,编写客户端代码来实例化具体元素和访问者,并通过元素的 accept 方法让访问者访问所有元素。

// 创建具体元素
const stringElement = new StringElement('Hello')
const numberElement = new NumberElement(123)
const booleanElement = new BooleanElement(true)
const arrayElement = new ArrayElement([1, 2, 3])
const objectElement = new ObjectElement({ key: 'value' })// 创建访问者
const typeChecker = new TypeChecker()// 使用访问者检查所有元素的类型
stringElement.accept(typeChecker) // 输出: "Type of 'Hello' is String."
numberElement.accept(typeChecker) // 输出: "Type of '123' is Number."
booleanElement.accept(typeChecker) // 输出: "Type of 'true' is Boolean."
arrayElement.accept(typeChecker) // 输出: "Type of '1,2,3' is Array."
objectElement.accept(typeChecker) // 输出: "Type of '{ key: 'value' }' is Object."
  1. **元素基类 Element**:定义了一个 accept 方法,该方法用于接收访问者。

  2. 具体元素:如 StringElementNumberElement 等,它们都继承自 Element 并实现了 accept 方法,该方法会调用访问者中对应的方法。

  3. **访问者接口 Visitor**:定义了一个通用的 visit 方法,具体访问者需要实现这个方法。

  4. **具体访问者 TypeChecker**:实现了访问者接口中的方法,并根据元素类型执行特定的操作。getType 方法使用 Object.prototype.toString.call() 方法来确定对象的确切类型,并返回相应的类型名称。

通过这种方式,我们可以在不修改现有元素类的情况下添加新的类型检查逻辑。如果需要添加更多的类型检查功能,只需扩展 Visitor 接口并实现新的具体访问者类即可。这种方法不仅提高了代码的可扩展性,还使得代码更加模块化和易于维护。

在实际开发过程中,可能我们不会完全标准化的像上述这种方式来使用访问者模式,但是我们应该在编码过程中借用其中的思想,所以在前端开发中,大多数情况下我们使用的也都是访问者模式的变种,保证单一职责原则,不修改现有对象结构。

五. 优缺点

1. 优点

  • 让用户可以在不修改现有对象结构的情况下添加新功能。

  • 符合单一职责原则,分离了数据结构和作用于结构上的操作。

2. 缺点

通过以上了解到,缺点很明显:

  • 增加了许多新的类。

  • 如果对象结构中元素种类频繁变动,则访问者类也会频繁变动。

六. 总结

访问者模式通过定义一个访问者的接口以及一系列访问者类,使得可以在不修改现有对象结构的前提下为对象结构添加新的操作。

这种方式非常适合于需要对复杂的数据结构执行多种不同且不相关操作的场景。然而,这也意味着当对象结构发生变化时,访问者模式可能会变得复杂且难以维护。

更多 JavaScript 设计模式请关注我的专栏:JavaScript 设计模式专栏

 


http://www.ppmy.cn/embedded/125029.html

相关文章

数学基础-向量投影

向量的点乘和叉乘是向量代数中的基本运算,在物理学、工程学和计算机图形学中有广泛的应用。下面我们分别讲解它们的定义、性质,并结合矩阵运算进行说明。 一、向量的点乘(内积) 定义: 对于两个n维实向量 a [ a 1 …

专业软件共享技术的应用与预测

摘要:为掌握集团公司科研软件许可证的真实需求,提高其利用率,通过对各分公司海量监测数据的统计分析,挖掘软件许可证应用峰谷周期不同的规律,利用共享方法设计和建立许可证应用中心,实现企业版软件在集团公…

代码随想录:53、寻宝

53.寻宝 采用两种最小生成树算法分别来做一下 Prim算法 #include <iostream>#include<vector> #include<climits>using namespace std;#define endl \nint main(){std::ios::sync_with_stdio(false);cin.tie(0); cout.tie(0);int v,e;int x,y,k;cin>>…

制作一个流水灯,控制发光二极管由上至下再由下至上反复循环点亮显示,每次点亮一个发光二级管(Proteus 与Keil uVision联合仿真)

一、代码编写 &#xff08;1&#xff09;编写程序来控制发光二极管由上至下的反复循环流水点亮&#xff0c;每次点亮一个发光二极管。 #define uchar unsigned char // 定义uchar为unsigned char类型uchar tab[] {0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0x7f, 0x…

如何保证 Redis 与数据库的数据一致性

在现代的应用开发中&#xff0c;Redis 作为一种高性能的内存数据库&#xff0c;常常被用来缓存热点数据&#xff0c;以提高系统的响应速度和吞吐量。然而&#xff0c;由于 Redis 是内存数据库&#xff0c;与传统的关系型数据库&#xff08;如 MySQL&#xff09;在数据存储和管理…

第五章:软件工程 (5.1软件工程定义--5.2软件需求)

5.1 软件工程定义 软件工程由方法、工具和过程3个部分组成。 方法&#xff1a; 完成软件项目的技术手段&#xff0c;支持整个软件生命周期 工具&#xff1a; 是人们在开发软件的活动中智力和体力的扩展与延伸&#xff0c;它自动或半自动地支持软件的开发和管理&#xff0c;支…

Sym-NCO:利用对称性进行神经组合优化

文章目录 Abstract1 Introduction2 组合优化马尔可夫决策过程中的对称性2.1 组合马尔可夫决策过程2.2 CO-MDP中的对称性3 对称神经组合优化3.1 通过LSym-RL正则化REINFORCE的问题和解决方案对称性3.2 通过预先识别的对称性学习不变表示: L i n v L_{inv} Linv​4 相关工作5 Ex…

回到原点再出发

原文What Goes Around Comes Around作者Michael Stonebraker & Joseph M. Hellerstein其他译文https://zhuanlan.zhihu.com/p/111322429 1. 摘要 本文总结了近35年来的数据模型方案&#xff0c;分成9个不同的时代&#xff0c;讨论了每个时代的方案。我们指出&#xff0c;…