设计模式 行为型 访问者模式(Visitor Pattern)与 常见技术框架应用 解析

访问者模式(Visitor Pattern)是一种行为设计模式,它允许你在不改变元素类的前提下定义作用于这些元素的新操作。这种模式将算法与对象结构分离,使得可以独立地变化那些保存在复杂对象结构中的元素的操作。

假设我们有一个复杂的对象结构,例如一个包含多种图形(圆形、矩形、三角形)的绘图系统。我们可能需要对这些图形进行多种操作,如计算面积、绘制轮廓、计算周长等。如果将这些操作的代码都放在图形类中,会使图形类变得非常臃肿。访问者模式就像是一个外来的"访问者",它可以独立于图形类定义这些操作,然后在需要的时候"访问"图形并执行相应的操作。

一、核心思想

核心思想是将算法从对象的结构中分离出来,封装在独立的访问者对象中。这样一来,就可以在不修改对象结构的情况下,为该结构中的元素添加新的操作或行为。

二、定义与结构

  • 定义:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
  • 结构
    • 访问者(Visitor):抽象类或者接口,定义了对每个具体元素访问的操作方法。
    • 具体访问者(ConcreteVisitor):实现访问者接口,实现对具体元素的操作。
    • 元素(Element) :抽象类或者接口,定义了一个接受访问者的方法(accept方法)。
    • 具体元素(ConcreteElement) :实现元素接口,在accept方法中调用访问者对应的操作方法。
    • 对象结构(ObjectStructure):包含可以被访问的元素集合,提供方法让访问者访问它的元素。

三、角色

  • 访问者(Visitor)
    • 这是一个抽象角色,用于声明访问具体元素的方法。例如,在图形系统中,它可能有visitCirclevisitRectangle等方法,这些方法的参数通常是对应的具体元素。它定义了对不同类型元素进行操作的统一接口。
  • 具体访问者(ConcreteVisitor)
    • 实现了访问者接口。它为每个访问方法提供了具体的实现,这些实现包含了针对具体元素的实际操作逻辑。比如,一个计算图形面积的具体访问者,会在visitCircle方法中实现计算圆形面积的逻辑,在visitRectangle方法中实现计算矩形面积的逻辑等。
  • 元素(Element)
    • 抽象元素角色,通常是一个抽象类或接口,定义了accept方法。这个方法接受一个访问者对象作为参数,用于将当前元素自身传递给访问者,以便访问者执行相应的操作。
  • 具体元素(ConcreteElement)
    • 实现了抽象元素角色定义的接口或抽象类。在accept方法中,它会调用访问者的相应方法,并将自身作为参数传递进去。例如,圆形类(Circle)作为具体元素,在其accept方法中会调用访问者的visitCircle方法,并把自己(圆形对象)传递给访问者。
  • 对象结构(ObjectStructure)
    • 这个角色用于管理和存储元素对象。它提供了方法来遍历元素集合,让访问者能够访问其中的每个元素。比如,在绘图系统中,对象结构可能是一个包含所有图形的列表,它有一个方法可以遍历这个列表,然后让访问者访问每个图形。

四、实现步骤及代码示例

  • 步骤一:定义访问者接口和具体访问者类
java 复制代码
// 访问者接口
interface Visitor {
    void visitCircle(Circle circle);
    void visitRectangle(Rectangle rectangle);
}
// 具体访问者类(计算面积)
class AreaCalculatorVisitor implements Visitor {
    @Override
    public void visitCircle(Circle circle) {
        double area = Math.PI * circle.getRadius() * circle.getRadius();
        System.out.println("圆形面积:" + area);
    }
    @Override
    public void visitRectangle(Rectangle rectangle) {
        double area = rectangle.getWidth() * rectangle.getHeight();
        System.out.println("矩形面积:" + area);
    }
}
  • 步骤二:定义元素接口和具体元素类
