设计模式(二十四)行为型:访问者模式详解

设计模式(二十四)行为型:访问者模式详解

访问者模式(Visitor Pattern)是 GoF 23 种设计模式中最具争议性但也最强大的行为型模式之一,其核心价值在于将作用于某种数据结构中的各元素的操作分离出来,封装到一个独立的访问者对象中,使得在不改变元素类的前提下可以定义新的操作 。它通过"双重分派"(Double Dispatch)机制,解决了在静态类型语言中对异构对象集合 进行多态操作扩展的难题。访问者模式是构建编译器(语法树遍历)、文档处理系统、复杂报表生成、UI 渲染引擎、静态代码分析工具等系统的理想选择,是实现"开闭原则"在操作维度上的终极体现。

一、详细介绍

访问者模式解决的是"一个数据结构(如对象树或列表)包含多种类型的元素,且需要对这些元素执行多种不同的、与元素本身无关的操作,且这些操作可能频繁新增"的问题。在传统设计中,通常将操作直接定义在元素类中。这导致:

  • 违反单一职责原则:元素类承担了数据和多种操作的职责。
  • 难以扩展操作:新增操作需要修改所有元素类,违反开闭原则。
  • 代码分散:同一操作的逻辑分散在多个元素类中。

访问者模式的核心思想是:将"数据结构"与"作用于数据的操作"解耦。数据结构中的元素接受一个访问者对象作为参数,回调访问者对象中对应其类型的方法。新的操作只需添加新的访问者类,无需修改任何元素类

该模式包含以下核心角色:

  • Visitor(访问者接口) :声明一组 visit() 方法,每个方法对应一种具体的元素类型(如 visit(ElementA), visit(ElementB))。它定义了所有可执行操作的抽象接口。
  • ConcreteVisitor(具体访问者) :实现 Visitor 接口,为每种元素类型提供具体的操作实现。每个具体访问者代表一种独立的操作(如打印、计算、导出)。
  • Element(元素接口) :声明一个 accept(Visitor) 方法,允许访问者"访问"自身。
  • ConcreteElementA, ConcreteElementB, ...(具体元素) :实现 Element 接口,实现 accept() 方法。在 accept() 中,调用访问者的 visit(this) 方法,将自身作为参数传入,触发正确的 visit 方法(关键:this 的静态类型是当前类,实现双重分派)。
  • ObjectStructure(对象结构) :可选角色,表示包含元素的集合或复合结构(如树、列表)。它提供一个接口,允许访问者遍历其所有元素,并调用每个元素的 accept() 方法。

访问者模式的关键优势:

  • 符合开闭原则(操作维度) :新增操作只需添加新的 ConcreteVisitor,无需修改 ElementConcreteElement
  • 符合单一职责原则 :元素类只负责数据和 accept,操作逻辑集中在访问者中。
  • 操作集中化:同一操作的逻辑集中在单个访问者类中,易于理解、维护和复用。
  • 支持新操作:可以轻松添加如打印、统计、转换、验证等新操作。

访问者模式的关键挑战(双重分派):

  1. 第一重分派 :在 ObjectStructure 中,调用 element.accept(visitor)。由于 element 是多态的,accept() 的调用会根据 element 的实际类型分派到 ConcreteElementA.accept()ConcreteElementB.accept()
  2. 第二重分派 :在 ConcreteElementX.accept() 中,调用 visitor.visit(this)this静态类型ConcreteElementX,因此编译器会选择 visitor 上参数类型为 ConcreteElementXvisit 方法。即使 visitor 是多态的,visit 方法的重载选择在编译时基于 this 的静态类型确定。

缺点与限制

  • 违反里氏替换原则accept() 方法暴露了具体类型。
  • 元素类难以修改 :新增元素类型需要修改所有 Visitor 接口及其所有实现类,违反开闭原则(结构维度)。
  • 复杂性高:理解双重分派和模式结构需要较高心智负担。
  • 过度设计:对于简单操作或稳定结构,可能不必要。

