设计模式之访问者模式

设计模式之访问者模式详解(Visitor Pattern) 最复杂的设计模式,并且使用频率不高,《设计模式》的作者评价为:大多情况下,你不需要使用访问者模式,但是一旦需要使用它时,那就真的需要使用了。 访问者模式是一种将数据操作和数据结构分离的设计模式。(觉得太抽象,可以看下面的例子)。 模式的定义与特点 访问者(Visitor)模式的定义:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的... 阅读详情

设计模式之访问者模式

阅读指引:本文较长,可按需跳读。

  • 想快速理解 → 读「概念」「举例理解」「双重分派:访问者模式的灵魂」;
  • 想看代码 → 读「代码示例」(含 UML、Java、C++);
  • 想区分相似模式 → 读「访问者模式与策略模式、迭代器模式的对比」;
  • 想判断该不该用 → 读「优缺点」「一个反面例子」「应用场景」;
  • 准备落地 → 读「注意事项」与「访问者模式不负责什么」。

概念

定义

访问者模式(Visitor Pattern) 是一种行为型设计模式。它的核心思想是:

将作用于某种对象结构中各元素的操作分离出来,封装到一个独立的“访问者”对象中,使得在不修改各元素类的前提下,可以定义作用于这些元素的新操作。

换句话说:当你的对象结构(元素类型)稳定,但施加于其上的操作经常变化时,把“操作”从“元素”中请出去,让元素只负责接受访问,操作由外部的访问者来承载。

结构

访问者模式通常有五个角色:

  1. Visitor 访问者接口

    • 为对象结构中每一种具体元素类型声明一个 visit(ConcreteElement) 方法。
    • 方法个数通常等于元素类型的个数。
  2. ConcreteVisitor 具体访问者

    • 实现 Visitor 接口,为每种元素提供该操作的具体算法。
    • 例如:“薪资计算访问者”为工程师、产品经理、管理者分别计算薪资。
  3. Element 元素接口

    • 声明一个 accept(Visitor) 方法。
    • 这是访问者模式对元素类的唯一侵入点。
  4. ConcreteElement 具体元素

    • 实现 accept(Visitor visitor),通常只有一行:visitor.visit(this);。
    • 元素将自身作为参数回传给访问者,完成第二次分派。
  5. ObjectStructure 对象结构

    • 持有一组元素,负责遍历它们并将访问者传递给每个元素。
    • 可以是集合、树、图等任何聚合结构。

举例理解

假设你维护一个员工管理系统。员工类型很稳定,只有三种:工程师、产品经理、管理者。但业务部门不断提出新的统计需求:算工资、算绩效、算人力成本、算培训预算……

不用访问者模式时,直觉做法是把每个统计方法都加到员工类上:

abstract class Employee {
    abstract double computeSalary();
    abstract double computePerformance();
    abstract double computeCost();
    // 未来还会有 computeTrainingBudget()、computeReport()……
}

每来一个新统计需求,你就要同时改三个子类。统计逻辑散落在各个员工类里,员工类越来越臃肿,新人不敢动。

访问者模式的解法:

  • 员工类(Element)只保留与员工自身最相关的行为,外加一个 accept(Visitor) 方法。
  • 每一种统计操作(薪资、绩效、成本……)被封装成一个独立的 Visitor 实现。
  • 新增统计需求时,只新增一个 Visitor 类,工程师、产品经理、管理者三个元素类一行都不用改。

用一个更生活化的类比:

访问者模式就像不同身份的人去同一栋楼里办事。 楼里的房间(元素)是固定的:财务室、人事室、IT 室。但来访的人(访问者)不同,在每个房间里做的事也不同:审计员在财务室查账,在人事室查考勤;新员工在财务室领工资卡,在人事室办入职。房间本身不需要知道“谁会来、来干什么”,它只需要“开门,让访问者进来做事”。新增一种来访身份(新的 Visitor),楼里的房间结构完全不用动。

这个类比的关键点:元素结构稳定,操作方(访问者)多变——这正是访问者模式的适用前提。


双重分派:访问者模式的灵魂

如果只记住访问者模式“把操作抽出去”,而不理解 双重分派(Double Dispatch) ,就等于没真正理解它。

什么是单分派

在 Java、C++、C# 等主流面向对象语言中,方法调用通常是单分派的:运行时选择哪个方法,只取决于接收者的运行时类型(this 的类型),参数类型在编译期就已经静态确定。

考虑下面的场景:

abstract class Employee { }
class Engineer extends Employee { }
class Manager extends Employee { }

class Visitor {
    void visit(Engineer e) { System.out.println("处理工程师"); }
    void visit(Manager m) { System.out.println("处理管理者"); }
}

如果你写 Employee e = new Engineer(); visitor.visit(e);,Java 会在编译期根据参数的静态类型 Employee 来选择重载版本。如果 Visitor 中没有 visit(Employee),编译就无法通过;即使有,也不会根据 e 的实际类型 Engineer 去调用 visit(Engineer)。参数类型的运行时信息,在单分派的重载解析中不起作用。

访问者如何实现双重分派

