设计模式-访问者模式

写在前面

Hello,我是易元,这篇文章是我学习设计模式时的笔记和心得体会。如果其中有错误,欢迎大家留言指正!


需求背景

通过构建一个文档处理系统,来深入学习访问者模式在实际生活中的应用,该系统需要处理多种类型的文档元素,如文本段落、图片、表格和代码块等,并对这些元素执行各种操作,如渲染、导出、拼写检查和内容统计等。 首先,使用传统方式实现处理系统,然后分析其中存在的问题,最后使用访问者模式进行重构,体会重构后带来的好处。

传统方式实现

抽象文档元素类

csharp 复制代码
public abstract class DocumentElement {

    private String content;

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

    /**
     * 渲染操作
     */
    public abstract void render();

    /**
     * 导出操作
     */
    public abstract String export();

    /**
     * 拼写检查操作
     */
    public abstract void spellCheck();

    public String getContent() {
        return content;
    }
}
  • DocumentElement 抽象类定义了所有文档元素共有的属性(内容) 和 操作(渲染、导出、拼写检查)。每个子类均需要实现所有抽象方法。

具体元素类

TextElement
typescript 复制代码
public class TextElement extends DocumentElement {

    public TextElement(String content) {
        super(content);
    }

    @Override
    public void render() {
        System.out.println("渲染文本段落: " + getContent());
    }

    @Override
    public String export() {
        return "<p>" + getContent() + "</p>";
    }

    @Override
    public void spellCheck() {
        System.out.println("对文本段落进行拼写检查: " + getContent());
    }
}
TableElement
csharp 复制代码
public class TableElement extends DocumentElement {

    private int rows;

    private int columns;

    public TableElement(String content, int rows, int columns) {
        super(content);
        this.rows = rows;
        this.columns = columns;
    }

    @Override
    public void render() {
        System.out.println("渲染表格: " + getRows() + "x" + getColumns() + ", 标题: " + getContent());
    }

    @Override
    public String export() {

        StringBuilder strBuilder = new StringBuilder();
        strBuilder.append("<table><caption>").append(getContent()).append("</caption>");

        for (int i = 0; i < rows; i++) {

            strBuilder.append("<tr>");
            for (int j = 0; j < columns; j++) {
                strBuilder.append("<td>数据</td>");
            }

            strBuilder.append("</tr>");
        }

        strBuilder.append("</table>");

        return strBuilder.toString();
    }

    @Override
    public void spellCheck() {
        System.out.println("对表格标题进行拼写检查: " + getContent());
    }

    public int getColumns() {
        return columns;
    }

    public int getRows() {
        return rows;
    }

}
ImageElement
typescript 复制代码
public class ImageElement extends DocumentElement {

    private String source;

    public ImageElement(String content, String source) {
        super(content);
        this.source = source;
    }

    @Override
    public void render() {
        System.out.println("渲染图片: " + getSource() + ", 描述: " + getContent());
    }

    @Override
    public String export() {
        return "<img src = " + getSource() + "alt = " + getContent() + " />";
    }

    @Override
    public void spellCheck() {
        System.out.println("对图片描述进行拼写检查: " + getContent());
    }

    public String getSource() {
        return source;
    }
}
CodeElement
typescript 复制代码
public class CodeElement extends DocumentElement {

    private String language;

    public CodeElement(String content, String language) {
        super(content);
        this.language = language;
    }

    @Override
    public void render() {
        System.out.println("渲染代码块: 语言 = " + getLanguage() + "\n" + getContent());
    }

    @Override
    public String export() {
        return "<pre><code class = " + getLanguage() + ">" + getContent() + "</code></pre>";
    }

    @Override
    public void spellCheck() {
        System.out.println("代码块不需要拼写检查");
    }
    
    public String getLanguage() {
        return language;
    }
    
}
Document
csharp 复制代码
public class Document {

    private String title;

    private List<DocumentElement> elements;

    public Document(String title) {
        this.title = title;
        this.elements = new ArrayList<>();
    }

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

    public void render() {
        System.out.println("渲染文档: " + title);
        for (DocumentElement element : elements) {
            element.render();
        }
    }

