设计模式之访问者模式

概述

这篇文章介绍了访问者模式(Visitor Pattern)的概念、优缺点、使用场景以及代码示例。

访问者模式的定义

Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于用于这些元素的新的操作。

——《设计模式之禅》

访问者模式的目的是将数据结构和操作分离,用于数据结构固定,操作易变的场景,例如给不同的主管生成不同的报表。

UML 类图

  • Visitor 抽象访问者,接口或抽象类,申明访问者可以访问哪些元素,通过 visit 方法的参数来定义哪些对象可以访问。
  • ConcreateVisitor 具体访问者,定义了对每个元素访问时的具体操作
  • Element 抽象元素,接口或抽象类,申明接受哪一类访问者,通过 accept 方法参数来指定。
  • ConcreateElement 具体元素,实现 accept 方法,通常都是 visitor.visit(this)
  • ObjectStruture 结构对象,元素产生者,一般容纳在多个不同类、不同接口的容器,如 List、Set、Map 等。

适用场景

  • 对象结构比较稳定,但经常需要在此对象结构上定义新操作。比如人固定分为男人、女人两大类,且基本不会扩展出其他类,但男人和女人的分歧却非常多。
  • 需要对一个对象结构中的对象进行很多不同的且不相关的操作,需要避免这些操作影响这些类,也不希望增加操作时修改这些类。比如对同一堆数据进行统计、生成报表或数据挖掘,就需要从不同的角度来分析

优缺点

优点

  • 符合单一职责原则:具体的元素实现类(ConcreateElement)负责数据加载,Visitor 实现具体操作,职责分明。
  • 优秀的扩展性:由于职责分明,所以易于扩展,要增加新的操作方式只需要增加 Visitor 即可实现。
  • 灵活性非常高:对具体的元素实现类可以进行不同的处理,这些处理使用访问者模式实现很容易,而且代码更加优雅,不然可能需要使用很多 if-else 来判断。

缺点

  • 具体元素对访问者公布细节:访问者需要知道其他类的细节才能实现访问操作,违反了迪米特原则。
  • 具体元素变更困难:一旦具体元素出现变更,涉及到的 Visitor 都需要修改。
  • 违法依赖倒置原则:访问者的 visit 方法依赖于具体元素,而不是依赖于抽象类。

示例

假设有一家公司,员工分为普通员工和管理者,公司 CEO 只关注管理者的绩效、管理得分和薪资,CTO 关注普通员工的绩效、关注管理者的绩效和管理得分,使用访问者模式完成设计。

员工抽象类

员工抽象类的目的是给所有具体类指定 accept 方法,也可以使用接口。这里需要指定一些共有的属性,所以使用的是抽象类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class Staff {
// 姓名
private String name;
// 薪资
private Integer salary;
// 绩效得分
private Integer performance;

public Staff(String name, Integer salary, Integer performance) {
this.name = name;
this.salary = salary;
this.performance = performance;
}

// accept 方法来接收访问者
protected abstract void accept(Visitor visitor);

// 省略了 setter、getter 方法
}

员工实现类

这里有两个员工实现类,分别是普通员工和管理者,实现了抽象类中的 accept 方法,方法内容都是 visitor.visit(this);

普通员工
1
2
3
4
5
6
7
8
9
10
11
public class Employee extends Staff {

public Employee(String name, Integer salary, Integer performance) {
super(name, salary, performance);
}

@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
管理者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Manager extends Staff {
// 管理得分
private Integer manageScore;

public Manager(String name, Integer salary, Integer performance,Integer manageScore) {
super(name, salary, performance);
this.manageScore = manageScore;
}

@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
// 省略 setter、getter 方法
}

访问者接口

在访问者接口中要提供访问所有员工实现类的访问方法。访问者中的 visit 方法的参数是具体的员工实现类,而不是抽象类,这里是访问者模式违反迪米特法则的地方。如果数据结构增加了新的数据类型,就需要修改访问者接口,相应的也要修改所有的实现类,这就是访问者模式要求元素结构稳定的原因。

1
2
3
4
5
6
public interface Visitor {
// 访问普通员工的方法
void visit(Employee e);
// 访问管理者的方法
void visit(Manager e);
}

访问者实现类

具体的访问者需要实现访问者接口,实现访问所有元素结构的方法,对不同的数据类型,处理的方法不同。具体访问者可以根据业务需要进行调整或增加,只需要实现 Visitor 接口即可,这就是访问者模式灵活的关键。

CEO

CEO 只关注管理者的绩效、管理得分和薪资,不关注普通员工。

1
2
3
4
5
6
7
8
9
10
11
public class Ceo implements Visitor {
@Override
public void visit(Employee e) {
// 不关注普通员工
}

@Override
public void visit(Manager e) {
System.out.println(this.getClass().getName() + " 关注 " + e.getName() + ",绩效:" + e.getPerformance() + ",管理得分:" + e.getManageScore() + ", 工资:" + e.getSalary());
}
}
CTO

CTO 关注普通员工绩效,关注管理者绩效和管理得分。

1
2
3
4
5
6
7
8
9
10
11
public class Cto implements Visitor {
@Override
public void visit(Employee e) {
System.out.println(this.getClass().getName() + " 关注 " + e.getName()+" 绩效:"+e.getPerformance());
}

@Override
public void visit(Manager e) {
System.out.println(this.getClass().getName() + " 关注 " + e.getName() + " 绩效:" + e.getPerformance() + ",管理得分:" + e.getManageScore());
}
}

结构对象

用来提供数据元素,实际项目中一般是由持久层提供。

1
2
3
4
5
6
7
8
9
10
public class ObjectStruture {
public static Staff createElement(){
Random random = new Random();
if (random.nextInt(100)>50){
return new Employee("员工",random.nextInt(5000)+5000,random.nextInt(100));
}else{
return new Manager("管理者",random.nextInt(5000)+10000,random.nextInt(100),random.nextInt(100));
}
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Client {
public static void main(String[] args) {
List<Staff> staffList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
staffList.add(ObjectStruture.createElement());
}

Visitor ceo = new Ceo();
for (Staff elem : staffList) {
elem.accept(ceo);
}

Visitor cto = new Cto();
for (Staff elem : staffList) {
elem.accept(cto);
}
}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Ceo 关注 管理者,绩效:76,管理得分:74, 工资:13852
Ceo 关注 管理者,绩效:69,管理得分:72, 工资:14417
Ceo 关注 管理者,绩效:18,管理得分:98, 工资:12138
Ceo 关注 管理者,绩效:53,管理得分:54, 工资:11972
Ceo 关注 管理者,绩效:50,管理得分:99, 工资:10379
Ceo 关注 管理者,绩效:60,管理得分:59, 工资:12208
Cto 关注 管理者 绩效:76,管理得分:74
Cto 关注 管理者 绩效:69,管理得分:72
Cto 关注 管理者 绩效:18,管理得分:98
Cto 关注 管理者 绩效:53,管理得分:54
Cto 关注 员工 绩效:47
Cto 关注 员工 绩效:23
Cto 关注 管理者 绩效:50,管理得分:99
Cto 关注 员工 绩效:33
Cto 关注 员工 绩效:18
Cto 关注 管理者 绩效:60,管理得分:59

访问者实现类

参考资料

总结

《设计模式之禅》中说访问者模式是一种集中规整模式,特别适合大规模重构的项目,在这一个阶段需求已经非常清晰,原系统功能点也已经明确,通过访问者模式可以很容易的把一些功能进行梳理,达到最终目的——功能集中化,如一个统一的报表运算、UI 展现等。