行为设计模式之Visitor(访问者)

行为设计模式之Visitor(访问者)

摘要:

访问者模式(Visitor)是一种行为设计模式,允许在不改变对象结构的情况下定义新操作。其核心思想是将算法与对象结构分离,通过"访问者"对象来实现对元素的操作。该模式包含Visitor(访问者)、ConcreteVisitor(具体访问者)、Element(元素)和ConcreteElement(具体元素)等角色。示例代码展示了两种不同的访问者(ConcreteVisitorOne和ConcreteVisitorTwo)分别对学生和教师对象执行不同的统计操作(如年龄总和、最高成绩等)。访问者模式适用于对象结构稳定但需要频繁添加新操作的场景,避免了业务逻辑"污染"对象类本身。

1)意图

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

2)结构

其中:

  • Visitor(访问者)为该对象结构中 ConcreteElement 的每一个类声明一个 Visit 操作。该操作的名字和特征标识了发送 Vist 请求给该访问者的那个类,这使得访问者可以确定正被访问元素的具体的类。这样访问者就可以通过该元素的特定接口直接访问它。
  • ConcreteVisitor (具体访问者)实现每个有 Visitor 声明的操作,每个操作实现本算法的一部分,而该算法片段乃是对应于结构中对象的类。Concrete Visitor为该算法提供了上下文并存储它的局部状态。这一状态常常在遍历该结构的过程中累积结果。
  • Element (元素)定义以一个访问者为参数的 Accept 操作。
  • ConcreteElement (具体元素)实现以一个访问者为参数的 Accept 操作。
  • ObjectStructure (对象结构)能枚举它的元素;可以提供一个高层的接口以允许该访问者访问它的元素;可以是一个组合或者一个集合,如一个列表或一个无序集合。

3)适用性

Visitor 模式适用于:

  • 一个对象结构包含很多类对象,它们有不同的接口,而用户想对这些对象实施一些依
    赖于其具体类的操作。
  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而又想要避免这些
    操作"污染"这些对象的类。
  • 定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。]
c 复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * @author psd
 */
public class VisitorPattern {
    public static void main(String[] args) {
        PersonStructure personStructure = new PersonStructure();
        ConcreteVisitorOne concreteVisitorOne = new ConcreteVisitorOne();
        personStructure.accept(concreteVisitorOne);
        System.out.println("学生的成绩总和:" + concreteVisitorOne.getSumStudentAge() + ",老师年龄总和:" + concreteVisitorOne.getSumTeacherAge());
        System.out.println("---------------------------------------------");
        ConcreteVisitorTwo concreteVisitorTwo = new ConcreteVisitorTwo();
        personStructure.accept(concreteVisitorTwo);
        System.out.println("学生的最高成绩:" + concreteVisitorTwo.getMaxStuScore() + ",老师最高工龄:" + concreteVisitorTwo.getMaxWorkingAge());
    }
}

class PersonStructure {
    private final List<Person> personList = new ArrayList<>();

    public PersonStructure() {
        personList.add(new Student("张三", 18, 90));
        personList.add(new Student("李四", 19, 80));
        personList.add(new Student("王五", 20, 70));

        personList.add(new Teacher("王老师", 50, 22));
        personList.add(new Teacher("张老师", 53, 31));
        personList.add(new Teacher("肖老师", 42, 19));

    }
    public void accept(Visitor visitor){
        personList.forEach(person -> person.accept(visitor));
    }
}

interface Visitor {
    /**
     * 访问者A
     * 
     * @param student
     *            学生
     */
    void visitConcreteElementStudentA(Student student);

    /**
     * 访问者B
     * 
     * @param teacher
     *            老师
     */
    void visitConcreteElementTeacherB(Teacher teacher);

}

class ConcreteVisitorTwo implements Visitor {

    int maxStuScore = 0;
    int maxWorkingAge = 0;

    public int getMaxStuScore() {
        return maxStuScore;
    }

    public void setMaxStuScore(int maxStuScore) {
        this.maxStuScore = maxStuScore;
    }

    public int getMaxWorkingAge() {
        return maxWorkingAge;
    }

    public void setMaxWorkingAge(int maxWorkingAge) {
        this.maxWorkingAge = maxWorkingAge;
    }

    @Override
    public void visitConcreteElementStudentA(Student student) {
        System.out.println("访问者2 学生:" + student.getName() + ",年级:" + student.getGrade());
        maxStuScore = Math.max(maxStuScore, student.getGrade());
    }

    @Override
    public void visitConcreteElementTeacherB(Teacher teacher) {
        System.out.println("访问者2 老师:" + teacher.getName() + ",年龄:" + teacher.getAge() + ",工龄:" + teacher.getWorkingAge());
        maxWorkingAge = Math.max(maxWorkingAge, teacher.getWorkingAge());
    }
}

