当前位置: 首页 > news >正文

设计模式(行为型)-访问者模式

定义

        访问者模式的定义为:封装某些作用于某种数据结构中各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。其核心思想在于将数据结构和对数据结构的操作分离,使得对数据结构的操作可以独立变化,从而提高系统的可维护性和可扩展性。

        举个生活中的例子,假设有一个动物园,里面有各种动物,如老虎、狮子、大象等。不同的游客来到动物园,可能有不同的目的,有的游客想要观察动物的习性,有的游客想要给动物拍照。在这个场景中,动物园里的动物就相当于数据结构中的元素,而游客就是访问者,游客对动物的不同操作就是访问者模式中定义的操作。通过访问者模式,我们可以轻松地添加新的游客(访问者),实现新的操作,而无需对动物(数据结构)本身进行修改。

类图

角色

        访问者模式包含以下几个重要角色,它们相互协作,共同完成数据结构与操作的解耦:

  • 抽象访问者(Visitor):抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是 visit 方法中的参数定义哪些对象是可以被访问的。它为所有具体访问者提供了统一的接口,使得不同的访问者可以以相同的方式访问元素。

  • 访问者(ConcreteVisitor):实现抽象访问者所声明的方法,它影响到访问者访问到一个类后该干什么,要做什么事情。每个具体访问者都针对不同的元素类型实现特定的操作,体现了访问者模式的灵活性。

  • 抽象元素类(Element):接口或者抽象类,声明接受哪一类访问者访问,程序上是通过 accept 方法中的参数来定义的。抽象元素一般有两类方法,一部分是本身的业务逻辑,另外就是允许接受哪类访问者来访问。它定义了元素与访问者交互的方式。

  • 元素类(ConcreteElement):实现抽象类所声明的 accept 方法,通常都是 visitor.visit (this),基本上已经形成一种定式了。在具体元素类中,通过调用访问者的 visit 方法,将自身传递给访问者,从而让访问者对自己进行操作。

  • 结构对象(ObjectStructure):一个元素的容器,一般包含一个容纳多个不同类、不同接口的容器,如 List、Set、Map 等,在项目中一般很少抽象出这个角色。它负责管理元素,并提供遍历元素的方法,以便访问者对元素进行操作。

优缺点

优点

  • 数据结构与操作解耦:使得数据结构和作用于结构上的操作解偶,使得操作集合可以独立变化。当数据结构保持稳定,而操作频繁变化时,访问者模式能够显著降低代码的耦合度,提高系统的可维护性。

  • 新增操作便捷:添加新的操作或者说访问者会非常容易。只需要创建一个新的具体访问者类,实现抽象访问者接口中的方法,即可实现新的操作,无需修改数据结构相关的代码。

  • 操作集中管理:将对各个元素的一组操作集中在一个访问者类当中,便于对操作进行统一管理和维护。这种集中式的设计使得代码结构更加清晰,易于理解和修改。

  • 类层次结构稳定:使得类层次结构不改变的情况下,可以针对各个层次做出不同的操作,而不影响类层次结构的完整性。即使在复杂的类层次结构中,也能通过访问者模式灵活地添加新操作,保持系统的稳定性。

  • 跨层次操作支持:可以跨越类层次结构,访问不同层次的元素类,做出相应的操作。这为处理复杂的数据结构提供了强大的能力,能够满足多样化的业务需求。

  • 算法扩展容易:使得给结构稳定的对象增加新算法变得容易,提高了代码的可维护性和可拓展性。通过新增访问者类,开发者可以快速实现新的算法逻辑,而无需对现有数据结构进行大规模修改。

缺点

  • 新增元素困难:增加新的元素会非常困难。因为在访问者类中,每个具体访问者都需要针对已有的元素类型实现相应的操作方法。当新增元素时,需要修改所有相关的访问者类,这会增加代码的维护成本和出错风险。

  • 实现复杂度高:实现起来比较复杂,会增加系统的复杂性。访问者模式涉及多个角色和类之间的交互,需要开发者深入理解其设计思想和实现机制,否则容易导致代码混乱,难以调试和维护。

  • 破坏封装性:破坏封装,如果将访问行为放在各个元素中,则可以不暴露元素的内部结构和状态,但使用访问者模式的时候,为了让访问者能获取到所关心的信息,元素类不得不暴露出一些内部的状态和结构。这违背了面向对象编程的封装原则,可能会增加系统的安全风险。