访问者模式通过两次虚方法调用,把“元素类型”和“访问者类型”都纳入运行时决策:

第一次分派:element.accept(visitor)。accept 是元素接口的虚方法,实际调用哪个 accept,取决于元素的运行时类型。

第二次分派:在具体元素的 accept 内部,执行 visitor.visit(this)。注意这里的 this 是具体元素类型(因为 accept 已经落在具体类里了)。visit 是访问者接口的虚方法,实际调用哪个 visit,取决于访问者的运行时类型,同时参数类型已经由 this 确定为具体元素类型。

最终执行的 visit 方法,同时由“元素的运行时类型”和“访问者的运行时类型”共同决定。这就是双重分派。

用一段最小代码把这条链路看清楚:

interface Visitor {
    void visit(Engineer e);
    void visit(Manager m);
}

abstract class Employee {
    abstract void accept(Visitor v);
}

class Engineer extends Employee {
    void accept(Visitor v) {
        v.visit(this);   // this 的静态类型已是 Engineer,第二次分派命中 visit(Engineer)
    }
}

class Manager extends Employee {
    void accept(Visitor v) {
        v.visit(this);   // 命中 visit(Manager)
    }
}

当 Employee e = new Engineer(); e.accept(new SalaryVisitor()); 执行时:

  1. e.accept(...) 动态绑定到 Engineer.accept;
  2. Engineer.accept 中的 this 是 Engineer 类型,v.visit(this) 编译时即绑定到 visit(Engineer) 这个重载;
  3. v 的运行时类型是 SalaryVisitor,虚分派落到 SalaryVisitor.visit(Engineer)。

没有访问者模式,你需要 instanceof 或反射来达到同样的效果;访问者模式用纯多态替代了这些运行时类型检查。

一句话:单分派只问“你是谁”(接收者);双重分派同时问“你是谁”和“来访的是谁”。访问者模式是用两次单分派模拟双重分派的经典技巧。


工作流程

  1. 元素侧:每个具体元素实现 accept(Visitor visitor),方法体几乎总是 visitor.visit(this);。
  2. 访问者侧:每个具体访问者实现 Visitor 接口,为每一种元素类型提供自己的处理逻辑。
  3. 对象结构侧:遍历持有的元素,对每个元素调用 accept(visitor)。
  4. 双重分派发生:accept 的动态绑定根据元素运行时类型选中具体元素的 accept;其中的 visit(this) 再根据访问者运行时类型选中具体访问者的对应重载。
  5. 操作完成:访问者在 visit 方法中拿到元素对象,通过元素的公开接口读取所需数据,执行该访问者所代表的业务逻辑。

一句话概括:

元素开门(accept),访问者进来做事(visit);做什么由访问者决定,对谁做由元素决定。


代码示例

UML

以“员工统计”为例:元素是 Engineer、ProductManager、Manager;访问者是“薪资计算”和“成本分析”。

accept(visitor)

passes to elements

«interface»

Visitor

+visit(Engineer) : void

+visit(ProductManager) : void

+visit(Manager) : void

SalaryVisitor

+visit(Engineer) : void

+visit(ProductManager) : void

+visit(Manager) : void

CostVisitor

+visit(Engineer) : void

+visit(ProductManager) : void

+visit(Manager) : void

«abstract»

Employee

+accept(Visitor) : void

+getName() : String

+getBaseSalary() : double

Engineer

+accept(Visitor) : void

ProductManager

+accept(Visitor) : void

Manager

+accept(Visitor) : void

EmployeeStructure

-List<Employee> employees

+add(Employee) : void

+accept(Visitor) : void

角色类职责
VisitorVisitor为每种元素声明 visit 方法
ConcreteVisitorSalaryVisitor、CostVisitor薪资算法、成本算法
ElementEmployee声明 accept,提供读取数据的方法
ConcreteElementEngineer、ProductManager、Manager实现 accept,将自身交给访问者
ObjectStructureEmployeeStructure遍历员工,把访问者分发给每个元素

Java 实现

import java.util.ArrayList;
import java.util.List;

// ---------- 1. 访问者接口 ----------
interface Visitor {
    void visit(Engineer e);
    void visit(ProductManager p);
    void visit(Manager m);
}

// ---------- 2. 具体访问者:薪资计算 ----------
class SalaryVisitor implements Visitor {
    public void visit(Engineer e) {
        System.out.println(e.getName() + " 薪资:" + e.getBaseSalary() * 1.2);
    }
    public void visit(ProductManager p) {
        System.out.println(p.getName() + " 薪资:" + p.getBaseSalary() * 1.3);
    }
    public void visit(Manager m) {
        System.out.println(m.getName() + " 薪资:" + m.getBaseSalary() * 1.5);
    }
}

// ---------- 3. 具体访问者:成本分析 ----------
class CostVisitor implements Visitor {
    public void visit(Engineer e) {
        System.out.println(e.getName() + " 成本:" + e.getBaseSalary() * 1.8);
    }
    public void visit(ProductManager p) {
        System.out.println(p.getName() + " 成本:" + p.getBaseSalary() * 2.0);
    }
    public void visit(Manager m) {
        System.out.println(m.getName() + " 成本:" + m.getBaseSalary() * 2.5);
    }
}