java 复制代码
// 元素接口
interface Shape {
    void accept(Visitor visitor);
}
// 具体元素类(圆形)
class Circle implements Shape {
    private double radius;
    public Circle(double radius) {
        this.radius = radius;
    }
    public double getRadius() {
        return radius;
    }
    @Override
    public void accept(Visitor visitor) {
        visitor.visitCircle(this);
    }
}
// 具体元素类(矩形)
class Rectangle implements Shape {
    private double width;
    private double height;
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    public double getWidth() {
        return width;
    }
    public double getHeight() {
        return height;
    }
    @Override
    public void accept(Visitor visitor) {
        visitor.visitRectangle(this);
    }
}
  • 步骤三:定义对象结构类并使用访问者模式
java 复制代码
import java.util.ArrayList;
import java.util.List;
// 对象结构类
class ShapeList {
    private List<Shape> shapes = new ArrayList<>();
    public void addShape(Shape shape) {
        shapes.add(shape);
    }
    public void accept(Visitor visitor) {
        for (Shape shape : shapes) {
            shape.accept(visitor);
        }
    }
}
// 主程序测试
public class Main {
    public static void main(String[] args) {
        ShapeList shapeList = new ShapeList();
        shapeList.addShape(new Circle(3.0));
        shapeList.addShape(new Rectangle(4.0, 5.0));
        Visitor areaCalculator = new AreaCalculatorVisitor();
        shapeList.accept(areaCalculator);
    }
}
  • 在上述代码中,Visitor是访问者接口,AreaCalculatorVisitor是具体访问者,用于计算图形面积。Shape是元素接口,CircleRectangle是具体元素。ShapeList是对象结构,用于管理图形对象。在main方法中,我们创建了图形对象列表,添加了圆形和矩形对象,然后创建了面积计算访问者,并让对象结构接受访问者来计算每个图形的面积。

五、常见技术框架应用

1、编译器中的语法树遍历

  • 在编译器开发中,语法树是一个复杂的对象结构。访问者模式可以用于遍历语法树并执行语义分析或代码生成等操作。
  • 步骤一:定义语法树节点(元素)接口和具体节点类
java 复制代码
// 语法树节点接口
interface ASTNode {
    void accept(ASTVisitor visitor);
}
// 具体节点类(表达式节点)
class ExpressionNode implements ASTNode {
    // 表达式内容等属性
    @Override
    public void accept(ASTVisitor visitor) {
        visitor.visitExpressionNode(this);
    }
}
// 具体节点类(语句节点)
class StatementNode implements ASTNode {
    // 语句内容等属性
    @Override
    public void accept(ASTVisitor visitor) {
        visitor.visitStatementNode(this);
    }
}
  • 步骤二:定义访问者接口和具体访问者类(语义分析访问者)
java 复制代码
// 访问者接口
interface ASTVisitor {
    void visitExpressionNode(ExpressionNode node);
    void visitStatementNode(StatementNode node);
}
// 具体访问者类(语义分析)
class SemanticAnalysisVisitor implements ASTVisitor {
    @Override
    public void visitExpressionNode(ExpressionNode node) {
        // 进行表达式语义分析,如检查操作数类型等
    }
    @Override
    public void visitStatementNode(StatementNode node) {
        // 进行语句语义分析,如检查变量声明等
    }
}
  • 步骤三:定义语法树(对象结构)类并使用访问者模式进行遍历
java 复制代码
import java.util.ArrayList;
import java.util.List;
// 语法树类
class SyntaxTree {
    private List<ASTNode> nodes = new ArrayList<>();
    public void addNode(ASTNode node) {
        nodes.add(node);
    }
    public void accept(ASTVisitor visitor) {
        for (ASTNode node : nodes) {
            node.accept(visitor);
        }
    }
}
// 主程序测试(假设在编译器的某个阶段)
public class Compiler {
    public static void main(String[] args) {
        SyntaxTree syntaxTree = new SyntaxTree();
        ASTNode expressionNode = new ExpressionNode();
        ASTNode statementNode = new StatementNode();
        syntaxTree.addNode(expressionNode);
        syntaxTree.addNode(statementNode);
        ASTVisitor semanticAnalysisVisitor = new SemanticAnalysisVisitor();
        syntaxTree.accept(semanticAnalysisVisitor);
    }
}
  • 这里的语法树由各种语法节点组成,通过访问者模式,语义分析访问者可以遍历语法树并对每个节点进行语义分析,而不需要将语义分析代码嵌入到语法树节点的内部。

2、前端框架中的访问者模式示例