    public String export() {
        StringBuilder strBuilder = new StringBuilder();
        strBuilder.append("<html><head><title>").append(title).append("</title></head><body>");
        for (DocumentElement element : elements) {
            strBuilder.append(element.export());
        }
        strBuilder.append("</body></html>");
        return strBuilder.toString();
    }

    public void spellCheck() {
        System.out.println("对文档进行拼写检查: " + title);
        for (DocumentElement element : elements) {
            element.spellCheck();
        }
    }

}
  • Document 包含一个元素列表,并提供了渲染、导出和拼写检查整个文档的方法
测试类
typescript 复制代码
public class VisitorTest {

    @Test
    public void test_document() {
        Document document = new Document("Java编程指南");

        document.addElement(new TextElement("Java是一种广泛使用的编程语言。"));
        document.addElement(new ImageElement("Java Logo", "java_logo.png"));
        document.addElement(new TableElement("Java版本历史", 5, 3));
        document.addElement(new CodeElement("public class Hello{public static void main(String[] args){System.out.println("Hello, World!");}}", "Java"));

        System.out.println("======== 渲染文档 ========");
        document.render();

        System.out.println("\n ======== 导出文档 ========");
        String exportDoc = document.export();
        System.out.println(exportDoc);

        System.out.println("\n ======== 拼写检查 ========");
        document.spellCheck();

    }
}
执行结果
typescript 复制代码
======== 渲染文档 ========
渲染文档: Java编程指南
渲染文本段落: Java是一种广泛使用的编程语言。
渲染图片: java_logo.png, 描述: Java Logo
渲染表格: 5x3, 标题: Java版本历史
渲染代码块: 语言 = Java
public class Hello{public static void main(String[] args){System.out.println("Hello, World!");}}

 ======== 导出文档 ========
<html><head><title>Java编程指南</title></head><body><p>Java是一种广泛使用的编程语言。</p><img src = java_logo.pngalt = Java Logo /><table><caption>Java版本历史</caption><tr><td>数据</td><td>数据</td><td>数据</td></tr><tr><td>数据</td><td>数据</td><td>数据</td></tr><tr><td>数据</td><td>数据</td><td>数据</td></tr><tr><td>数据</td><td>数据</td><td>数据</td></tr><tr><td>数据</td><td>数据</td><td>数据</td></tr></table><pre><code class = Java>public class Hello{public static void main(String[] args){System.out.println("Hello, World!");}}</code></pre></body></html>

 ======== 拼写检查 ========
对文档进行拼写检查: Java编程指南
对文本段落进行拼写检查: Java是一种广泛使用的编程语言。
对图片描述进行拼写检查: Java Logo
对表格标题进行拼写检查: Java版本历史
代码块不需要拼写检查

Process finished with exit code 0

传统实现中存在的问题

添加新操作的困难

现在系统内需要增加一个内容统计功能,用于统计文档中各元素的字符数、单词数等信息,传统实现中,要修改大量代码。 首先 抽象类 DocumentElement 中添加一个新的抽象方法:

csharp 复制代码
    /* ============================== 增加统计文档内容功能 ============================== */
    public abstract void countStats();

然后,我们需要在每个具体元素类中实现这个抽象方法:

java 复制代码
// 在 CodeElement 中添加
@Override
public void countStats() {
    String code = getContent();
    int charCount = code.length();
    int lineCount = code.split("\n").length;
    System.out.println("代码块统计: " + charCount + " 个字符, " + lineCount + " 行代码");
}
 
// 在 ImageElement 中添加
@Override
public void countStats() {
    String description = getContent();
    int charCount = description.length();
    int wordCount = description.split("\s+").length;
    System.out.println("图片描述统计: " + charCount + " 个字符, " + wordCount + " 个单词");
}
 
@Override
public void countStats() {

    String caption = getContent();
    int charCount = caption.length();
    int wordCount = caption.split("\s+").length;
    int cellCount = getRows() * getColumns();

    System.out.println("表格统计: 标题包含 " + charCount + " 个字符, " + wordCount + " 个单词, 共 " + cellCount + " 个单元格");

}

