访问者 Visitor

访问者模式是一种行为设计模式,它能将算法与其所作用的对象隔离开来。允许在运行时将一个或多个操作应用于一组对象,将操作对象结构分离。

访问者模式是以行为(某一个操作)作为扩展对象功能的出发点,在不改变已有类的功能的前提下进行批量扩展。

🎈 在学习访问者模式之前,需要想搞清楚以下概念:

📜 双分派是在执行一个方法时,不仅要根据对象运行时的类型来决定,还要根据方法参数运行时的类型来决定。

📜 单分派是在执行一个方法时,不仅要根据对象运行时的类型来决定,还要根据方法参数编译时的类型来决定。

💡 Java 支持单分派不支持多分派!Java 重载方法的参数类型在编译时就已经确定了。就算我们使用 Parent value = new Child() 去调用重载方法 method(value)(🎗️ method(Parent)、🎗️ method(Child)),执行的也只会是 method(Parent)。因为单分派在编译时方法参数类型就已经确定,所以就算使用父类类型来保存子类对象,也只能调用父类参数类型的重载方法。


为什么要使用?

访问者模式的对象职责:

在运行时能够将一个或多个操作应用于一组对象,将操作与对象结构分离。

👉 解决编程部分语言不支持动态双分派的能力。

👉 需要动态绑定不同的对象和对象操作。

👉 通过行为与对象结构的分离实现对象的职责分离,提高代码复用性。

💡 对于一些数据元素相对稳定,而访问方法或操作方法多变的情况,访问者模式是一个好的解决方案。


模式结构

  • 访问者(Visitor)接口声明了一系列以对象结构的具体元素为参数的访问者方法。如果编程语言支持重载,这些方法的名称可以是相同的,但是其参数一定是不同的。
  • 具体访问者(Concrete Visitor)会为不同的具体元素类实现相同行为的几个不同版本。
  • 元素(Element)接口声明了一个方法来“接收”访问者。该方法必须有一个参数被声明为访问者接口类型。
  • 具体元素(Concrete Element)必须实现接收方法。该方法的目的是根据当前元素类将其调用重定向到相应访问者的方法。请注意,即使元素基类实现了该方法,所有子类都必须对其进行重写并调用访问者对象中的合适方法。
  • 客户端(Client)通常会作为集合或其他复杂对象(例如一个组合树)的代表。客户端通常不知晓所有的具体元素类,因为它们会通过抽象接口与集合中的对象进行交互。

访问者模式的类图:

访问者模式是一种双分派模式,所以对于 visit(element: ConcreteElementA),它知道 ConcreteElementA 中独有的 featureA 方法。Client 通过 element.accept(new ConcreteVisitor()),为需要访问的 element 传入指定的 Visitor。那么通过在 accept(visitor: Visitor) 中执行 visitor.visit(this) 来实现传入可变类型的参数。


模式实现

该示例使用访问者模式来实现双分派效果,使其对象类型和方法参数类型都在运行时决定。

示例程序的类图

代码实现

访问者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package example;

/** 访问者 */
public interface Visitor {
/**
* 为ConcreteElementA定制访问方法
* @param element 元素
*/
void visit(ConcreteElementA element);

/**
* 为ConcreteElementB定制访问方法
* @param element 元素
*/
void visit(ConcreteElementB element);
}
具体访问者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package example;

/** 具体访问者 */
public class ConcreteVisitor implements Visitor {
/**
* 为ConcreteElementA定制访问方法
* @param element 元素
*/
@Override
public void visit(ConcreteElementA element) {
element.featureA();
}

/**
* 为ConcreteElementB定制访问方法
* @param element 元素
*/
@Override
public void visit(ConcreteElementB element) {
element.featureB();
}
}
元素
1
2
3
4
5
6
7
8
9
10
package example;

/** 元素 */
public interface Element {
/**
* 元素访问入口
* @param visitor 访问者
*/
void accept(Visitor visitor);
}
具体元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package example;

/** 具体元素A */
public class ConcreteElementA implements Element {
/**
* 元素访问入口
* @param visitor 访问者
*/
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}

/** ConcreteElementA特有方法 */
public void featureA() {
System.out.println("执行ConcreteElementA特有方法featureA...");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package example;

/** 具体元素B */
public class ConcreteElementB implements Element {
/**
* 元素访问入口
* @param visitor 访问者
*/
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}

/** ConcreteElementB特有方法 */
public void featureB() {
System.out.println("执行ConcreteElementB特有方法featureB...");
}
}

代码测试

1
2
3
4
5
6
7
8
9
10
11
12
import example.*;

/** 测试访问者模式 */
public class Test {
public static void main(String[] args) {
Element elementA = new ConcreteElementA();
Element elementB = new ConcreteElementB();
Visitor visitor = new ConcreteVisitor();
elementA.accept(visitor);
elementB.accept(visitor);
}
}

输出结果

1
2
执行ConcreteElementA特有方法featureA...
执行ConcreteElementB特有方法featureB...

从案例中,我们可以得出访问者模式的几点特征:

  1. 这是一种双分派模式,我们的变量类型均为接口(即父类),却可以调用不同子类的不同方法。
  2. 我们可以给 Element 类添加任意多个操作(即添加 ConcreteVisitor)。这不会影响到原有的代码结构。
  3. 这种模式无法自由添加或删除 ConcreteElement 类。因为一旦有新的 ConcreteElement,那么每个 Visitor 都要添加新的 visit 方法。

常用场景和解决方案

  • 对象的数据结构相对稳定,而操作却经常变化的时候。
  • 需要将数据结构与不常用的操作进行分离的时候。
  • 需要在运行时动态决定使用哪些对象和方法的时候。
  • 需要在不同类的一组对象上执行同一个操作。
  • 当某个行为仅在类层次结构中的一些类中有意义,而在其他类中没有意义时,可使用该模式。你只需实现接收相关类的对象作为参数的访问者方法并将其他方法留空即可。

模式的优缺点

优点 缺点
开闭原则。你可以引入在不同类对象上执行的新行为,且无需对这些类做出修改。 每次在元素层次结构中添加或移除一个类时,你都要更新所有的访问者。
单一职责原则。可将同一行为的不同版本移到同一个类中。 在访问者同某个元素进行交互时,它们可能没有访问元素私有成员变量和方法的必要权限。
访问者对象可以在与各种对象交互时收集一些有用的信息。当你想要遍历一些复杂的对象结构(例如对象树),并在结构中的每个对象上应用访问者时,这些信息可能会有所帮助。

使用访问者模式的优势

  • 简化客户端操作。比如,扫描文件时,对客户端来说只需要执行扫描,不需要知道文件类型、如何读取文件等操作。
  • 增加新的访问操作和访问者会非常便捷。
  • 通过行为能够快速组合一组复杂的对象结构。

使用访问者模式的劣势

  • 增加新的数据结构困难,需要变更操作。
  • 具体元素在变更时需要修改代码,容易引入问题。

拓展知识

  • 可以使用访问者对整个组合模式树执行操作。
  • 可以同时使用访问者和迭代器模式来遍历复杂数据结构,并对其中的元素执行所需操作,即使这些元素所属的类完全不同。


🔙 设计模式

📌最后:希望本文能够给您提供帮助,文章中有不懂或不正确的地方,请在下方评论区💬留言!

🔗参考文献:

🌐 设计模式 –refactoringguru

▶️ bilibili-趣学设计模式;黄靖锋. –拉勾教育

📖 图解设计模式 /(日)结城浩著;杨文轩译. –北京:人民邮电出版社,2017.1