以下是一些前端框架中访问者模式应用的具体例子:

处理复杂UI组件树

假设我们有一个复杂的用户界面,它由各种不同的UI组件构成,比如按钮、输入框、下拉菜单等。每个组件可能都需要执行特定的操作,例如验证、序列化或渲染。我们可以定义一个Visitor来封装这些操作,而不是将所有逻辑都放在组件内部,这样可以使代码更加模块化和易于维护。

javascript 复制代码
// 抽象访问者 (Abstract Visitor)
class UIComponentVisitor {
    visitButton(button) { }
    visitInput(input) { }
    visitDropdown(dropdown) { }
}

// 具体访问者 (Concrete Visitors)
class ValidatorVisitor extends UIComponentVisitor {
    visitButton(button) {
        // 验证按钮的逻辑
        console.log('Validating button:', button);
    }

    visitInput(input) {
        if (!input.value) {
            console.error('Input is required:', input);
        } else {
            console.log('Input validated:', input);
        }
    }

    visitDropdown(dropdown) {
        // 验证下拉菜单的逻辑
        console.log('Validating dropdown:', dropdown);
    }
}

class SerializerVisitor extends UIComponentVisitor {
    visitButton(button) {
        // 序列化按钮的状态
        console.log('Serializing button:', button);
    }

    visitInput(input) {
        // 序列化输入框的值
        console.log('Serializing input value:', input.value);
    }

    visitDropdown(dropdown) {
        // 序列化下拉菜单的选择
        console.log('Serializing dropdown selection:', dropdown.selectedOptions);
    }
}

// 抽象元素 (Abstract Element)
class UIComponent {
    constructor() {
        this.name = 'UIComponent';
    }

    accept(visitor) {
        visitor[`visit${this.constructor.name}`](this);
    }
}

// 具体元素 (Concrete Elements)
class Button extends UIComponent {
    constructor(text) {
        super();
        this.text = text;
        this.name = 'Button';
    }
}

class Input extends UIComponent {
    constructor(value) {
        super();
        this.value = value;
        this.name = 'Input';
    }
}

class Dropdown extends UIComponent {
    constructor(options, selectedOptions) {
        super();
        this.options = options;
        this.selectedOptions = selectedOptions;
        this.name = 'Dropdown';
    }
}

// 使用访问者模式
const components = [new Button('Submit'), new Input('Some text'), new Dropdown(['Option1', 'Option2'], ['Option1'])];

const validator = new ValidatorVisitor();
const serializer = new SerializerVisitor();

components.forEach(component => component.accept(validator));
console.log('\n');
components.forEach(component => component.accept(serializer));

在这个例子中,ValidatorVisitorSerializerVisitor是具体的访问者,它们实现了针对不同类型UI组件的不同行为。UIComponent及其子类作为具体元素,提供了accept方法以接受访问者的访问。这种方式使得我们可以很容易地添加新的操作类型,而无需修改现有的UI组件类。

React 中的虚拟 DOM 遍历与更新(间接体现访问者模式思想)

  • 背景
    在 React 中,虚拟 DOM(Virtual DOM)是一个关键概念。它是真实 DOM 结构在 JavaScript 对象层面的一种表示,通过对比虚拟 DOM 的前后状态变化来决定如何高效地更新真实 DOM。
  • 分析
    • 元素(类似访问者模式中的元素角色) :React 组件对应的虚拟 DOM 节点可以看作是元素。例如,一个简单的 <div> 组件在虚拟 DOM 里会被表示成一个包含各种属性(如 propschildren 等)的 JavaScript 对象,它定义了 accept(这里没有显式的 accept 方法名,但有类似机制)操作,也就是允许 React 的更新机制来处理它。
    • 访问者(类似访问者模式中的访问者角色) :React 的 Diffing 算法(用于对比新旧虚拟 DOM 差异的机制)以及后续的 Reconciliation 过程(协调更新的过程)可以看作是访问者。Diffing 算法会遍历虚拟 DOM 树(从根节点开始),访问每个虚拟 DOM 节点(元素),去判断节点的类型、属性以及子节点等是否发生了变化,这个过程就类似访问者对不同元素进行访问并执行相应操作。例如,当检测到一个 <div> 组件的 props 发生了改变,访问者(更新机制)就会执行对应的更新真实 DOM 中该 <div> 对应部分的操作,比如更新 style 属性或者 innerHTML 等。
    • 具体过程示例
      假如有一个简单的 React 组件结构如下:
