Java访问者模式源码剖析及使用场景

访问者模式

一、介绍

Java 中的访问者(Visitor)模式是一种行为型设计模式,它将数据结构与数据操作分离,使得在不修改数据结构的情况下可以增加新的操作。该模式主要包含以下几个角色:

  1. 抽象访问者(Visitor): 定义了一个访问具体元素的接口,为每个具体元素类声明一个访问操作。
  2. 具体访问者(ConcreteVisitor): 实现了抽象访问者角色所声明的接口,定义了相应的访问操作。
  3. 抽象元素(Element): 声明一个接受访问操作的接口,这个接口的入口参数是抽象访问者角色。
  4. 具体元素(ConcreteElement): 实现了抽象元素角色所定义的接受访问操作的接口。
  5. 对象结构(ObjectStructure): 主要是用来存储元素对象的容器,提供让访问者对象遍历容器中所有元素的方法。

优点:

  • 符合单一职责原则,将数据结构与数据操作分离,提高了代码的可维护性。
  • 增加新的操作非常方便,只需要添加一个新的访问者即可,而不需要修改已有的数据结构代码。
  • 访问者模式使得数据结构对象的操作与维护相分离,符合开放-封闭原则。

缺点:

  • 增加新的数据结构类比较困难,因为每增加一个新的数据结构类,就需要修改所有的访问者实现。
  • 具体元素对访问者公布细节,会带来一些对象状态的透明性问题。
  • 访问者模式具有一定的复杂性,使用不当会增加系统的复杂度。

理解:

假设是一名老师,要去给不同的年级的学生上课。不同年级的学生,他们的学习能力不同,需要采取不同的教学方式。

  • 首先,学生就是我们的"元素(Element)",不同年级的学生就是不同的"具体元素(ConcreteElement)"。
  • 然后,你作为老师就相当于一个"访问者(Visitor)"。不同的授课方式就是"具体访问者(ConcreteVisitor)"。

现在你要上课了,步骤如下:

  1. 你(访问者)进入一个教室(对象结构),里面坐着不同年级的学生们(元素们)。
  2. 你会观察一下教室里都有哪些年级的学生,比如一年级学生、二年级学生等等。
  3. 根据不同年级学生的情况,你会采取不同的授课方式。比如对一年级生,你会使用简单生动的教学方式;对二年级生,你就可以讲一些深入的知识了。

这里的关键点是:

  • 不同年级的学生(元素)并不知道你(访问者)会采取什么样的授课方式,它们只知道接受授课。
  • 而你作为老师(访问者),可以根据不同年级的学生采取不同的授课方式。

这样做的好处是什么呢?

  1. 新增一个年级的学生(元素)非常容易,只需要新增一个"具体元素"就行了,不需要修改之前所有的"访问者"。
  2. 如果要新增一种授课方式(访问者),你只需要新增一个"具体访问者"就行了,不需要修改所有的"元素"。

这样就可以很好地遵守"开闭原则",使得系统扩展相对容易,并提高代码的可维护性。

总的来说,访问者模式的核心思想就是:"将数据结构和作用于结构上的操作解耦,使得操作集合可相对自由地扩展"。这样不仅令系统数据结构的扩展更加灵活,也使给定的操作集更具统一性。

二、报表系统开发

在实际项目中,访问者模式常常被用于需要对一组异构元素执行不同操作的场景。下面是一个在报表系统中应用访问者模式的场景。

需求描述:我们需要开发一个报表系统,可以生成不同类型的报表,包括表格报表(TabularReport)和数据透视表报表(PivotTableReport)。每种报表都需要支持多种输出格式,如Excel、PDF和HTML。

使用访问者模式的优势:

  1. 报表类型和输出格式是相互独立的,我们可以很方便地添加新的报表类型或输出格式,而不需要修改现有代码。
  2. 报表生成逻辑与报表数据结构解耦,提高了代码的可维护性和可扩展性。
java 复制代码
// 抽象访问者,定义访问表格报表和数据透视表报表的方法。
interface ReportVisitor {
    void visitTabularReport(TabularReport report);
    void visitPivotTableReport(PivotTableReport report);
}