// ---------- 4. 元素接口 ----------
abstract class Employee {
    private final String name;
    private final double baseSalary;

    Employee(String name, double baseSalary) {
        this.name = name;
        this.baseSalary = baseSalary;
    }

    public abstract void accept(Visitor visitor);

    public String getName() { return name; }
    public double getBaseSalary() { return baseSalary; }
}

// ---------- 5. 具体元素 ----------
class Engineer extends Employee {
    Engineer(String name, double base) { super(name, base); }
    @Override public void accept(Visitor v) { v.visit(this); }
}

class ProductManager extends Employee {
    ProductManager(String name, double base) { super(name, base); }
    @Override public void accept(Visitor v) { v.visit(this); }
}

class Manager extends Employee {
    Manager(String name, double base) { super(name, base); }
    @Override public void accept(Visitor v) { v.visit(this); }
}

// ---------- 6. 对象结构 ----------
class EmployeeStructure {
    private final List<Employee> employees = new ArrayList<>();

    void add(Employee e) { employees.add(e); }

    void accept(Visitor visitor) {
        for (Employee e : employees) {
            e.accept(visitor);
        }
    }
}

// ---------- 7. 客户端 ----------
public class VisitorDemo {
    public static void main(String[] args) {
        EmployeeStructure structure = new EmployeeStructure();
        structure.add(new Engineer("张三", 20000));
        structure.add(new ProductManager("李四", 25000));
        structure.add(new Manager("王五", 30000));

        System.out.println("===== 薪资统计 =====");
        structure.accept(new SalaryVisitor());

        System.out.println("\n===== 成本分析 =====");
        structure.accept(new CostVisitor());
    }
}

关键点:SalaryVisitor 和 CostVisitor 都没有修改 Engineer、ProductManager、Manager 的任何一行代码。新增“培训预算统计”,只需新增一个 TrainingBudgetVisitor,元素侧纹丝不动。

C++ 实现

#include <iostream>
#include <memory>
#include <string>
#include <vector>

class Engineer;
class ProductManager;
class Manager;

// 1. 访问者接口
class Visitor {
public:
    virtual ~Visitor() = default;
    virtual void visit(const Engineer& e) = 0;
    virtual void visit(const ProductManager& p) = 0;
    virtual void visit(const Manager& m) = 0;
};

// 2. 元素接口
class Employee {
public:
    Employee(std::string name, double base) : name_(std::move(name)), base_(base) {}
    virtual ~Employee() = default;

    virtual void accept(Visitor& visitor) const = 0;

    const std::string& name() const { return name_; }
    double baseSalary() const { return base_; }

private:
    std::string name_;
    double base_;
};

// 3. 具体元素
class Engineer : public Employee {
public:
    using Employee::Employee;
    void accept(Visitor& v) const override { v.visit(*this); }
};

class ProductManager : public Employee {
public:
    using Employee::Employee;
    void accept(Visitor& v) const override { v.visit(*this); }
};

class Manager : public Employee {
public:
    using Employee::Employee;
    void accept(Visitor& v) const override { v.visit(*this); }
};

// 4. 具体访问者
class SalaryVisitor : public Visitor {
public:
    void visit(const Engineer& e) override {
        std::cout << e.name() << " 薪资:" << e.baseSalary() * 1.2 << "\n";
    }
    void visit(const ProductManager& p) override {
        std::cout << p.name() << " 薪资:" << p.baseSalary() * 1.3 << "\n";
    }
    void visit(const Manager& m) override {
        std::cout << m.name() << " 薪资:" << m.baseSalary() * 1.5 << "\n";
    }
};

class CostVisitor : public Visitor {
public:
    void visit(const Engineer& e) override {
        std::cout << e.name() << " 成本:" << e.baseSalary() * 1.8 << "\n";
    }
    void visit(const ProductManager& p) override {
        std::cout << p.name() << " 成本:" << p.baseSalary() * 2.0 << "\n";
    }
    void visit(const Manager& m) override {
        std::cout << m.name() << " 成本:" << m.baseSalary() * 2.5 << "\n";
    }
};

// 5. 对象结构
class EmployeeStructure {
public:
    void add(std::unique_ptr<Employee> e) { employees_.push_back(std::move(e)); }

    void accept(Visitor& visitor) const {
        for (const auto& e : employees_) {
            e->accept(visitor);
        }
    }

private:
    std::vector<std::unique_ptr<Employee>> employees_;
};

int main() {
    EmployeeStructure s;
    s.add(std::make_unique<Engineer>("张三", 20000));
    s.add(std::make_unique<ProductManager>("李四", 25000));
    s.add(std::make_unique<Manager>("王五", 30000));

    std::cout << "===== 薪资统计 =====\n";
    SalaryVisitor sv;
    s.accept(sv);

    std::cout << "\n===== 成本分析 =====\n";
    CostVisitor cv;
    s.accept(cv);
}

