• 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. 报表生成逻辑与报表数据结构解耦,提高了代码的可维护性和可扩展性。
    // 抽象访问者,定义访问表格报表和数据透视表报表的方法。
    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    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 接口中:

    // 抽象访问者
    public interface TokenHandler {
        String handleToken(String content);
    }
    
    // 抽象元素
    public interface Token {
        String getContent();
        void accept(TokenHandler handler);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • TokenHandler 接口定义了访问者如何处理标记的方法。
    • Token 接口定义了元素如何接受访问者的访问操作。

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

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

    // 具体访问者 - 处理变量标记
    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));
        }
    
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • VariableTokenHandler 是一个具体的访问者实现,用于处理变量标记。
    • VariableToken 是一个具体的元素实现,表示一个变量标记,它会将自身传递给访问者进行处理。

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

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

    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()));
                // ...
            }
            // ...
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

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

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

    4. 扩展访问者和元素

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

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

    // 具体访问者
    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));
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

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

    public String parse(String text) {
        // ...
        if (isMyToken(text)) {
            Token token = new MyToken(extractContent(text));
            token.accept(myTokenHandler);
            // ... 处理结果
        }
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  • 相关阅读:
    mybatis循环插入
    网络安全(黑客)自学
    五、伊森商城 前端基础-Vue p24
    Java基础之接口
    servlet---前端代码发送
    监听DIV元素尺寸变化
    零信任迈入2.0时代
    自动化测试和测试自动化你分的清楚吗?
    力扣 -- 279. 完全平方数(完全背包问题)
    8.6 矢量图层点要素基于规则(Rule-based)渲染使用
  • 原文地址:https://blog.csdn.net/weixin_44716935/article/details/136672151