访问者模式适用于:

  • 数据结构稳定,但操作频繁变化(如编译器 AST)。
  • 需要对复杂对象结构执行多种不同的操作。
  • 操作需要访问元素的私有成员(访问者可通过友元或公共方法访问)。
  • 需要避免在元素类中堆积大量无关操作。

二、访问者模式的UML表示

以下是访问者模式的标准 UML 类图:
implements implements implements implements implements calls visit() calls visit() calls visit() contains calls accept() <<interface>> Visitor +visit(elementA: ConcreteElementA) +visit(elementB: ConcreteElementB) +visit(elementC: ConcreteElementC) ConcreteVisitor1 +visit(elementA: ConcreteElementA) +visit(elementB: ConcreteElementB) +visit(elementC: ConcreteElementC) ConcreteVisitor2 +visit(elementA: ConcreteElementA) +visit(elementB: ConcreteElementB) +visit(elementC: ConcreteElementC) <<interface>> Element +accept(visitor: Visitor) ConcreteElementA +accept(visitor: Visitor) +operationA() ConcreteElementB +accept(visitor: Visitor) +operationB() ConcreteElementC +accept(visitor: Visitor) +operationC() ObjectStructure -elements: List<Element> +attach(element: Element) +detach(element: Element) +accept(visitor: Visitor)

图解说明

  • Element 接口定义 accept(Visitor)
  • ConcreteElementX 实现 accept(),内部调用 visitor.visit(this)
  • Visitor 接口为每种 ConcreteElement 声明一个 visit 重载方法。
  • ConcreteVisitor 实现所有 visit 方法,提供具体操作。
  • ObjectStructure 管理元素集合,并提供 accept(Visitor) 遍历所有元素。

三、一个简单的Java程序实例及其UML图

以下是一个文档处理系统的示例,文档包含段落(Paragraph)、图片(Image)、表格(Table)元素,需要支持打印和统计字数操作。

Java 程序实例
java 复制代码
// 访问者接口
interface DocumentElementVisitor {
    void visit(Paragraph paragraph);
    void visit(Image image);
    void visit(Table table);
}

// 元素接口
interface DocumentElement {
    void accept(DocumentElementVisitor visitor);
}

// 具体元素:段落
class Paragraph implements DocumentElement {
    private String text;

    public Paragraph(String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }

    // accept 实现:回调访问者,传入自身(this)
    @Override
    public void accept(DocumentElementVisitor visitor) {
        visitor.visit(this); // 双重分派的关键:this 是 Paragraph 类型
    }

    public void spellCheck() {
        System.out.println("🔍 段落拼写检查: " + text);
    }
}

// 具体元素:图片
class Image implements DocumentElement {
    private String filename;
    private int width;
    private int height;

    public Image(String filename, int width, int height) {
        this.filename = filename;
        this.width = width;
        this.height = height;
    }

    public String getFilename() {
        return filename;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

    @Override
    public void accept(DocumentElementVisitor visitor) {
        visitor.visit(this); // this 是 Image 类型
    }

    public void compress() {
        System.out.println("🗜️  压缩图片: " + filename);
    }
}

// 具体元素:表格
class Table implements DocumentElement {
    private String[][] data;
    private int rows;
    private int cols;

    public Table(String[][] data) {
        this.data = data;
        this.rows = data.length;
        this.cols = data.length > 0 ? data[0].length : 0;
    }

    public String[][] getData() {
        return data;
    }

    public int getRows() {
        return rows;
    }

    public int getCols() {
        return cols;
    }

    @Override
    public void accept(DocumentElementVisitor visitor) {
        visitor.visit(this); // this 是 Table 类型
    }