// 具体访问者 - Excel 输出
class ExcelVisitor implements ReportVisitor {
    @Override
    public void visitTabularReport(TabularReport report) {
        // 生成 Excel 表格报表
        System.out.println("Generating Excel tabular report...");
    }

    @Override
    public void visitPivotTableReport(PivotTableReport report) {
        // 生成 Excel 数据透视表报表
        System.out.println("Generating Excel pivot table report...");
    }
}

// 具体访问者 - PDF 输出
class PdfVisitor implements ReportVisitor {
    // ...
}

// 具体访问者 - HTML 输出
class HtmlVisitor implements ReportVisitor {
    // ...
}

// 抽象元素
interface Report {
    void accept(ReportVisitor visitor);
}

// 具体元素 - 表格报表
class TabularReport implements Report {
    private String data;

    public TabularReport(String data) {
        this.data = data;
    }

    @Override
    public void accept(ReportVisitor visitor) {
        visitor.visitTabularReport(this);
    }

    // 其他方法...
}

// 具体元素 - 数据透视表报表
class PivotTableReport implements Report {
    private String data;

    public PivotTableReport(String data) {
        this.data = data;
    }

    @Override
    public void accept(ReportVisitor visitor) {
        visitor.visitPivotTableReport(this);
    }

    // 其他方法...
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        List<Report> reports = new ArrayList<>();
        reports.add(new TabularReport("Tabular report data"));
        reports.add(new PivotTableReport("Pivot table report data"));

        ReportVisitor excelVisitor = new ExcelVisitor();
        for (Report report : reports) {
            report.accept(excelVisitor);
        }

        // 也可以使用其他访问者生成 PDF 或 HTML 报表
    }
}
  1. ReportVisitor 接口定义了访问表格报表和数据透视表报表的方法。
  2. ExcelVisitorPdfVisitorHtmlVisitor 是具体的访问者实现,分别用于生成不同格式的报表。
  3. Report 接口定义了接受访问者访问的方法 accept()
  4. TabularReportPivotTableReport 是两个具体的报表实现,它们实现了 accept() 方法,将自身作为参数传递给访问者的访问操作。
  5. 在客户端代码中,我们创建了一个报表列表,包含表格报表和数据透视表报表。然后创建了一个 Excel 访问者,并让它访问每个报表元素,生成相应的 Excel 报表。

通过使用访问者模式,我们可以很方便地添加新的报表类型或输出格式,而无需修改现有代码。例如,如果需要添加一种新的报表类型,只需创建一个新的具体报表类并实现 Report 接口即可。如果需要添加一种新的输出格式,只需创建一个新的具体访问者类并实现 ReportVisitor 接口即可。

三、MyBatis中如何使用访问者模式?

MyBatis 中,访问者模式被广泛地用于处理映射文件(Mapper XML)的解析和执行 SQL 查询操作。具体来说,MyBatis 使用了访问者模式来实现对 Mapper XML 文件中定义的不同元素(如 select、insert、update、delete 等)的解析和执行

在 MyBatis 中,访问者模式的使用主要集中在 org.apache.ibatis.parsing 包中,用于解析映射配置文件和动态 SQL 语句。我们重点分析 GenericTokenParser 类和相关组件。

1. 抽象访问者和抽象元素

在 MyBatis 中,抽象访问者和抽象元素分别定义在 TokenHandlerToken 接口中:

java 复制代码
// 抽象访问者
public interface TokenHandler {
    String handleToken(String content);
}

// 抽象元素
public interface Token {
    String getContent();
    void accept(TokenHandler handler);
}
  • TokenHandler 接口定义了访问者如何处理标记的方法。
  • Token 接口定义了元素如何接受访问者的访问操作。

2. 具体访问者和具体元素

MyBatis 提供了一些具体的访问者和元素实现,例如:

java 复制代码
// 具体访问者 - 处理变量标记
public class VariableTokenHandler implements TokenHandler {
    private PropertyParser propertyParser;