代码示例

        为了更直观地理解访问者模式,我们以一个简单的图形绘制系统为例进行代码实现。在这个系统中,有不同类型的图形,如圆形和正方形,我们希望能够对这些图形进行不同的操作,如计算面积和周长。

  • 首先,定义抽象元素类Shape
public abstract class Shape {public abstract void accept(ShapeVisitor visitor);
}
  • 然后,定义具体元素类CircleSquare
public class Circle extends Shape {private double radius;public Circle(double radius) {this.radius = radius;}public double getRadius() {return radius;}@Overridepublic void accept(ShapeVisitor visitor) {visitor.visit(this);}
}public class Square extends Shape {private double sideLength;public Square(double sideLength) {this.sideLength = sideLength;}public double getSideLength() {return sideLength;}@Overridepublic void accept(ShapeVisitor visitor) {visitor.visit(this);}
}
  • 接着,定义抽象访问者接口ShapeVisitor

public interface ShapeVisitor {void visit(Circle circle);void visit(Square square);
}
  • 再定义具体访问者类AreaCalculatorPerimeterCalculator

public class AreaCalculator implements ShapeVisitor {@Overridepublic void visit(Circle circle) {double area = Math.PI * circle.getRadius() * circle.getRadius();System.out.println("Circle area: " + area);}@Overridepublic void visit(Square square) {double area = square.getSideLength() * square.getSideLength();System.out.println("Square area: " + area);}
}public class PerimeterCalculator implements ShapeVisitor {@Overridepublic void visit(Circle circle) {double perimeter = 2 * Math.PI * circle.getRadius();System.out.println("Circle perimeter: " + perimeter);}@Overridepublic void visit(Square square) {double perimeter = 4 * square.getSideLength();System.out.println("Square perimeter: " + perimeter);}
}
  • 最后,定义结构对象类ShapeContainer
import java.util.ArrayList;
import java.util.List;public class ShapeContainer {private List<Shape> shapes = new ArrayList<>();public void addShape(Shape shape) {shapes.add(shape);}public void accept(ShapeVisitor visitor) {for (Shape shape : shapes) {shape.accept(visitor);}}
}
  • 在客户端代码中,我们可以这样使用访问者模式:
public class Main {public static void main(String[] args) {ShapeContainer container = new ShapeContainer();container.addShape(new Circle(5));container.addShape(new Square(4));ShapeVisitor areaCalculator = new AreaCalculator();ShapeVisitor perimeterCalculator = new PerimeterCalculator();container.accept(areaCalculator);container.accept(perimeterCalculator);}
}

        运行上述代码,将输出各个图形的面积和周长,展示了访问者模式如何在不改变图形数据结构的情况下,实现不同的操作。

应用场景

  • 对象结构稳定,操作多变:当对象结构比较稳定,但是需要在对象结构的基础上定义新的操作时,访问者模式是一个理想的选择。例如,在一个文档处理系统中,文档的结构(如段落、标题、图片等)相对稳定,但用户可能需要对文档进行不同的操作,如打印、统计字数、检查拼写等。通过访问者模式,可以轻松地添加新的操作,而无需修改文档的结构。

  • 同一类对象不同操作:需要对同一个类的不同对象执行不同的操作,但是不希望增加操作的时候改变这些类。比如在一个图形编辑软件中,对于不同的图形对象(如圆形、矩形、三角形),可能需要执行不同的绘制、旋转、缩放等操作。使用访问者模式,可以将这些操作封装在不同的访问者类中,避免在图形类中添加过多的操作方法,保持类的简洁性。

  • 分离元素职责:当一个数据结构中,一些元素类需要负责与其不相关的操作的时候,为了将这些操作分离出去,以减少这些元素类的职责时,可以使用访问者模式。例如,在一个电子商务系统中,商品类主要负责存储商品的基本信息,如名称、价格、库存等。而对商品的操作,如计算折扣、统计销量等,可以通过访问者模式将这些操作分离到不同的访问者类中,使商品类专注于自身的核心职责。

  • 消除类型判断:有时在对数据结构上的元素进行操作的时候,需要区分具体的类型,这时候使用访问者模式可以针对不同的类型,在访问者类中定义不同的操作,从而去除掉类型判断。例如,在一个编译器中,对于不同的语法树节点(如表达式节点、语句节点、声明节点等),需要执行不同的语义分析操作。通过访问者模式,可以将这些操作封装在不同的访问者类中,避免在语义分析代码中出现大量的类型判断语句,提高代码的可读性和可维护性。