class ConcreteVisitorOne implements Visitor {

    int sumStudentAge = 0;
    int sumTeacherAge = 0;

    public int getSumStudentAge() {
        return sumStudentAge;
    }

    public void setSumStudentAge(int sumStudentAge) {
        this.sumStudentAge = sumStudentAge;
    }

    public int getSumTeacherAge() {
        return sumTeacherAge;
    }

    public void setSumTeacherAge(int sumTeacherAge) {
        this.sumTeacherAge = sumTeacherAge;
    }

    @Override
    public void visitConcreteElementStudentA(Student student) {
        System.out.println("访问者1 学生:" + student.getName() + ",年龄:" + student.getAge());
        sumStudentAge += student.getAge();
    }

    @Override
    public void visitConcreteElementTeacherB(Teacher teacher) {
        System.out.println("访问者1 老师:" + teacher.getName() + ",年龄:" + teacher.getAge());
        sumTeacherAge += teacher.getAge();
    }
}

class Student extends Person {
    private int grade;

    public Student(String name, int age, int grade) {
        super(name, age);
        this.grade = grade;
    }

    public int getGrade() {
        return grade;
    }

    public void setGrade(int grade) {
        this.grade = grade;
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visitConcreteElementStudentA( this);
    }
}

class Teacher extends Person {
    private int workingAge;

    public Teacher(String name, int age, int workingAge) {
        super(name, age);
        this.workingAge = workingAge;
    }

    public int getWorkingAge() {
        return workingAge;
    }

    public void setWorkingAge(int workingAge) {
        this.workingAge = workingAge;
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visitConcreteElementTeacherB(this);
    }
}

abstract class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public abstract void accept(Visitor visitor);
}

4)核心使用场景

访问者模式适用于以下典型场景:

1. 数据结构稳定,但操作频繁变化或需要扩展

  • 问题: 系统拥有一个相对稳定的对象结构 (如由多个固定类型的元素组成的复合结构),但需要经常新增或修改对这些元素的操作
  • 访问者模式的解决: 将操作(算法)封装在独立的访问者对象中。添加新操作只需增加新的访问者类,无需修改已有的元素类或对象结构本身。符合开闭原则(对扩展开放,对修改关闭)。
  • 经典例子:编译器/解释器:
    • 稳定结构: 抽象语法树(AST),节点类型相对固定(如变量声明节点赋值语句节点二元表达式节点函数调用节点)。
    • 多变操作: 语法检查、类型推断、代码优化、代码生成(到不同平台如 JVM, LLVM IR)、代码格式化、度量计算(圈复杂度)等。每种操作都可以是一个独立的访问者(TypeCheckingVisitor, CodeGenerationVisitor, PrettyPrintVisitor)。

2. 需要对一个复杂对象结构(如组合树)中的元素执行多种不相关的操作

  • 问题: 一个对象结构(如组合模式构建的树)包含多种类型的元素。需要对这些元素执行许多不同类型且可能不相关 的操作。如果将这些操作分散在元素类中,会导致:
    • 元素类变得臃肿,职责过多。
    • 难以新增操作(需要修改所有元素类)。
    • 破坏了元素的单一职责原则。
  • 访问者模式的解决: 将各种操作提取到各自的访问者类中。元素类只保留基本的属性和结构关系,操作逻辑被分离出去。
  • 例子:文档对象模型(DOM)处理:
    • 稳定结构: DOM树(元素节点、文本节点、属性节点等)。
    • 多样操作: 渲染到屏幕(HTML/CSS)、导出为PDF、提取文本内容、查找死链、计算SEO指标、序列化为XML/JSON等。每种操作可作为一个访问者(RenderVisitor, ExportToPDFVisitor, TextExtractionVisitor)。

3. 需要集中相关操作并隔离无关操作

  • 问题: 某些操作只作用于对象结构中的部分元素 ,或者操作逻辑本身具有很强的内聚性(如所有与"打印"相关的逻辑),但与其他操作逻辑无关。
  • 访问者模式的解决: 将相关的操作集中在一个访问者类中,使代码组织更清晰。同时,访问者模式天然地将作用于不同元素类型上的操作代码隔离在不同的visitXxx()方法中。
  • 例子:自动化测试/验证工具:
    • 稳定结构: UI组件树(按钮、文本框、下拉菜单、容器等)。
    • 集中操作: 一个AccessibilityCheckVisitor可集中实现所有检查无障碍标准(如WCAG)的逻辑(检查按钮是否有aria-label、图片是否有alt文本、颜色对比度是否足够等),而UISnapshotVisitor则负责生成UI截图用于视觉回归测试。