    public void validate() {
        System.out.println("✅ 表格数据验证: " + rows + "x" + cols + " 表格");
    }
}

// 具体访问者:打印访问者
class PrintVisitor implements DocumentElementVisitor {
    @Override
    public void visit(Paragraph paragraph) {
        System.out.println("🖨️  打印段落: \"" + paragraph.getText() + "\"");
    }

    @Override
    public void visit(Image image) {
        System.out.println("🖼️  打印图片: " + image.getFilename() + 
                          " (" + image.getWidth() + "x" + image.getHeight() + ")");
    }

    @Override
    public void visit(Table table) {
        System.out.println("📊 打印表格: " + table.getRows() + " 行, " + table.getCols() + " 列");
        for (int i = 0; i < table.getRows(); i++) {
            for (int j = 0; j < table.getCols(); j++) {
                System.out.print("[" + table.getData()[i][j] + "] ");
            }
            System.out.println();
        }
    }
}

// 具体访问者:字数统计访问者
class WordCountVisitor implements DocumentElementVisitor {
    private int wordCount = 0;

    @Override
    public void visit(Paragraph paragraph) {
        String[] words = paragraph.getText().split("\\s+");
        int count = words.length;
        wordCount += count;
        System.out.println("📝 段落字数: \"" + paragraph.getText() + "\" -> " + count + " 词");
    }

    @Override
    public void visit(Image image) {
        // 图片无文字,不计数,但可记录
        System.out.println("🖼️  图片: " + image.getFilename() + " (0 词)");
    }

    @Override
    public void visit(Table table) {
        int count = 0;
        for (int i = 0; i < table.getRows(); i++) {
            for (int j = 0; j < table.getCols(); j++) {
                if (table.getData()[i][j] != null && !table.getData()[i][j].trim().isEmpty()) {
                    count += table.getData()[i][j].split("\\s+").length;
                }
            }
        }
        wordCount += count;
        System.out.println("📊 表格字数: " + count + " 词");
    }

    // 获取统计结果
    public int getWordCount() {
        return wordCount;
    }
}

// 对象结构:文档
class Document {
    private java.util.List<DocumentElement> elements = new java.util.ArrayList<>();

    public void addElement(DocumentElement element) {
        elements.add(element);
    }

    public void removeElement(DocumentElement element) {
        elements.remove(element);
    }