访问者模式在实际框架中的应用

JDK 的 NIO 模块下的 FileVisitor 接口

        在 JDK 的 NIO 模块中,FileVisitor接口是访问者模式的一个典型应用。FileVisitor接口定义了四个方法,分别用于访问文件或目录前、访问文件或目录后、访问失败时以及访问结束时的操作。通过实现FileVisitor接口,开发者可以自定义对文件系统的遍历和操作逻辑。例如,要实现一个文件搜索功能,只需要创建一个实现FileVisitor接口的类,在visitFile方法中判断文件是否符合搜索条件,即可在不修改文件系统数据结构的前提下,实现文件搜索操作。

Spring IoC 中的 BeanDefinitionVisitor

        在 Spring IoC 容器中,BeanDefinitionVisitor类也运用了访问者模式。BeanDefinition表示 Spring 容器中的 Bean 定义,BeanDefinitionVisitor用于对BeanDefinition进行各种操作,如解析、修改等。通过BeanDefinitionVisitor,Spring 能够在不改变BeanDefinition结构的情况下,灵活地添加新的操作逻辑,实现对 Bean 定义的动态处理,增强了 Spring 框架的扩展性和灵活性。

总结与注意事项

        访问者模式作为一种强大的设计模式,通过将数据结构与操作分离,为软件开发带来了诸多优势,特别是在对象结构稳定、操作多变的场景下,能够显著提高代码的可维护性和可扩展性。然而,它也存在一些缺点,如新增元素困难、实现复杂、破坏封装等。因此,在使用访问者模式时,开发者需要准确识别其适用场景,确保对象结构相对稳定,且主要需求是对数据结构进行多样化的操作。

        同时,在实际应用中,还需要注意以下几点:

  1. 确保对象结构中的元素可以迭代访问,以便访问者能够对每个元素进行操作。

  2. 在抽象访问者接口中,为每个具体元素类型定义相应的 visit 方法,确保访问者能够处理所有可能的元素类型。

  3. 在具体元素类的 accept 方法中,遵循visitor.visit(this)的定式,将自身传递给访问者,实现元素与访问者的交互。

  4. 谨慎处理元素类的封装性问题,尽量减少因访问者模式导致的内部结构暴露,确保系统的安全性和稳定性。

http://www.dtcms.com/a/264142.html

相关文章:

  • python训练day46 通道注意力
  • 【kernel8】spi协议,验证,模型,设备树处理,spidev,衍生协议
  • AI人工客服实战指南:基于大模型构建生产级智能对话系统
  • Hadoop、Spark、Flink 三大大数据处理框架的能力与应用场景
  • ESP32-S3开发板深度评测:AI语音识别与图像处理全面解析
  • C++ 第四阶段 STL 容器 - 第九讲:详解 std::map 与 std::unordered_map —— 关联容器的深度解析
  • Springboot整合高德地图
  • NeurIPS-2023《A Definition of Continual Reinforcement Learning》
  • 基于GD32 MCU的IAP差分升级方案
  • 迎战 AI Overviews:SEO 不被淘汰的实战策略
  • SpringBoot全局异常详解
  • Electron 应用打包与分发:从开发到交付的完整指南
  • 多容器应用与编排——AI教你学Docker
  • Java-String类静态成员方法深度解析
  • AR 地产互动沙盘:为地产沙盘带来变革​
  • OpenCV-Python Tutorial : A Candy from Official Main Page(二)
  • 设备管理的重要性:企业数字化浪潮下的核心命题
  • 企业上网行为管理:零信任安全产品的对比分析
  • Linux基本命令篇 —— grep命令
  • 防 XSS和CSRF 过滤器(Filter)
  • go语言安装达梦数据完整教程
  • JVM 中的垃圾回收算法及垃圾回收器详解
  • 【仿muduo库实现并发服务器】Connection模块
  • CentOS 8中 更新或下载时报错:为仓库 ‘appstream‘ 下载元数据失败 : Cannot prepare internal
  • 02.SpringBoot常用Utils工具类详解
  • 从马赛克到色彩错乱:一次前景图像处理异常的全流程踩坑记录
  • Python实例题:基于 Python 的简单爬虫与数据可视化
  • 【IP 潮玩行业深度研究与学习】
  • 【仿muduo库实现并发服务器】eventloop模块
  • 香橙派3B学习笔记14:deb 打包程序_解包前后脚本运行