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

一. 前言

在面向对象编程中,访问者模式(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 = left
    this.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.mfbz.cn/a/888658.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【AI绘画SD教程】Lineart线稿上色和IP-Adapter 人像写真插件实操教学,轻松助你生成多种风格的AI人像大片!SD零基础入门到精通教程

大家好,我是画画的小强 今天给大家分享一下如何用AI绘画工具StableDiffusion当中的 LineArt线稿处理 和 IP-Adapter 实操教学。 本期教程开始之前,请先确保你的电脑已经安装好StableDiffusion这款AI绘图工具,如你还没有安装使用&#xff0c…

最新价值5000元的V2M2引擎传奇源码2024BLUE升级版 团购

最新团购的V2M2引擎源码2024年BLUE升级版 特点优势是最新XE12编辑器,微端,各种自定义UI 无限仿GOM引擎功能 参考地址:最新价值5000元的V2M2引擎传奇源码2024BLUE升级版[原始团购版]_1234FCOM专注游戏工具及源码例子分享下载地址:BlueCodePXL…

适合技术小白入门 AI 编程的六个场景

前言 AI 编程最近特别热闹。 自媒体文章说它很强大,确实身边也会看到技术小白用它做出酷炫作品,令人艳羡。 但你自己用时却常遇到坑,找技术朋友一问听到的回答是“AI 干不了这个、铁定会把你带沟里去”。 谁说得对?技术小白到底能…

Linux——磁盘分区、挂载

Linux 分区 原理介绍 原理图如下 当我们在/home目录下新建一个文件a.txt时,该文件实际上是存放在硬盘B的分区1中的,这就是图里说的,当进入某个目录,可以进入到该目录下挂载的分区里的意思 硬盘说明 应用实例:挂载一个…

Linux的六个入侵检查思路及预防

背景 入侵检查是保障计算机安全运行的重要手段之一, 通过操作系统的静态配置分析、日志分析、异常行为分析以及文件完整性等方式来做检查,来判断我们的操作系统是否有受到入侵。今天阿祥就介绍十个简单的入侵检查思路及应对措施,希望对大家有…

基于JavaWeb开发的java springmvc+mybatis学生考试系统设计和实现

基于JavaWeb开发的java springmvcmybatis学生考试系统设计和实现 🍅 作者主页 网顺技术团队 🍅 欢迎点赞 👍 收藏 ⭐留言 📝 🍅 文末获取源码联系方式 📝 🍅 查看下方微信号获取联系方式 承接各…

45岁被裁员的程序员,何去何从?

在当今快速变化的技术行业,职业生涯的稳定性受到挑战。在45岁被裁员,对很多程序员来说,可能是一种惊慌失措的体验。然而,这个阶段也可以被视为一个重新审视和调整方向的机会。本文将对可能的出路进行全方位的分析,并提…

音视频入门基础:FLV专题(9)——Script Tag简介

一、SCRIPTDATA 根据《video_file_format_spec_v10_1.pdf》第75页到76页,如果某个Tag的Tag header中的TagType值为18,表示该Tag为Script Tag(脚本Tag,又称Data Tag、SCRIPTDATA tag)。这时如果Filter的值不为1表示未加…

蓝桥杯【物联网】零基础到国奖之路:十五. 扩展模块之双路ADC

蓝桥杯【物联网】零基础到国奖之路:十五. 扩展模块之双路ADC 第一节 硬件解读第二节 CubeMX配置第三节 代码编写 第一节 硬件解读 STM32的ADC是12位,通过硬件过采样扩展到16位,模数转换器嵌入到STM32L071xx器件中。有16个外部通道和2个内部通道&#xf…

Docker学习和部署ry项目

文章目录 停止Docker重启设置开机自启执行docker ps命令,如果不报错,说明安装启动成功2.然后查看数据卷结果3.查看数据卷详情结果4.查看/var/lib/docker/volumes/html/_data目录可以看到与nginx的html目录内容一样,结果如下:5.进入…

Flink源码剖析

写在前面 最近一段时间都没有更新博客了,原因有点离谱,在实现flink的两阶段提交的时候,每次执行自定义的notifyCheckpointComplete时候,好像就会停止消费数据,完成notifyComplete后再消费数据;基于上述原因…

kubernetes 中的微服务

微服务:用控制器来完成集群的工作负载,那么应用如何暴漏出去?需要通过微服务暴漏出去后才能被访问 - Service是一组提供相同服务的Pod对外开放的接口。 - 借助Service,应用可以实现服务发现和负载均衡。 - service默认只支持…

带隙基准Bandgap电路学习(一)

一、原理图 Bandgap中的运放(折叠式Cascode)采用P输入对,是因为运放输入端接的PNP三极管发射极端的电位,电压小,为了确保输入对管能够饱和工作,故采用P输入对管。此外,P管作为输入管&#xff0c…

【HTTPS】深入解析 https

我的主页:2的n次方_ 1. 背景介绍 在使用 http 协议的时候是不安全的,可能会出现运营商劫持等安全问题,运营商通过劫持 http 流量,篡改返回的网页内容,例如广告业务,可能会通过 Referer 字段 来统计是…

springboot医院预约挂号系统

基于springbootvue实现的医院预约挂号系统 (源码L文ppt)4-085 4.1系统功能模块设计 医院预约挂号系统与数据分析系统在设计与实施时,采取了模块性的设计理念,把相似的系统的功能整合到一个模组中,以增强内部的功能…

【MySQL】DML数据操作语句和基本的DQL语句

目录 一、Mysql对数据的增删改 1. 增加数据 2. 修改数据(UPDATE语句) 3. 删除 3.1 delete、truncate、drop区别 二、DQL语言(重点) 1. 单表查询 1.1 最简单的查询 1.2 从表中获取数据 1.3 字段名起别名 1.4 添加字段 1…

计算机毕业设计 玩具租赁系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍:✌从事软件开发10年之余,专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ 🍅文末获取源码联系🍅 👇🏻 精…

Java—逻辑控制与输入输出

各位看官:如果您觉得这篇文章对您有帮助的话 欢迎您分享给更多人哦 感谢大家的点赞收藏评论,感谢您的支持!!! 一.顺序结构: 我每天起床,躺在床上玩手机,然后吃中午饭,睡…

应用UX体验标准

1、应用导航 标准编号 2.1.1.1 系统返回 标准描述 所有界面都可以执行系统返回操作。 除一级界面外,所有全屏界面均需要提供返回/关闭/取消按钮。(全屏沉浸式场景除外) 测试方法 使用侧边返回手势,验证当前应用界面是否可以执行系统返回操作。检查…

一个为分布式环境设计的任务调度与重试平台,高灵活高效率,系统安全便捷,分布式重试杀器!(附源码)

背景 近日挖掘到一款名为“SnailJob”的分布式重试开源项目,它旨在解决微服务架构中常见的重试问题。在微服务大行其道的今天,我们经常需要对某个数据请求进行多次尝试。然而,当遇到网络不稳定、外部服务更新或下游服务负载过高等情况时,请求…