    public VariableTokenHandler(Properties properties) {
        this.propertyParser = new PropertyParser(properties);
    }

    @Override
    public String handleToken(String content) {
        return propertyParser.parse(content);
    }
}

// 具体元素 - 变量标记
public class VariableToken implements Token {
    private String content;

    public VariableToken(String content) {
        this.content = content;
    }

    @Override
    public String getContent() {
        return content;
    }

    @Override
    public void accept(TokenHandler handler) {
        replaceBy(handler.handleToken(content));
    }

    // ...
}
  • VariableTokenHandler 是一个具体的访问者实现,用于处理变量标记。
  • VariableToken 是一个具体的元素实现,表示一个变量标记,它会将自身传递给访问者进行处理。

3. 使用访问者模式解析标记

GenericTokenParser 类中,MyBatis 使用访问者模式解析配置文件和动态 SQL 语句中的标记。以下是关键代码:

java 复制代码
public class GenericTokenParser {
    private final String openToken;
    private final String closeToken;
    private final TokenHandler handler;

    // ...

    public String parse(String text) {
        // ...
        int start = text.indexOf(openToken);
        int end = text.indexOf(closeToken, start + openToken.length());
        if (start > -1 && end > start) {
            StringBuilder builder = new StringBuilder();
            // ...
            // 创建标记元素并让访问者处理它
            Token token = new VariableToken(text.substring(start + openToken.length(), end));
            token.accept(handler);
            builder.append(handler.handleToken(token.getContent()));
            // ...
        }
        // ...
    }
}

parse 方法中,MyBatis 会解析文本,识别出标记的起始和结束位置。然后,它会创建一个具体的标记元素(如 VariableToken)。接下来,它会让具体的访问者(如 VariableTokenHandler)访问和处理这个标记元素。

通过这种方式,MyBatis 将标记的解析操作和标记的数据结构分离,符合访问者模式的设计思想。

4. 扩展访问者和元素

由于 MyBatis 使用了访问者模式,因此扩展新的标记类型和处理逻辑变得非常方便。只需要实现新的具体访问者和具体元素,并在 GenericTokenParser 中进行调用即可。

例如,如果需要添加一种新的标记类型 MyToken,我们可以创建如下的具体访问者和具体元素:

java 复制代码
// 具体访问者
public class MyTokenHandler implements TokenHandler {
    @Override
    public String handleToken(String content) {
        // 处理 MyToken 的逻辑
        return "...";
    }
}

// 具体元素
public class MyToken implements Token {
    private String content;

    public MyToken(String content) {
        this.content = content;
    }

    @Override
    public String getContent() {
        return content;
    }

    @Override
    public void accept(TokenHandler handler) {
        // 将自身传递给访问者
        if (handler instanceof MyTokenHandler) {
            replaceBy(handler.handleToken(content));
        }
    }
}

然后,在 GenericTokenParser 中添加相应的处理逻辑:

java 复制代码
public String parse(String text) {
    // ...
    if (isMyToken(text)) {
        Token token = new MyToken(extractContent(text));
        token.accept(myTokenHandler);
        // ... 处理结果
    }
    // ...
}
相关推荐
m0_748236112 分钟前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
倔强的石头10610 分钟前
【C++指南】类和对象(九):内部类
开发语言·c++
ProtonBase13 分钟前
如何从 0 到 1 ,打造全新一代分布式数据架构
java·网络·数据库·数据仓库·分布式·云原生·架构
Watermelo61714 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
乐之者v20 分钟前
leetCode43.字符串相乘
java·数据结构·算法
半盏茶香1 小时前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法
Evand J2 小时前
LOS/NLOS环境建模与三维TOA定位,MATLAB仿真程序,可自定义锚点数量和轨迹点长度
开发语言·matlab
LucianaiB2 小时前
探索CSDN博客数据:使用Python爬虫技术
开发语言·爬虫·python
Ronin3052 小时前
11.vector的介绍及模拟实现
开发语言·c++
计算机学长大白3 小时前
C中设计不允许继承的类的实现方法是什么?
c语言·开发语言