jsx 复制代码
function App() {
    return (
        <div className="app">
            <h1>Hello</h1>
            <p>World</p>
        </div>
    );
}

当组件的状态发生改变(比如 className 变为 app-new),React 会重新构建新的虚拟 DOM 树,然后通过 Diffing 算法这个"访问者"去遍历新旧虚拟 DOM 树的各个节点(元素),对比发现 <div> 节点的 className 属性变化了,就会执行相应的更新真实 DOM 中对应 <div> 元素的操作,将 class 属性更新为 app-new

Vue.js 的模板编译与指令解析(体现访问者模式思路)

  • 背景
    Vue.js 在将模板(template)编译成渲染函数(render 函数)以及解析模板中的指令(如 v-ifv-for 等)时运用了类似访问者模式的思路。
  • 分析
    • 元素(类似访问者模式中的元素角色) :模板中的 HTML 标签以及文本节点等可以看作是元素。例如 <div v-if="show">{``{ message }}</div> 中的 <div> 标签、文本插值 {``{ message }} 等,它们构成了整个模板这个"数据结构",并且每个元素都能被访问处理。
    • 访问者(类似访问者模式中的访问者角色) :Vue 的模板编译器就是访问者。它会对模板这个元素集合进行遍历,解析每个元素。比如,对于指令类的元素,像遇到 v-if 指令时,访问者(编译器)会解析出条件判断逻辑,根据对应的数据(show 变量的值)来决定是否生成该 <div> 元素的渲染代码;对于文本插值元素 {``{ message }},访问者会解析出需要将对应的数据(message 值)渲染到此处的操作。
    • 具体过程示例
      假设我们有如下 Vue 模板:
html 复制代码
<template>
    <div>
        <p v-if="isShow">This is visible</p>
        <p v-else>This is hidden</p>
        <span>{{ greeting }}</span>
    </div>
</template>

当 Vue 进行模板编译时,编译器这个"访问者"会遍历整个模板的各个元素。对于带有 v-if 指令的 <p> 元素,它会检查 isShow 数据的值,如果为 true,则生成将 This is visible 渲染到页面的相关代码逻辑;对于文本插值的 <span> 元素,编译器会根据 greeting 变量的值,生成将其正确渲染到对应位置的代码,从而实现模板到渲染函数的转换,后续基于渲染函数就能更新页面 DOM 了。

Ember.js 的渲染系统(部分体现访问者模式)

  • 背景
    Ember.js 有一套自己的渲染机制,用于根据定义的模板和组件来生成和更新页面内容。
  • 分析
    • 元素(类似访问者模式中的元素角色):在 Ember.js 中,组件对应的模板内容、DOM 元素等可以视为元素。例如,一个自定义的组件模板里包含的各种 HTML 标签、绑定的数据等组成了要处理的元素集合。
    • 访问者(类似访问者模式中的访问者角色) :Ember.js 的渲染引擎和相关的更新机制充当访问者。渲染引擎会遍历组件的模板元素,比如遇到绑定数据的地方(类似 {``{someProperty}}),访问者(渲染引擎)会去获取对应的数据值,并将其正确渲染到 DOM 中;当组件状态变化触发更新时,更新机制同样会访问各个元素,判断哪些元素需要重新渲染,然后执行相应的更新操作。
    • 具体过程示例
      假设有一个 Ember.js 组件的模板如下:
handlebars 复制代码
{{! my-component.hbs }}
<h2>{{title}}</h2>
<p>Description: {{description}}</p>

当 Ember.js 渲染这个组件时,渲染引擎这个"访问者"会访问模板中的每个元素,对于 {``{title}}{``{description}} 这些绑定数据的元素,会从组件对应的 JavaScript 对象(包含 titledescription 等属性)中获取相应的值,然后将其渲染到对应的 HTML 标签内,生成最终的 DOM 结构展示给用户。当组件的 titledescription 属性值发生变化时,更新机制又会作为访问者再次访问这些元素,重新获取新值并更新 DOM 展示。