4. 需要跨越多个类层次执行操作

  • 问题: 操作逻辑的实现需要依赖于多个不同类的具体类型信息,并且这些类可能属于不同的继承层次。
  • 访问者模式的解决: 访问者模式通过双分派(元素accept(visitor) -> 访问者visit(element))让访问者对象能够根据元素的具体类型调用正确的方法。访问者可以"访问"任意实现了accept方法的元素,无论它们属于哪个类层次。
  • 例子:保险/金融系统的费率计算:
    • 稳定结构: 包含不同类型保单(车险、寿险、房屋险)和客户(个人、企业)的对象结构。
    • 复杂操作: 一个PremiumCalculationVisitor需要根据具体的保单类型具体的客户类型 的组合来计算不同的费率。访问者内部可以通过visit(CarInsurancePolicy, PersonalCustomer)visit(LifeInsurancePolicy, CorporateCustomer)等重载方法来处理各种组合情况。

⚠️ 关键前提(何时才应考虑访问者模式)

  1. 对象结构必须稳定: 被访问的元素类层次结构(有哪些具体元素类)很少变化。如果经常需要添加新的元素类型,那么每次添加新元素都需要修改所有的访问者接口和类(添加新的visit(NewElement)方法),这会非常痛苦且违反开闭原则。
  2. 操作需要频繁变化或扩展: 系统需要定义很多作用于元素上的操作,或者这些操作经常需要变化、新增。
  3. 操作逻辑与元素结构分离的需求强烈: 将操作分散在元素类中会导致元素类职责过重、难以维护,或者出于架构设计考虑(如希望核心领域模型保持纯净,不包含业务逻辑)。

5)常见应用领域

  • 编译器与解释器: 抽象语法树(AST)的遍历与各种处理(类型检查、代码生成、优化、格式化等)。这是最经典的应用。
  • 文档处理:
    • XML/JSON/HTML DOM树的解析、转换、渲染、验证、查询。
    • 富文本文档(如Word处理器)中不同元素(段落、图片、表格、公式)的导出(PDF/HTML)、拼写检查、字数统计。
  • UI框架与GUI工具包:
    • 复杂UI组件树的渲染、布局计算、事件模拟、无障碍检查、自动化测试、主题切换。
  • 静态代码分析工具: 遍历代码结构(类、方法、字段、语句)进行度量计算、代码异味检测、依赖分析等。
  • 序列化/反序列化框架: 将对象结构转换为特定格式(XML, JSON, 二进制)或从特定格式恢复。访问者可以处理不同类型对象的序列化细节。
  • 复杂配置/模型处理: 如处理一个由多种规则、策略、条件组成的配置模型,需要执行验证、解释、优化等操作。

✅ 访问者模式的优势总结

  • 开闭原则: 易于添加新操作(新访问者),无需修改元素类或对象结构。
  • 单一职责原则: 将相关的操作集中在一个访问者类中,将无关的操作分散到不同的访问者中。元素类只需关注自身数据和结构。
  • 复用性: 访问者对象可以携带状态,在遍历过程中累积结果(如计算总和、收集信息),便于复用访问逻辑。
  • 灵活性: 访问者可以在遍历对象结构时执行任何需要的操作。

❌ 访问者模式的缺点与注意事项

  • 破坏封装: 访问者通常需要访问元素的内部细节(非公开成员),这可能迫使元素暴露其内部状态(通过public方法或Friend类等方式),破坏了元素的封装性。
  • 对元素结构变化的抵抗力差: 添加新的元素类型 非常困难,需要修改所有已有的访问者接口和类(添加对应的visit(NewElementType)方法),违反了开闭原则。这是使用访问者模式的最大代价。
  • 复杂性: 模式本身较难理解(尤其是双分派),增加了代码的间接性和理解难度。
  • 可能违背依赖倒置原则: 具体的访问者类依赖于具体的元素类(体现在visit(ConcreteElementA)等方法签名上),而不是只依赖于抽象。

📌 总结:何时选择访问者模式?

当你需要为一个相对稳定的复杂对象结构(通常是树状结构)定义许多不同的、可能不相关的操作,并且预期这些操作会频繁增加或变化,而对象结构本身很少改变时,访问者模式是一个强大的工具。

核心口诀:
"结构稳定操作多,解耦扩展选访客。"

在编译器、复杂文档处理、静态分析、UI框架等需要分离数据结构和作用于其上的多样化操作的场景中,访问者模式的价值尤为突出。但在对象结构不稳定或操作种类较少的场景下,应谨慎考虑其引入的复杂性和对封装的破坏。

喜欢我的文章记得点个在看,或者点赞,持续更新中ing...