// 在 TextElement 中添加
@Override
public void countStats() {
    String content = getContent();
    int charCount = content.length();
    int wordCount = content.split("\s+").length;
    System.out.println("文本段落统计: " + charCount + " 个字符, " + wordCount + " 个单词");
}

最后,我们还需要在 Document 类中添加一个方法来调用所有元素的统计方法:

csharp 复制代码
public void countStats() {
    System.out.println("统计文档内容: " + title);
    for (DocumentElement element : elements) {
        element.countStats();
    }
}

在修改过程中,不难发现:每次添加新操作,都需要修改所有的元素类,而在大型系统中,可能有几十甚至上百种元素类型,不仅工作量巨大,而且容易遗漏某些类的修改,导致发生错误。

代码分散,维护困难

在新增功能时不难看出,类似的一些功能,分散在每个类中,难以统一维护,当操作逻辑需要修改时,需要在多个文件中进行相同的修改。

违反开闭原则

传统实现中明显违反了开闭原则,每次添加新操作,我们都需要修改现有的元素类,而不是通过添加新的类或模块来实现。

访问者模式

什么是访问者模式

访问者模式是一种行为设计模式,允许在不修改已有类结构的情况下,定义作用于这些类的新操作,这种模式的核心思想是将操作与数据结构分离,使得可以在不改变元素类的前提下,定义新的操作。

通俗的来说,访问者模式就像是一个检查员,在各个场所进行检查。不同类型的检查员可以对同一组场所执行不同类型的检查,而场所本身不需要知道如何进行这些检查,只需要接待检察员即可。

访问者结构

  1. 访问者(Visitor): 定义对每种元素类型执行的操作,通常包含多个名为 visit 的方法,每个方法接受不同类型的元素做为参数。
  2. 具体访问者(Concrete Visitor): 实现访问者接口中声明的方法,为每种元素类型提供具体的操作实现。
  3. 元素(Element): 定义一个接受访问者的方法(通常命名为"accept"),该方法以一个访问者对象作为参数。
  4. 具体元素(Concrete Element): 实现元素接口,在"accept"方法中调用访问者的对应 "visit" 方法。
  5. 对象结构(Object structrue): 包含所有元素的集合,提供一个高层接口允许访问者访问其所有元素。

使用访问者模式重构

重构核心思想

重构的核心思想是将操作(如渲染、导出、拼写、检查等) 从元素类中分离出来,定义为独立的访问者类,元素类中只需要提供一个 accept 方法来接受访问者,而不需要知道具体的操作细节。

步骤

1.定义访问者接口

首先,定义一个访问者接口,其包含针对每种元素类型的visit方法:

csharp 复制代码
public interface DocumentElementVisitor {

    void visit(TextElement element);

    void visit(ImageElement element);

    void visit(TableElement element);

    void visit(CodeElement element);

}
  • DocumentElementVisitor 定义了访问者可以访问的所有元素类型,每个 visit 方法接受一个特定类型的元素作为参数。
2.修改元素接口

接下来,修改元素接口,新增一个 accept 方法来接受访问者:

typescript 复制代码
public abstract class DocumentElement {

    private String content;

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

    public String getContent() {
        return content;
    }

    /**
     * 接受访问者的方法
     *
     * @param visitor
     */
    public abstract void accept(DocumentElementVisitor visitor);
}
  • 移除了原来的 render()export()spellCheck() 方法,这些操作酱油访问者来实现。
3.实现具体元素类
typescript 复制代码
public class CodeElement extends DocumentElement {

    private String language;

    public CodeElement(String content, String language) {
        super(content);
        this.language = language;
    }

    public String getLanguage() {
        return language;
    }

    @Override
    public void accept(DocumentElementVisitor visitor) {
        visitor.visit(this);
    }
}

public class ImageElement extends DocumentElement {

    private String source;

    public ImageElement(String content, String source) {
        super(content);
        this.source = source;
    }

    public String getSource() {
        return source;
    }

    @Override
    public void accept(DocumentElementVisitor visitor) {
        visitor.visit(this);
    }
}