C++ 版与 Java 版结构完全对应。accept 中的 v.visit(*this) 两次分派同样成立:第一次 accept 的虚分派根据元素动态类型选择,第二次 visit 的重载根据 *this 的静态类型(已在具体类中)和 Visitor 的动态类型共同选中。


访问者模式与策略模式、迭代器模式的对比

访问者模式容易被和策略模式混淆(都是“行为型”且都涉及“算法封装”),也容易被和迭代器模式混淆(都涉及“遍历一组对象”)。核心区别如下。

访问者模式 vs 策略模式

维度访问者模式策略模式
解决的问题对一组不同类型的元素施加不同操作对同一件事选择不同算法
算法与谁交互算法作用于元素,需要知道元素的具体类型算法被上下文调用,通常不关心调用者内部结构
分派方式双重分派(元素类型 + 访问者类型)单分派(通常只依赖策略对象的运行时类型)
扩展方向扩展操作容易,扩展元素类型困难扩展算法容易,元素/上下文通常不变
典型场景编译器 AST 上的类型检查、代码生成、格式化支付方式、折扣计算、排序策略

最根本的区别:策略模式封装的是“一个行为族,供上下文替换”;访问者模式封装的是“一组操作,横切一组元素类型”。策略模式的算法之间互不知道;访问者模式的一个访问者必须知道所有元素类型,才能为每个类型提供重载。

访问者模式 vs 迭代器模式

维度访问者模式迭代器模式
侧重点对元素做什么(处理)如何遍历元素(遍历)
扩展开放的方向操作可扩展遍历方式/集合种类可扩展
是否依赖具体元素类型依赖,visit 必须按具体类型重载通常不依赖,只依赖统一接口
典型组合对象结构遍历 + 每元素差异化操作隐藏集合内部表示,统一遍历协议

它们经常配合使用:对象结构用迭代器遍历,遍历到的每个元素再 accept 访问者。访问者负责“差异化处理”,迭代器负责“怎么走到每个元素”。

一句话区分:迭代器解决“怎么走过去”,访问者解决“走过去之后对不同的人做什么”。


访问者模式的优缺点

优点

1. 新增操作符合开闭原则。 新增一种作用于元素结构上的操作,只需新增一个 Visitor 实现类,元素类和对象结构无需修改。

2. 相关行为集中,符合单一职责。 同一类操作(例如所有“成本计算”逻辑)集中在一个访问者类中,而不是散落在各个元素类里。元素类保持轻量。

3. 元素结构与操作彻底解耦。 元素只关心“接受访问”,不关心“访问者要做什么”。操作可以独立演化,甚至可以在不重新编译元素类的情况下新增。

4. 跨元素类型的操作成为可能。 某些操作需要同时理解多种元素类型(例如“统计工程师与管理者的人数比例”),访问者天然可以在自己的多个 visit 方法之间共享状态,完成跨类型的聚合逻辑。

5. 用多态替代 instanceof/反射。 双重分派在编译期就确定了 visit 重载的候选集,把“运行时类型检查”的负担从手写 if (e instanceof …) 转移到了类型系统。新增元素类型时,如果某个 Visitor 没有实现对应的 visit,编译器会直接报错,而不是在运行时悄悄走错分支。

缺点

1. 新增元素类型极其困难。 这是访问者模式最根本的代价。每新增一个 ConcreteElement,你必须在 Visitor 接口中新增一个 visit(NewElement) 方法,进而所有已有的 ConcreteVisitor 实现类都必须修改。这意味着:访问者模式对“操作扩展”开放,但对“元素类型扩展”是关闭的,甚至比不用模式更封闭。

2. 可能破坏元素封装。 访问者要完成有意义的操作,通常需要通过元素的 getter 读取内部数据。如果为了“方便访问者”而把本应私有的字段公开,就削弱了封装。好的实践是:元素通过必要的、受控的公开接口暴露访问者所需的最小数据,而不是把内部状态全盘托出。

3. 违背依赖倒置原则。 Visitor 接口依赖的是具体元素类(visit(Engineer) 而非 visit(Employee)),而不是抽象元素。这是双重分派能够工作的前提,但确实让访问者接口与具体元素类型绑定了。

4. 元素类需要一个“为扩展预留”的 accept 方法。 即使某个元素类根本没有访问者需要访问,它也通常要实现 accept,带来轻微的“模式痕迹”。

5. 不适合元素类型频繁变化的场景。 如果元素类型本身还在快速演化,引入访问者模式会让每次类型变更都变成“全量 Visitor 修改”,维护成本远高于收益。这正是“一个反面例子”要强调的。


一个反面例子

访问者模式有一个极窄但极深的适用窗口:元素类型稳定,操作种类多且持续增长。一旦放错场景,代价立竿见影。

// 场景:系统只有 Engineer 和 Manager 两种员工,
// 未来一年只有“算工资”一种操作,且元素类型大概率还会增加(比如新增 Intern)。
// 此时引入 Visitor:
// - Visitor 接口 + 2 个具体 Visitor + 抽象 Employee + 2 个元素 + ObjectStructure
// - 文件数从 2 变 7;
// - 新增 Intern 时,要改 Visitor 接口 + 所有具体 Visitor + 新增元素类;
// - 而如果一开始只用 if-else 或简单多态,新增 Intern 只需加一个类/分支。