    // 接受访问者,遍历所有元素
    public void accept(DocumentElementVisitor visitor) {
        for (DocumentElement element : elements) {
            element.accept(visitor);
        }
    }
}

// 客户端使用示例
public class VisitorPatternDemo {
    public static void main(String[] args) {
        System.out.println("📄 文档处理系统 - 访问者模式示例\n");

        // 创建文档和元素
        Document document = new Document();
        document.addElement(new Paragraph("这是一个关于设计模式的文档。"));
        document.addElement(new Image("diagram.png", 800, 600));
        document.addElement(new Paragraph("访问者模式非常强大。"));
        document.addElement(new Table(new String[][]{
            {"模式", "类型", "用途"},
            {"访问者", "行为型", "分离操作"},
            {"策略", "行为型", "替换算法"}
        }));
        document.addElement(new Paragraph("总结:访问者模式适用于稳定结构。"));

        // 使用打印访问者
        System.out.println("--- 执行打印操作 ---");
        PrintVisitor printVisitor = new PrintVisitor();
        document.accept(printVisitor); // 遍历元素,触发 accept -> visit

        System.out.println("\n--- 执行字数统计操作 ---");
        WordCountVisitor wordCountVisitor = new WordCountVisitor();
        document.accept(wordCountVisitor);
        System.out.println("📊 文档总字数: " + wordCountVisitor.getWordCount() + " 词");

        // 演示新增操作无需修改元素类
        System.out.println("\n--- 新增操作:元素类型检查 ---");
        // 只需定义新访问者
        class TypeCheckVisitor implements DocumentElementVisitor {
            @Override
            public void visit(Paragraph paragraph) {
                System.out.println("✅ 元素: 段落, 内容长度: " + paragraph.getText().length());
            }

            @Override
            public void visit(Image image) {
                System.out.println("✅ 元素: 图片, 文件: " + image.getFilename() + 
                                  ", 尺寸: " + image.getWidth() + "x" + image.getHeight());
            }

            @Override
            public void visit(Table table) {
                System.out.println("✅ 元素: 表格, 大小: " + table.getRows() + "x" + table.getCols());
            }
        }
        TypeCheckVisitor typeCheckVisitor = new TypeCheckVisitor();
        document.accept(typeCheckVisitor);
    }
}
实例对应的UML图(简化版)

implements implements implements implements implements implements calls visit() calls visit() calls visit() contains calls accept() <<interface>> DocumentElementVisitor +visit(paragraph: Paragraph) +visit(image: Image) +visit(table: Table) PrintVisitor +visit(paragraph: Paragraph) +visit(image: Image) +visit(table: Table) WordCountVisitor -wordCount: int +visit(paragraph: Paragraph) +visit(image: Image) +visit(table: Table) +getWordCount() TypeCheckVisitor +visit(paragraph: Paragraph) +visit(image: Image) +visit(table: Table) <<interface>> DocumentElement +accept(visitor: DocumentElementVisitor) Paragraph -text: String +accept(visitor: DocumentElementVisitor) +getText() Image -filename: String -width: int -height: int +accept(visitor: DocumentElementVisitor) +getFilename() Table -data: String[][] +accept(visitor: DocumentElementVisitor) +getData() Document -elements: List<DocumentElement> +addElement(element: DocumentElement) +accept(visitor: DocumentElementVisitor)

运行说明

  • DocumentElement 定义 accept()
  • Paragraph, Image, Table 实现 accept(),内部调用 visitor.visit(this)
  • DocumentElementVisitor 为每种元素声明 visit 重载。
  • PrintVisitor, WordCountVisitor 实现 visit 方法,提供具体操作。
  • Documentaccept() 遍历所有元素,调用其 accept()
  • 新增 TypeCheckVisitor 无需修改任何元素类,完美体现开闭原则。

四、总结

特性 说明
核心目的 分离数据结构与操作,支持在不修改元素的情况下新增操作
实现机制 双重分派:元素 accept 访问者,访问者 visit 元素
优点 符合开闭原则(操作维度)、操作集中化、支持新操作、符合单一职责
缺点 违反里氏替换、元素新增困难(违反开闭原则-结构维度)、复杂性高、依赖具体类型
适用场景 稳定数据结构(如AST)、多操作需求、编译器、文档处理、报表生成
不适用场景 结构频繁变化、操作简单、避免继承/重载的语言

访问者模式使用建议

  • 仅在数据结构
相关推荐
困鲲鲲4 小时前
设计模式:中介者模式 Mediator
设计模式·中介者模式
程序员编程指南6 小时前
Qt 并行计算框架与应用
c语言·数据库·c++·qt·系统架构
困鲲鲲8 小时前
设计模式:状态模式 State
设计模式·状态模式
蝸牛ちゃん10 小时前
设计模式(十一)结构型:外观模式详解
设计模式·系统架构·软考高级·外观模式
weixin_4708802611 小时前
设计模式实战:自定义SpringIOC(亲手实践)
设计模式·面试·个人提升·springioc·设计模式实战·spring框架原理
阳光明媚sunny12 小时前
创建型设计模式-工厂方法模式和抽象工厂方法模式
设计模式·工厂方法模式
困鲲鲲13 小时前
设计模式:代理模式 Proxy
设计模式·代理模式
就是帅我不改16 小时前
深入实战工厂模式与观察者模式:工厂模式与观察者模式在电商系统中的应用
后端·设计模式
云中飞鸿1 天前
结合项目阐述 设计模式:单例、工厂、观察者、代理
设计模式