public class TableElement extends DocumentElement {

    private int rows;

    private int columns;

    public TableElement(String content, int rows, int columns) {
        super(content);
        this.rows = rows;
        this.columns = columns;
    }

    public int getRows() {
        return rows;
    }

    public int getColumns() {
        return columns;
    }

    @Override
    public void accept(DocumentElementVisitor visitor) {
        visitor.visit(this);
    }
}
  • 在元素类的 accept() 方法中,调用访问者的 visit() 方法,并将自身作为参数传入,这是实现双重分发的关键步骤。
4.实现具体访问者类
typescript 复制代码
public class ExportVisitor implements DocumentElementVisitor {

    private StringBuilder result = new StringBuilder();

    @Override
    public void visit(TextElement element) {
        result.append("<p>")
                .append(element.getContent())
                .append("</p>");
    }

    @Override
    public void visit(ImageElement element) {
        result.append("<img src=>")
                .append(element.getSource())
                .append(" alt=")
                .append(element.getContent())
                .append(" />");
    }

    @Override
    public void visit(TableElement element) {
        result.append("<table><caption>")
                .append(element.getContent())
                .append("</caption>");

        for (int i = 0; i < element.getRows(); i++) {
            result.append("<tr>");
            for (int j = 0; j < element.getColumns(); j++) {
                result.append("<td>数据</td>");
            }
            result.append("</tr>");
        }

        result.append("/table");
    }

    @Override
    public void visit(CodeElement element) {
        result.append("<pre><code class=")
                .append(element.getLanguage())
                .append(" >")
                .append(element.getContent())
                .append("</code></pre>");
    }

    public String getResult() {
        return result.toString();
    }
}

public class RenderVisitor implements DocumentElementVisitor {

    @Override
    public void visit(TextElement element) {
        System.out.println("渲染文本段落: " + element.getContent());
    }

    @Override
    public void visit(ImageElement element) {
        System.out.println("渲染图片: " + element.getSource() + " , 描述: " + element.getContent());
    }

    @Override
    public void visit(TableElement element) {
        System.out.println("渲染表格: " + element.getRows() + " x " + element.getColumns() + " ,标题: " + element.getContent());
    }

    @Override
    public void visit(CodeElement element) {
        System.out.println("渲染代码块: 语言=" + element.getLanguage() + "\n" + element.getContent());
    }

}

public class SpellCheckVisitor implements DocumentElementVisitor {

    @Override
    public void visit(TextElement element) {
        System.out.println("对文本段落进行拼写检查: " + element.getContent());
    }

    @Override
    public void visit(ImageElement element) {
        System.out.println("对图片描述进行拼写检查: " + element.getContent());
    }

    @Override
    public void visit(TableElement element) {
        System.out.println("对表格标题进行拼写检查: " + element.getContent());
    }

    @Override
    public void visit(CodeElement element) {
        System.out.println("代码块不需要拼写检查");
    }
}
  • 每个访问者类都实现了 DocumentElementVisitor 接口,并为每种元素类型提供了具体的操作实现。
5.修改文档类
typescript 复制代码
public class Document {

    private String title;

    private List<DocumentElement> elements;

    public Document(String title) {
        this.title = title;
        this.elements = new ArrayList<>();
    }

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

    public void accept(DocumentElementVisitor visitor) {
        for (DocumentElement element : elements) {
            element.accept(visitor);
        }
    }

    public String getTitle() {
        return title;
    }
}
  • 新增一个 accept() 方法,遍历所有元素让它们接受访问者。