判断标准:

  • 元素类型已稳定(未来大概率不新增),且操作种类多且会增长 → 访问者模式值得;
  • 元素类型仍在频繁变化 → 不要用访问者模式,优先用多态、策略或简单的条件分支;
  • 元素类型稳定但操作只有 1~2 种且几乎不变 → 也没必要,直接写在元素类里或普通多态即可;
  • 操作会增长,但元素类型也同步增长 → 访问者模式的两难会同时爆发,应慎重评估,或考虑下面提到的 Acyclic Visitor 等变体。

访问者模式不是“消除 if-else”的银弹。它是在用“元素扩展的困难”换取“操作扩展的容易”。 这笔交易是否划算,完全取决于你的变化方向。


应用场景

判断是否适用访问者模式,先问两个问题:
① 对象结构中的元素类型是否足够稳定?
② 是否有多种不相关的操作需要施加于这些元素,且操作种类预计会增长?
两个答案都为“是”,访问者模式才值得考虑。

典型应用

编译器与静态分析。 抽象语法树(AST)是最经典的访问者模式场景。AST 节点类型(变量声明、二元表达式、函数调用、if 语句……)相对稳定,但施加于 AST 上的操作不断增长:类型检查、常量折叠、代码生成、格式化、lint 规则、重构。每新增一种分析或变换,就是一个新的 Visitor,AST 节点类保持稳定。

文档导出与格式转换。 文档对象模型(段落、标题、列表、表格、图片等)元素类型稳定,但导出格式可以是 PDF、HTML、Markdown、纯文本、XML。每种导出格式是一个 Visitor,文档结构不变。

报表与统计系统。 如本文的员工例子:员工类型(工程师、产品经理、管理者)稳定,但薪资、绩效、成本、人效、预算等统计口径不断新增。每种统计是一个 Visitor,员工类不受污染。

UI 组件树的横切操作。 控件树(按钮、输入框、下拉框、表格)结构稳定,但需要施加的操作多样:渲染、主题应用、无障碍属性收集、事件绑定、序列化。访问者可以把每种横切操作从控件类中分离出去。

文件系统遍历中的差异化处理。 文件、目录、符号链接等节点类型稳定,但操作可以是:计算总大小、查找重复文件、生成目录树、权限审计、备份。每种操作是一个 Visitor。

游戏中的实体效果施加。 游戏实体类型(玩家、敌人、道具、障碍物)相对稳定,但“效果”可以多种多样:碰撞处理、伤害计算、渲染、音效触发、AI 感知。访问者可以让每种“效果系统”独立演化。


注意事项

必读:适用性的前提

1. 元素类型必须足够稳定。 访问者模式的全部收益建立在“元素类型很少变化”这一前提上。如果元素类型还在频繁增删,访问者接口的每次变更都会引发所有访问者实现类的连锁修改,维护成本会迅速超过收益。

2. 操作种类多且会增长,才是访问者的用武之地。 只有一两种固定操作时,普通多态或策略模式更直接。访问者模式的价值在于隔离“操作”这一变化维度,没有这个变化维度,模式就是空转。

3. 不要用访问者模式替代所有多态。 如果操作天然属于元素自身(例如“计算自身面积”之于图形),把它放在元素类里仍然是正确的。访问者模式适合的是那些横切多个元素类型、且与元素核心职责关系较弱的操作。

必读:封装与数据暴露

4. 只暴露访问者所需的最小数据,不要为了访问者把元素“扒光”。 访问者需要数据才能工作,但元素不应该因此变成透明的数据结构。通过有意义的公开方法(如 getName()、getBaseSalary())暴露必要信息,而不是把所有字段改成 public。封装一旦被访问者破坏,后续任何元素内部实现的变更都会变得危险。

5. 访问者不应反向修改元素的核心状态。 如果访问者需要回传结果,应通过 visit 方法的返回值或结果收集对象来表达,而不是让访问者直接调用元素的 setter 改状态。访问者模式是“操作外置”,不是“状态控制权外置”。

必读:元素扩展的应对

6. 新增元素类型时,做好“全量 Visitor 修改”的心理准备和工程准备。 这是访问者模式的硬约束,不是可以通过“设计得更好”来消除的。如果项目预期元素类型会增长,应在架构决策阶段就明确这一代价,或者提前评估 Acyclic Visitor 等变体。

7. Acyclic Visitor 可以解耦“新增元素”与“修改所有访问者”的绑定,但它把编译期错误转化成了运行时的类型转换失败。 在 Acyclic Visitor 中,Visitor 接口不再为每种元素声明 visit,而是由具体访问者按需实现特定元素的访问接口,通过运行时转换来分派。代价是:如果你漏掉了某个元素的处理,编译器不会告诉你,只会在运行时失败。是否采用,取决于项目对“编译期安全”和“扩展灵活性”的权衡。