总之,在前端框架中,访问者模式(或其类似思路)常被用于高效地处理页面元素的渲染、更新以及对模板、DOM 相关结构的操作,使得代码结构更清晰,便于扩展和维护不同的功能逻辑。

六、应用场景

bash 复制代码
数据结构稳定,但作用于数据结构的操作经常变化的场景。
需要将数据结构与数据操作分离的场景。
需要对不同数据类型进行操作,而不适用分支判断具体类型的场景。
元素具体类型并非单一,访问者均可操作的场景。
  1. 数据结构的操作分离:当有一个复杂的数据结构(如树形结构、图形系统中的图形集合等),并且需要对这个数据结构执行多种不同的操作(如计算、打印、转换等)时,访问者模式可以将操作代码从数据结构类中分离出来,使得数据结构的定义更加清晰,操作的扩展更加容易。
  2. 编译器设计:如前面提到的语法树遍历,用于语义分析、中间代码生成、代码优化等阶段。不同的编译器阶段可以定义不同的访问者来对语法树进行操作。
  3. XML文档处理:可以将XML文档看作是一个树形的数据结构,访问者模式可以用于对XML元素进行不同的操作,如验证、转换、提取信息等。
  4. 编译器构建:在编译器中,不同的节点类型(如表达式、语句等)可以通过访问者模式进行不同的处理,如语法检查、代码生成等。
  5. 文档处理:在文档处理系统中,不同的文档元素(如文本、图片、表格等)可以通过访问者模式进行不同的处理,如渲染、统计字数等。
  6. 图形界面工具:在图形界面工具中,不同的UI组件(如按钮、文本框、菜单等)可以通过访问者模式进行不同的操作,如绘制、事件处理等。
  7. 数据分析:在数据分析系统中,不同的数据结构(如树、图、表等)可以通过访问者模式进行不同的分析操作,如计算总和、平均值等。
  8. 游戏开发:在游戏开发中,不同的游戏对象(如玩家、敌人、道具等)可以通过访问者模式进行不同的操作,如更新状态、渲染图像等。

七、优缺点

优点

  1. 分离操作和数据结构:使得数据结构的定义和操作的定义可以独立变化,提高了代码的可维护性和可扩展性。当需要添加新的操作时,只需要创建新的访问者类,而不需要修改数据结构类。
  2. 符合开闭原则:对于数据结构和操作的扩展是开放的,对于修改是封闭的。可以方便地添加新的元素(数据结构中的节点)和新的访问者(操作)。
  3. 增加代码的复用性:访问者类可以在多个不同的数据结构上复用,只要这些数据结构的元素接口是兼容的。

缺点

  1. 增加了代码的复杂性:访问者模式需要定义多个接口和类,包括访问者接口、具体访问者类、元素接口、具体元素类和对象结构类等,这使得代码结构相对复杂,对于简单的应用场景可能会增加不必要的复杂性。
  2. 违背了迪米特法则:因为访问者模式需要访问数据结构中的元素,可能会导致访问者和元素之间的耦合度过高,访问者需要知道元素的内部结构和接口细节,这在一定程度上违背了迪米特法则(最少知识原则)。
相关推荐
LTPP2 分钟前
自动化 Rust 开发的革命性工具:lombok-macros
前端·后端·github
一个热爱生活的普通人2 分钟前
Go语言中 Mutex 的实现原理
后端·go
Victor3562 分钟前
Dubbo(31)如何优化Dubbo的启动速度?
后端
qianmoq3 分钟前
轻松掌握Java多线程 - 第二章:线程的生命周期
java·后端
inxunoffice4 分钟前
批量给 PDF 添加页眉页脚以及页码信息
前端·javascript·pdf
Postkarte不想说话4 分钟前
FreeSWITCH与FreeSWITCH对接
后端
孔令飞5 分钟前
关于 LLMOPS 的一些粗浅思考
人工智能·云原生·go
小戴同学5 分钟前
实时系统降低延时的利器
后端·性能优化·go
风象南5 分钟前
Spring Boot 实现文件断点续传
java·spring boot·后端
Cache技术分享5 分钟前
36. Java 控制流语句 Break 语句
前端·后端