6.测试类
typescript 复制代码
@Test
public void test_doc() {
    Document document = new Document("Java 编程指南");

    document.addElement(new TextElement("Java是一种广泛使用的编程语言。"));
    document.addElement(new ImageElement("Java Logo", "java_logo.png"));
    document.addElement(new TableElement("Java版本历史", 5, 3));
    document.addElement(new CodeElement("public class Hello{public static void main(String[] args){System.out.println("Hello, World!");}}", "Java"));

    System.out.println("======== 渲染文档 ========");
    RenderVisitor renderVisitor = new RenderVisitor();
    document.accept(renderVisitor);

    System.out.println("\n ======== 导出文档 ========");
    ExportVisitor exportVisitor = new ExportVisitor();
    document.accept(exportVisitor);
    String exportedDoc = "<html><head><title>" + document.getTitle() + "</title></head><body>" + exportVisitor.getResult() + "</body></html>";
    System.out.println(exportedDoc);

}
执行结果
typescript 复制代码
======== 渲染文档 ========
渲染文本段落: Java是一种广泛使用的编程语言。
渲染图片: java_logo.png , 描述: Java Logo
渲染表格: 5 x 3 ,标题: Java版本历史
渲染代码块: 语言=Java
public class Hello{public static void main(String[] args){System.out.println("Hello, World!");}}

 ======== 导出文档 ========
<html><head><title>Java 编程指南</title></head><body><p>Java是一种广泛使用的编程语言。</p><img src=>java_logo.png alt=Java Logo /><table><caption>Java版本历史</caption><tr><td>数据</td><td>数据</td><td>数据</td></tr><tr><td>数据</td><td>数据</td><td>数据</td></tr><tr><td>数据</td><td>数据</td><td>数据</td></tr><tr><td>数据</td><td>数据</td><td>数据</td></tr><tr><td>数据</td><td>数据</td><td>数据</td></tr>/table<pre><code class=Java >public class Hello{public static void main(String[] args){System.out.println("Hello, World!");}}</code></pre></body></html>

Process finished with exit code 0

重构后代码的可扩展性

实现统计功能

ini 复制代码
public class StatsVisitor implements DocumentElementVisitor {

    private int totalCharCount = 0;

    private int totalWordCount = 0;

    private int totalLinCount = 0;

    @Override
    public void visit(TextElement element) {
        String content = element.getContent();
        int charCount = content.length();
        int wordCount = content.split("\s+").length;

        totalCharCount += charCount;
        totalWordCount += wordCount;

        System.out.println("文本段落统计: " + charCount + " 个字符, " + wordCount + " 个单词");
    }

    @Override
    public void visit(ImageElement element) {
        String description = element.getContent();
        int charCount = description.length();
        int wordCount = description.split("\s+").length;

        totalCharCount += charCount;
        totalWordCount += wordCount;

        System.out.println("图片描述统计: " + charCount + "个字符, " + wordCount + " 个单词");
    }

    @Override
    public void visit(TableElement element) {
        String caption = element.getContent();
        int charCount = caption.length();
        int wordCount = caption.split("\s+").length;
        int cellCount = element.getRows() * element.getColumns();

        totalCharCount += charCount;
        totalWordCount += wordCount;

        System.out.println("表格统计: 标题包含 " + charCount + " 个字符," + wordCount + " 个单词,共 " + cellCount + " 个单元格");
    }

    @Override
    public void visit(CodeElement element) {
        String code = element.getContent();
        int charCount = code.length();
        int lineCount = code.split("\n").length;

        totalCharCount += charCount;
        totalLinCount += lineCount;

        System.out.println("代码块统计: " + charCount + " 个字符, " + lineCount + " 行代码");
    }

    public void printTotalStats() {
        System.out.println("\n总计统计: ");
        System.out.println("总字符数: " + totalCharCount);
        System.out.println("总单词数: " + totalWordCount);
        System.out.println("总代码行数: " + totalLinCount);
    }
}
  • StatsVisitor 新增统计访问者,不需要修改任何现有的类,所有与内容统计相关的代码都集中在一个访问者类中。

添加新元素类型: 公式元素

在传统实现中,我们仅需要创建一个新的元素类FormulaElement,继承自 DocumentElement,在这个类中实现所有抽象方法(render()export()spellCheck() 等)

在访问者模式中,将变得复杂起来,我们需要:

  1. 创建一个新的元素类 FormulaEment,继承自 DocumentElementVisitor
  2. DocumentElementVisitor 中添加一个新的 visit 方法
  3. 在所有现有的访问者类中实现这个新方法。 该功能添加将修改多个文件,当元素层次结构经常变化时,访问者模式可能不是最佳选择。