进阶:状态、并发与生命周期

8. 访问者可以是有状态的,但共享有状态访问者需要极其谨慎。 如果访问者在 visit 方法之间累积统计结果(例如求和、计数),它天然是有状态的。有状态访问者不能被多个线程或多次遍历并发共享,除非做同步或每次遍历创建新实例。无状态访问者(纯函数式逻辑)可以安全复用。

9. 对象结构的遍历与访问者的执行通常应放在同一条控制流中,避免访问者在遍历中途被跨线程复用。 如果对象结构很大且遍历耗时,并发化的收益需要与“有状态访问者的线程安全成本”一起评估,不能默认访问者模式“天然适合并行”。

10. 访问者的实例化策略要明确。 无状态访问者可以缓存或做成单例;有状态访问者通常每次遍历新建。如果通过 DI 容器管理访问者,要注意作用域(singleton vs prototype)与状态语义的匹配。

进阶:测试与可维护性

11. 每个具体访问者应独立测试。 用 mock 元素或简单测试桩验证每个 visit 方法的行为。元素侧的 accept 也应测试“是否把正确的访问者分发到了正确的 visit 重载”——这本质上是双重分派链路的集成测试。

12. 访问者的命名应表达“操作语义”,而不是“访问者类型”。 用 SalaryCalculatorVisitor、CostAnalyzerVisitor、TypeCheckVisitor,而不是 VisitorA、MyVisitor。访问者的类名就是它所代表的那项操作的文档。

13. 在 Visitor 接口上写清楚每个 visit 的契约。 包括:该方法是否允许修改元素、是否可能抛出异常、是否有顺序依赖、是否线程安全。访问者是横切多个类型的,契约不清会放大维护成本。


访问者模式不负责什么

职责谁负责不该由谁负责
遍历对象结构ObjectStructure 或迭代器Visitor(访问者只处理单个元素,不负责“怎么走到下一个”)
决定“是否要施加某个操作”客户端或对象结构Visitor(访问者被调用就执行,不做前置的业务条件判断)
元素的创建与生命周期工厂、DI 容器、业务层Visitor(访问者消费元素,不创建元素)
元素类型的演化管理架构设计阶段Visitor(模式本身不解决“元素变了怎么办”,只暴露这一代价)
跨元素的并发控制对象结构或外部并发层Visitor(访问者不应承担同步职责)

一句话:访问者模式只做一件事——把“对一组稳定元素施加的可变操作”从元素类中分离出来,并用双重分派把操作精确路由到“访问者 × 元素类型”的组合上。 遍历、创建、并发、元素演化,都不归它管。


总结

  • 元素稳定,操作多且会变,才用访问者。 反过来(元素常变,操作很少),访问者模式会让你付出远大于收益的修改成本。
  • 双重分派是它的灵魂:accept 决定“谁来接待”,visit(this) 决定“来访者做什么”。没有双重分派,访问者模式就退化成了 instanceof 的包装。
  • 它用“元素扩展的封闭”换取“操作扩展的开放”。这是一笔方向性极强的交易,签之前先看清自己的变化方向在哪一侧。
  • 落地三件事:元素只暴露最小必要数据,访问者尽量无状态或明确状态作用域,新增元素类型的代价在架构阶段就当作已知成本来管理。
【设计模式】——访问者模式(Visitor Pattern) 访问者模式(Visitor Pattern)是一种行为型设计模式,用于在不改变现有对象结构的情况下,定义对对象结构中各个元素的访问操作。本文将详细探讨访问者模式在C++中的应用,并通过示例代码进行说明。 阅读详情

相关推荐

【Java设计模式】Java设计模式之(十四)访问者模式(Visitor Pattern)

访问者模式属于行为型模式。 在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。根据模式,元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作。 一、访问者模式介绍 1.1 模式意图 用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。 1.2 通用类图 二、模版方法模式的应用 所有设计模式的代码实现例子都

No8g攻城狮的博客 1694

设计模式-访问者模式-Visitor Design Pattern

这是一种行为设计模式。它管理对象之间的算法、关系和责任。它用于在运行时对一组类似的对象执行操作。它将操作与对象结构解耦。使用访问者模式,我们可以在不改变现有代码的情况下轻松地向现有的类层次结构添加新的行为。有时,它也被称为行为模式(behavioral pattern)。访问者模式的主要目标是将操作逻辑从对象移动到另一个类中。使用访问者模式,我们可以实现开闭原则。根据GoF,访问者设计模式被定义为:访问者模式是一种操作对象结构中元素的操作方法。它允许您定义一个新的操作,而。

Java程序员的知识博客 1356

彩虹期权定价

彩虹期权的定价较为复杂,因为它涉及多个标的资产的联合分布。蒙特卡洛模拟(Monte Carlo Simulation):通过模拟多个标的资产价格路径,计算期权的预期支付。解析近似方法:如马尔可夫链近似方法或多变量Black-Scholes公式的扩展。数值方法:如有限差分法或傅里叶变换法,适用于某些特定条件下的多资产期权定价。由于彩虹期权涉及多个标的资产,我们通常使用蒙特卡洛模拟来进行定价。接下来,我将演示如何使用Heston模型和蒙特卡洛模拟方法来定价彩虹期权。

