访问者模式
# 一、概述
访问者(Visitor)模式表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素类的前提下定义作用于这个元素的新操作。
使用访问者模式的前提是对象结构中的数据元素是已知、有限且稳定的(即不经常变化)。
# 1.1 解决了什么问题
有这样的场景,小米公司主要销售手机、电视和和电脑三类产品,数据团队每天需要向公司高层以及业务方发送商业报表,报表的内容包括销量、GMV、毛利、日活、新增用户量众多指标。需要注意的是,公司的人员角色不同,所以可以看到的指标内容也应当是不同的,也就是说要根据不同的角色发送不同访问视角的报表。
在公司早期发展阶段,公司的人员以及角色构成比较简单,所以采用普通的 if...else... 进行角色判断,然后不同的角色发送不同的指标,这是没问题的。
但是随着公司的发展,团队越来越壮大,人员构成也越来越复杂,权限管控越来越严格,一旦将销售额以及毛利这种关键指标发送给不相干的人,很可能造成商业机密泄露。如果继续按照 if...else...的方式进行发送报表,存在以下问题:
- 频繁修改已经上线且稳定运行的代码很容易出问题,存在风险;
- 大量的 if...else...会使得代码非常难以维护。
# 1.2 解决方案
访问者模式建议将对数据的操作放入一个名为访问者的独立类中,而不是将其和数据放在同一个类中,被访问的类里面加一个对外提供接待访问者的接口。换句话说就是将数据结构与数据的操作分离,解决数据结构和操作耦合的问题。
# 1.3 适用场景
- 对象结构比较稳定,但经常需要在此对象结构上定义新的操作。
- 需要对一个对象结构中的各个元素进行很多不同操作,且这些操作之间没有关联,同时要避免让这些操作更改这些元素的类。
# 二、实现方式
# 2.1 角色
- ObjectStructure:定义当中所提到的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素提供访问者访问。
- Visitor:接口或抽象类,定义了对每个 Element 访问的方法 visite(Element element)。它的参数是被访问的 Element,所以说有几种 Element 就定义几个 visite 方法,这也是要求 ObjectStructure 中 Element 不经常变化的原因,否则需要频繁修改 Visitor 中的 visite 方法。
- ConcreteVisitor:具体的访问者,实现 Visitor,需要定义元素类访问时产生的具体行为。
- Element:接口或抽象类,定义了一个 accept(Visitor visitor)方法,用以接受访问者的访问。
- ConcreteElement:具体的元素类,提供接受 Visitor 访问时的具体实现。
# 2.2 代码
首先定义抽象指标类,对应角色中的 Element:
/**
* 抽象指标类
*/
public abstract class ProductMetrics {
// 产品名称
private String name;
// 商品交易总额
private float GMV;
// 销量
private int sales;
// 日活
private int DAU;
// 平均在线时长
private float averageOnlineTime;
public ProductMetrics(String name) {
Random random = new Random();
this.name = name;
this.GMV = random.nextFloat() * 10000 + 10000;
this.sales = random.nextInt(500) + 500;
this.DAU = random.nextInt(10000) + 10000;
this.averageOnlineTime = random.nextFloat() * 100 + 100;
}
public String getName() {
return name;
}
public float getGMV() {
return GMV;
}
public int getSales() {
return sales;
}
public int getDAU() {
return DAU;
}
public float getAverageOnlineTime() {
return averageOnlineTime;
}
/**
* 核心方法,接受访问者的访问
* Ï
*
* @param visitor
*/
public abstract void accept(Visitor visitor);
}
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
抽象指标类虽然代码多,但是最核心的是accept(Visitor visitor)
方法,其为访问者提供一个接受访问的方法。
紧接着定义每一款产品的具体指标类,对应 ConcreteElement 角色:
/**
* 手机指标
*/
public class PhoneMetrics extends ProductMetrics {
public PhoneMetrics(String name) {
super(name);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 电视指标
*/
public class TelevisionMetrics extends ProductMetrics {
public TelevisionMetrics(String name) {
super(name);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 电脑指标
*/
public class ComputerMetrics extends ProductMetrics {
public ComputerMetrics(String name) {
super(name);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
在每个具体指标类,即具体的元素类中实现了accept(Visitor visitor)
方法,调用了访问者的访问方法。
紧接着定义抽象的访问者,对应 Visitor 角色:
public interface Visitor {
/**
* 访问手机类
*
* @param phone
*/
void visit(PhoneMetrics phone);
/**
* 访问电视类
*
* @param television
*/
void visit(TelevisionMetrics television);
/**
* 访问电脑类
*
* @param computer
*/
void visit(ComputerMetrics computer);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
一般来说,有几个 Element 的具体实现类就需要定义几个 visit 方法,因为要针对每一种 Element 提供不同的访问方式。
定义不同的具体访问者对象,对应 ConcreteVisitor 角色:
/**
* 老板视角可以访问所有指标
*/
public class BossVisitor implements Visitor {
@Override
public void visit(PhoneMetrics phone) {
System.out.println("PhoneMetrics{" + "name='" + phone.getName() + '\'' + ", GMV=" + phone.getGMV() + ", sales=" + phone.getSales() + ", DAU=" + phone.getDAU() + ", averageOnlineTime=" + phone.getAverageOnlineTime() + '}');
}
@Override
public void visit(TelevisionMetrics television) {
System.out.println("TelevisionMetrics{" + "name='" + television.getName() + '\'' + ", GMV=" + television.getGMV() + ", sales=" + television.getSales() + ", DAU=" + television.getDAU() + ", averageOnlineTime=" + television.getAverageOnlineTime() + '}');
}
@Override
public void visit(ComputerMetrics computer) {
System.out.println("ComputerMetrics{" + "name='" + computer.getName() + '\'' + ", GMV=" + computer.getGMV() + ", sales=" + computer.getSales() + ", DAU=" + computer.getDAU() + ", averageOnlineTime=" + computer.getAverageOnlineTime() + '}');
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 内容运营团队视角,只能访问日活和活跃时长两个指标
*/
public class LauncherTeamVisitor implements Visitor {
@Override
public void visit(PhoneMetrics phone) {
System.out.println("PhoneMetrics{" + "name='" + phone.getName() + '\'' + ", DAU=" + phone.getDAU() + ", averageOnlineTime=" + phone.getAverageOnlineTime() + '}');
}
@Override
public void visit(TelevisionMetrics television) {
System.out.println("TelevisionMetrics{" + "name='" + television.getName() + '\'' + ", DAU=" + television.getDAU() + ", averageOnlineTime=" + television.getAverageOnlineTime() + '}');
}
@Override
public void visit(ComputerMetrics computer) {
System.out.println("ComputerMetrics{" + "name='" + computer.getName() + '\'' + ", DAU=" + computer.getDAU() + ", averageOnlineTime=" + computer.getAverageOnlineTime() + '}');
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
定义 ObjectStructure 角色:
/**
* 报表类
*/
public class BusinessReport {
private List<ProductMetrics> metrics = new LinkedList<>();
public BusinessReport() {
metrics.add(new PhoneMetrics("手机指标"));
metrics.add(new TelevisionMetrics("电视指标"));
metrics.add(new ComputerMetrics("电脑指标"));
}
public void show(Visitor visitor) {
for (ProductMetrics metric : metrics) {
metric.accept(visitor);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
使用示例:
public class VisitorTest {
public static void main(String[] args) {
BusinessReport report = new BusinessReport();
System.out.println("---------- Boss视角报表 ----------");
report.show(new BossVisitor());
System.out.println("---------- 运营团队视角报表 ----------");
report.show(new LauncherTeamVisitor());
}
}
2
3
4
5
6
7
8
9
10
11
---------- Boss视角报表 ----------
PhoneMetrics{name='手机指标', GMV=13125.203, sales=805, DAU=15607, averageOnlineTime=185.97504}
TelevisionMetrics{name='电视指标', GMV=17075.887, sales=525, DAU=18214, averageOnlineTime=130.15582}
ComputerMetrics{name='电脑指标', GMV=11547.258, sales=900, DAU=11478, averageOnlineTime=184.32672}
---------- 运营团队视角报表 ----------
PhoneMetrics{name='手机指标', DAU=15607, averageOnlineTime=185.97504}
TelevisionMetrics{name='电视指标', DAU=18214, averageOnlineTime=130.15582}
ComputerMetrics{name='电脑指标', DAU=11478, averageOnlineTime=184.32672}
2
3
4
5
6
7
8
可以看到由于访问者角色的不同,所以提供了不同视角的报表。后续无论增加多少个访问者角色,都只需增加新的 Visitor 实现类即可,不需要更改已有代码。但前提是产品类型稳定(即元素种类稳定),否则就需要修改 Visitor 接口。
访问者算是设计模式中最难理解的一个,其有一个很重要的特性是使用了双分派机制。
# 三、源码中的应用
- javax.lang.model.element.AnnotationValue 和 AnnotationValueVisitor
- javax.lang.model.element.Element 和 ElementVisitor
- javax.lang.model.type.TypeMirror 和 TypeVisitor
- java.nio.file.FileVisitor 和 SimpleFileVisitor
- javax.faces.component.visit.VisitContext 和 VisitCallback