前后对比

  • 重构前
  1. 每个元素都包含所有操作的实现。
  2. 添加新操作需要修改所有元素类。
  3. 相关功能的代码分散在多个类中。
  • 重构后
  1. 元素类中只包含一个 accept() 方法,不包含具体操作的实现。
  2. 每种操作都由一个独立的访问者类实现。
  3. 添加新操作只需要添加一个新的访问者类,不需要修改元素类。
  4. 相关功能的代码集中在一个访问者类中。

长话短说

核心思想

  1. 操作与数据结构分离 访问者模式的核心思想是将操作与胡数据结构分离,在传统的面向对象设计中,我们通常将数据和操作封装在一个类中,而访问者模式采取了不同的方法:
  • 数据结构: 由元素类负责,它们只包含数据和基本行为。
  • 操作: 由访问者类负责,它们包含针对不同元素类型的操作实现。 这种分离,使得我们可以在不修改元素类的情况下添加新的操作。
  1. 双重分发机制 访问者模式通过 "双重分发" 的机制来解决方法调用问题,在Java等单分发语言中,方法的调用取决于两个因素即对象的运行时类型和方法的名称,但在某些情况下我们需要根据两个对象的类型来决定执行哪个方法,这就是双重分发问题,通常使用 instanceof 关键字进行类型检查,然后决定调用哪个方法,但访问者模式通过以下步骤实现了双重分发
  2. 元素调用访问者的 visit 方法,并将自身作为参数传入。
  3. 由于Java的方法重载机制,会根据参数的静态类型选择合适的 visit方法。
  4. 访问者可以根据元素的具体类型执行相应的操作。 这种机制避免了显示的类型检查及类型转换,使得代码更加易于维护。
  5. 集中相关操作 访问者模式将相关的操作集中在一个访问者类中,而不是分散在多个元素类中,这种集中有以下好处:
  6. 简化修改,当需要修改某个操作的逻辑时,只需要修改一个访问者类,而不是多个元素类。
  7. 提高内聚性,相关的功能代码集中在一起,便于理解和维护。
  8. 便于优化,可以在访问者类中实现全局优化,而不受元素类的限制。
  9. 状态累积 访问者可以在遍历过程中累积状态,在处理复杂对象结构时非常有用,例如,在文档处理中,统计访问者可以累积各种统计信息,并在遍历结束后提供总计结果。

适用场景

  1. 对象结构相对稳定,但操作经常变化 当系统中的对象结构相对稳定,但需要经常添加新的操作时,可以使用访问者,在这种情况下,访问者模式允许我们在不修改元素类的情况下添加新的操作。
  2. 需要对复杂对象结构执行多种不同操作 当需要对一个复杂的对象结构执行多种不同且不相关的操作时,访问者模式可以将这些操作分离到不同的访问者类中。避免使元素类变的膨胀。
  3. 需要跨越多个不同类的对象进行操作 当操作需要处理多种不同类型的对象,并且每种对象的处理逻辑不同时,访问者模式可以避免使用类型检查和类型转换。

实施步骤

1.定义元素接口

定义元素接口,应该包含一个accept方法,该方法接受一个访问者对象作为参数

csharp 复制代码
public interface Element{
 void accept(Visitor visitor);
}
2.实现具体元素类

实现具体的元素类,它们应该实现元素接口,并在accept方法中调用访问者的相应 visit 方法

typescript 复制代码
public class ConcreteElementA implements Element {
 private String data;
 
 public ConcreteElementA(String data){
  this.data = data;
 }
 
 public String getData(){
  return data;
 }
 
 @Override
 public void accept(Visitor visitor){
  visitor.visit(this);
 }
}

public class ConcreteElementB implements Element{
 private int value;
 
 public ConcreteElementB(int value){
  this.value = value;
 }
 
 public int getValue(){
  return value;
 }
 
 @Override
 public void accept(Visitor visitor){
  visitor.visit(this);
 }
}
定义访问者接口

定义访问者接口,接口中为每种元素类型定义一个 visit 方法:

csharp 复制代码
public interface Visitor{
 void visit(ConcreteElementA element);
 
 void visit(ConcreteElementB element);
}
4.实现具体访问者类

实现访问者接口,并为每种元素类型提供具体的操作实现:

typescript 复制代码
public class ConcreteVisitor1 implements Visitor{
 
 @Override
 public void visit(ConcreteElementA element){
  System.out.println("Visitor1:Processing" + element.getValue());
 }
 
 @Override
 public void visit(ConcreteElementB element){
  System.out.println("Visitor1:Processing" + element.getValue());
 }
}

public class ConcreteVisitor2 implements Visitor{
 
 @Override
 public void visit(ConcreteElementA element){
  System.out.println("Visitor2:Processing" + element.getValue());
 }

 @Override
 public void visit(ConcreteElementB element){
  System.out.println("Visitor2:Processing" + element.getValue());
 }
}
5.创建对象结构

可以是一个简单的集合,也可以是一个复杂的组合结构:

typescript 复制代码
public class ObjectStructure{
 private List<Element> elements = new ArrayList<>();
 
 public void addElement(Element element){
  elements.add(element);
 }
 
 public void removeElement(Element element){
  elements.remove(element);
 }
 
 public void accept(Visitor visitor){
  for(Element element : elements){
   element.accept(visitor);
  }
 }
}
6.客户端代码
java 复制代码
public class Client{
 public static void main(String[] args){
  ObjectStructure structrue = new ObjectStructure();
  structrue.addElement(new ConcreteElementA("Element A"));
  structrue.addElement(new ConcreteElementB(42));
  
  Visitor visitor1 = new ConcreteVisitor1();
  Visitor visitor2 = new ConcreteVisitor2();
  
  // 使用访问者1处理对象结构
  System.out.println("Using Visitor1: ");
  structrue.accept(visitor1);
  
  // 使用访问者2处理对象结构
  System.out.println("Using Visitor2: ");
  structrue.accept(visitor2);
  
 }
}

访问者模式的最佳实践

1.保持元素接口简单

元素接口应尽可能简单,只包含必要的方法,过于复杂的元素接口可能会导致访问者类也变得复杂。

2.使用访问者接口

即使只有一个具体访问者类,也应该定义一个访问者接口,这样可以在将来添加新的访问者类时,不需要修改元素类。

3.考虑使用默认实现

如果有多个访问者类,并且它们对某些元素类型的处理逻辑相似,可以考虑在访问者接口中提供默认实现,或者创建一个抽象访问者类。

less 复制代码
public abstract class BaseVisitor implements Visitor{
 
 @Override
 public void visit(ConcreteElementA element){
  // 默认实现
 }
 
 @Override
 public void visit(ConcreteElementB element){
  // 默认实现
 }
}

4.避免修改元素的状态

访问者应该主要用于执行操作,而不是修改元素的状态,如果需要修改元素的状态,应该谨慎考虑,并确保不会破坏元素的一致性。

相关推荐
砖头拍死你37 分钟前
51单片机如何使用printf打印unsigned long的那些事
java·前端·51单片机
用户1512905452201 小时前
css —pointer-events属性_css pointer-events
前端
帅夫帅夫1 小时前
Axios 入门指南:从基础用法到实战技巧
前端
云边散步1 小时前
《校园生活平台从 0 到 1 的搭建》第四篇:微信授权登录前端
前端·javascript·后端
讨厌吃蛋黄酥1 小时前
React样式冲突终结者:CSS模块化+Vite全链路实战指南🔥
前端·javascript·react.js
架构师沉默1 小时前
让我们一起用 DDD,构建更美好的软件世界!
java·后端·架构
噔噔4281 小时前
使用webworker优化大文件生成hash的几种方式
前端
Hilaku1 小时前
原生<dialog>元素:别再自己手写Modal弹窗了!
前端·javascript·html
研究司马懿1 小时前
【Golang】Go语言函数
开发语言·后端·golang
tuokuac1 小时前
创建的springboot工程java文件夹下还是文件夹而不是包
java·spring boot·后端