智能科技前沿 1859

23种设计模式之访问者模式(Visitor Pattern)

本篇文章将23种设计模式中的访问者模式,此篇文章为一天学习一个设计模式系列文章,后面会分享其他模式知识。

小威的博客 1万+

【随机过程】Markov Chain的定义及发展历程与实际应用

本模块书写的初衷:由于在学习张波张景肖等学者所著的《应用随机过程》存在着不甚了解的知识点,经过个人查找资料并整理、分享,希望能够为有同样疑惑的读者贡献绵薄之力,同时如果个人编写内容有错误,恳请读者指正。本文为对《应用随机过程》第42页中Markov Chain基本概念的补充。1.Markov Chain的定义俄国数学家 Andrey Andreyevich Markov 研究并提出一个用数学方法就能解释自然变化的一般规律模型,被命名为马尔科夫链(Markov Chain)。

qq_49657150的博客 3749

锁定放大器

锁定放大器

qq_46757048的博客 4706

设计模式-访问者模式-Visitor Pattern

访问者模式提供了一种将操作和对象结构分离的方法,使得你可以在不修改对象结构的情况下增加新操作。这种模式在处理复杂对象结构和需要动态添加操作的场景中非常有用。访问者模式是一种强大的模式,它允许你向对象结构添加新的操作而不需要修改对象结构本身。然而,它也带来了一些缺点,如增加系统复杂性和维护难度。因此,在决定使用访问者模式时,需要权衡其优缺点,确保它适合你的应用场景。通常,当对象结构相对稳定,而需要对结构中的元素执行多种操作时,访问者模式是一个不错的选择。

wesigj的博客 1614

使用 Java + PaddleOCR 实现表格识别,支持导出 HTML 和 Excel(附源码)

使用Java+PaddleOCR快速搭建表格识别系统,无需处理底层模型细节。系统支持表格结构识别和内容识别,可将图片转换为可编辑表格格式。文章提供了完整的实现流程,包括Maven依赖引入、模型下载(包含检测、识别、分类和表格结构模型)及参数配置说明。SmartJavaAI支持多种高性能模型选择,适用于不同应用场景,帮助开发者快速实现表格识别功能。

deng775747758的博客 1476

设计模式 22 访问者模式 Visitor Pattern

访问者模式是一种行为型设计模式,它允许你在不改变已有类结构的情况下,为一组对象添加新的操作。它将算法与对象结构分离,使你能够在不修改现有类的情况下,为这些类添加新的操作。访问者模式是一种强大的设计模式,但需要谨慎使用。在实施访问者模式时,需要仔细考虑元素接口和访问者接口的设计,以及具体访问者类的实现。同时,需要了解访问者模式的局限性,并选择合适的应用场景。

jxusthusiwen的专栏 1306

无实习经验?IT应届生简历封神思路:把校园经历写成offer收割机

摘要: IT应届生无实习经历如何打造竞争力简历?本文揭秘“无实习简历封神指南”,通过四大步骤将校园经历转化为HR认可的硬核技能: 素材挖掘:课程设计、个人项目、社团活动、竞赛证书都是隐藏的“实战经验”,用STAR法则细化成果。 技能转化:匹配岗位JD关键词,用“动词+量化结果”包装经历(如“优化查询效率30%”)。 避坑指南:避免流水账、夸大技能或堆砌无关经历,聚焦技术能力与岗位需求。 核心逻辑:HR看重潜力而非经验,用校园经历证明学习能力、问题解决能力和技术落地能力。 无实习≠无竞争力,学会“翻译”经历,

通俗易懂学IT 424

Matlab:Matlab 软件学习之Matlab内置的各种系统的简介、案例应用(基于Simulink模糊控制仿真/二阶瞬态震荡电路案例)之详细攻略

选择左上角要编辑变量的图标,给定当前变量的论域Range,设置隶属函数类型type(八种隶属函数类型以供选择),最后选中某个隶属度函数(选中为红色),移动绘图区的每个隶属度函数的三个端点(竖向为权重)。(1)、首先在模糊规则编辑器左边选择一个输入变量,并选中它的一个语言值(隶属函数),然后编辑器右边选择一个输出变量,并选中它的一个语言值(隶属函数),然后将这种关系添加到模糊规则中。制定完成后,会形成模糊规则矩阵,系统会根据模糊输入量(如r1、r2),经过模糊规则(如45条),进而确定输出量(Y1、Y2)。

走向CTO之路 1123

设计模式:访问者模式(Visitor Pattern)

访问者模式是一种行为型设计模式,它将数据结构与对数据的操作分离。通过在不改变数据结构的前提下,定义作用于这些数据结构的新操作。

TechNomad的博客 1509

多模态大模型助力AI人工智能领域的智能决策

我们的目的是深入了解多模态大模型是怎样在AI人工智能领域帮助做出智能决策的。范围涵盖了多模态大模型的基本概念、工作原理、在不同场景下的应用,以及未来的发展方向等。接下来,我们会先介绍多模态大模型和智能决策的核心概念,再讲讲它们之间的关系。然后说说多模态大模型在智能决策中的算法原理和操作步骤,用数学公式来解释一下。接着通过一个项目实战案例让大家更直观地感受。之后看看多模态大模型在实际中的应用场景,推荐一些学习的工具和资源。最后探讨未来的发展趋势和挑战,进行总结并提出一些思考题。多模态大模型。

AIGC应用创新大全的博客 919

23种设计模式——访问者模式 (Visitor Pattern)

本文介绍了23种经典设计模式之——访问者模式 (Visitor Pattern)的使用及说明

Meteors.的博客 1200

UCIE Specification详解(五)

Logical Physical Layer(逻辑物理层)

Crazzy_M的博客 1227

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

【设计模式二十五之访问者模式VisitorPattern】访问者VisitorPattern模式

访问者模式细说访问者模式细说访问者模式定义UML模型基于UML的代码场景一场景代码场景二场景代码访问者模式应用和注意事项 细说访问者模式 提示: 博主:章飞 _906285288的博客 博客地址:http://blog.youkuaiyun.com/qq_29924041 细说访问者模式 定义 UML模型 基于UML的代码 场景一 场景 代码 场景二 场景 代码 访问者模式应用和注意事项 欢迎继续...

章飞 _906285288的博客 505

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

设计模式 - 行为型模式_ 访问者模式Visitor Pattern

文章目录定义 定义 访问者模式是⼀种⾏为设计模式, 它能将算法与其所作⽤的对象隔离开来。 在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。 通过这种方式,元素的执行算法可以随着访问者改变而改变。这种类型的设计模式属于行为型模式。根据模式,元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作。 ...

小工匠 2万+

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

设计模式教程:访问者模式(Visitor Pattern)

表示一个作用于某对象结构中的各元素的操作,它可以在不改变元素类的前提下定义作用于这些元素的新操作。

遇见伯灵说的博客 1109

设计模式访问者模式(VisitorPattern)

核心模块:抽象访问者、具体访问者、抽象元素、具体元素、对象结构 所有代码请访问:git@code.aliyun.com:289804201/PatternLearn.git 目的:封装一些施加于某种数据元上上的操作,使不改变元素类的情况下定义作用于这个元素的新操作; 使用场景:数据元很少改变,但是数据元上的操作经常改变; 优点:方便添加新操作,有关元素对象的访问

didiao11300的专栏 273

【C#设计模式】21.访问者模式(Visitor Pattern)【行为型】

在软件构建过程中,由于需求的改变,某些类层次结构中常常需要增加新的行为(方法),如果直接在基类中做这样的更改,将会给子类带来很繁重的变更负担,甚至破坏原有设计。如何在不更改类层次结构的前提下,在运行时根据需要透明地为类层次结构上的各个类动态添加新的操作,从而避免上述问题?.........

星Yeah 的博客 1705

设计模式【访问者模式VisitorPattern】

访问者模式 在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。这种类型的设计模式属于行为型模式。根据模式,元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作。 介绍 意图:主要将数据结构与数据操作分离。 主要解决:稳定的数据结构和易变的操作耦合问题。

458

设计模式——访问者模式(Visitor Pattern)+ Spring相关源码

访问者模式,将 数据结构 与 数据操作 分离。

码鹿的记录 1500

Java设计模式——访问者模式【Visitor Pattern】

Java设计模式、访问者模式、Visitor Pattern

scorpio的博客 386

设计模式---访问者模式(DesignPattern_Visitor)

摘录自:设计模式与游戏完美开发 十年磨一剑,作者将设计模式理论巧妙地融入到实践中,以一个游戏的完整实现呈现设计模式的应用及经验的传承 《轩辕剑》之父——蔡明宏、资深游戏制作人——李佳泽、Product Evangelist at Unity Technologies——Kelvin Lo、信仁软件设计创办人—— 赖信仁、资深3D游戏美术——刘明恺 联合推荐全书采用了整合...

su9257的博客 103

设计模式 - 学习笔记 - VisitorPattern 访问者模式

设计模式 - 学习笔记 - 访问者模式VisitorPattern学习总结代码示例养身保健接口具体养身保健服务访问者接口具体访问者程序猿old镖客运行结果参考资料 学习总结 访问者模式的目的是将数据与操作数据的行为解构开,针对已知的数据(Element),可以扩展不同的行为(Visitor). 养身保健的服务是固定的(比如只有4个)。 访问者接口决定所有访问者都知道有4个服务。 可扩展点就是访问者。通过创建新的访问者实现访问者接口来实现扩展。 局限在于保健服务定死了。如果需要添加新服务,那么所有访问者都要

灵台方寸山斜月三星洞 555
上一篇: 设计模式之模板方法模式
下一篇: Spring AI 2.0 系列(一):Spring AI 2.0升级了什么?
LiaoYuan.Dai
博客等级 码龄7年 247粉丝 24原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值