25.4 访问者模式的扩展

25.4 访问者模式的扩展

访问者模式是经常用到的模式,虽然你不注意,有可能你起的名字也不是什么Visitor, 但是它确实是非常容易使用到的,在这里我提出两个扩展的功能供大家参考。

25.4.1 统计功能

在例子中我们也提到访问者的统计功能,汇总和报表是金融类企业非常常用的功能,基本上都是一堆的计算公式,然后出一个报表,很多项目采用了数据库的存储过程来实现,我不是很推荐这种方式,除非海量数据处理,一个晚上要批处理上亿、几十亿条的数据,除了存储过程来处理还没有其他办法,你要是用应用服务器来处理,连接数据库的网络就是处于100%占用状态,一个晚上也未必能处理完这批数据!除了这种海量数据外,我建议数据统计和报表的批处理通过访问者模式来处理会比较简单。好,那我们来统计一下公司人员的工资总额,先看类图,如图25-6所示。

image-20210930103615070

图25-6 统计功能的访问者模式
没什么变化?仔细看IVisitor接口,增加了一个getTotalSalary方法,在Visitor实现类中实现该方法。我们先看接口,如代码清单25-17所示。

代码清单25-17 抽象访问者

1
2
3
4
5
6
7
8
public interface IVisitor {
//首先定义我可以访问普通员工
public void visit(CommonEmployee commonEmployee);
//其次定义,我还可以访问部门经理
public void visit(Manager manager);
//统计所有员工工资总和
public int getTotalSalary();
}

这就多了一个getTotalSalary方法。我们再来看实现类,如代码清单25-18所示。

代码清单25-18 具体访问者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Visitor implements IVisitor {
//部门经理的工资系数是5
private final static int MANAGER_COEFFICIENT = 5;
//员工的工资系数是2
private final static int COMMONEMPLOYEE_COEFFICIENT = 2;
//普通员工的工资总和
private int commonTotalSalary = 0;
//部门经理的工资总和
private int managerTotalSalary =0;
//计算部门经理的工资总和
private void calManagerSalary(int salary){
this.managerTotalSalary = this.managerTotalSalary + salary *MANAGER_COEFFICIENT ;
}
//计算普通员工的工资总和
private void calCommonSlary(int salary){
this.commonTotalSalary = this.commonTotalSalary + salary*COMMONEMPLOYEE_COEFFICIENT;
}
//获得所有员工的工资总和
public int getTotalSalary(){
return this.commonTotalSalary + this.managerTotalSalary;
}
}

员工和经理层的信息就不再展示了,请参考代码清单25-6。程序还是比较简单的,分别计算普通员工和经理级员工的工资总和,然后加起来。注意,我们在实现时已经考虑员工工资和经理工资的系数不同。

我们再来看Client类的模拟,如代码清单25-19所示。

代码清单25-19 场景类

1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
IVisitor visitor = new Visitor();
for(Employee emp:mockEmployee()){
emp.accept(visitor);
}
System.out.println("本公司的月工资总额是:"+visitor.getTotalSalary());
}

}

其中mockEmployee静态方法没有任何改动,请参考代码清单25-10,在此不再赘述。运行结果如下所示:

1
2
3
4
姓名:张 三 性别:男 薪水:1800 工作:编写Java程序,绝对的蓝领、苦工加搬运工 
姓名:李 四 性别:女 薪水:1900 工作:页面美工,审美素质太不流行了!
姓名:王 五 性别:男 薪水:18750 业绩:基本上是负值,但是我会拍马屁呀
本公司的月工资总额是:101150

然后你想修改工资的系数,没有问题!想换个展示格式,也没有问题!多多练习吧,这都是非常简单的。

25.4.2 多个访问者

在实际的项目中,一个对象,多个访问者的情况非常多。其实我们上面例子就应该是两个访问者,为什么呢?报表分两种:第一种是展示表,通过数据库查询,把结果展示出来, 这个就类似于我们的那个列表;第二种是汇总表,这个是需要通过模型或者公式计算出来的,一般都是批处理结果,这个类似于我们计算工资总额,这两种报表格式是对同一堆数据的两种处理方式。从程序上看,一个类就有个不同的访问者了。修改一下类图,如图25-7所示。

类图看着挺复杂,其实也没什么复杂的,只是多了两个接口和两个实现类,分别负责展示表和汇总表的业务处理,IVisitor接口没有改变,请参考代码清单25-5所示代码,这里不再赘述。我们来看展示报表接口,如代码清单25-20所示。

代码清单25-20 展示表接口

1
2
3
4
public interface IShowVisitor extends IVisitor {
//展示报表
public void report();
}

展示表的实现也比较简单,如代码清单25-21所示。

代码清单25-21 具体展示表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ShowVisitor implements IShowVisitor {
private String info = "";
//打印出报表
public void report() {
System.out.println(this.info);
}
//访问普通员工,组装信息
public void visit(CommonEmployee commonEmployee) {
this.info = this.info + this.getBasicInfo(commonEmployee) + "工作:"+commonEmployee.getJob()+"\t\n";
}
//访问经理,然后组装信息
public void visit(Manager manager) {
this.info = this.info + this.getBasicInfo(manager) + "业绩: "+manager.getPerformance() + "\t\n";
}
//组装出基本信息
private String getBasicInfo(Employee employee){
String info = "姓名:" + employee.getName() + "\t";
info = info + "性别:" + (employee.getSex() == Employee.FEMALE?"女": "男") + "\t";
info = info + "薪水:" + employee.getSalary() + "\t";
return info;
}
}

image-20210930104007400

图25-7 多访问者的类图

汇总表实现数据汇总功能,其接口如代码清单25-22所示。

代码清单25-22 汇总表接口

1
2
3
4
public interface ITotalVisitor extends IVisitor {
//统计所有员工工资总和
public void totalSalary();
}

就一句话,非常简单,我们再来看具体的汇总表访问者,如代码清单25-23所示。

代码清单25-23 具体汇总表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TotalVisitor implements ITotalVisitor {
//部门经理的工资系数是5
private final static int MANAGER_COEFFICIENT = 5;
//员工的工资系数是2
private final static int COMMONEMPLOYEE_COEFFICIENT = 2;
//普通员工的工资总和
private int commonTotalSalary = 0;
//部门经理的工资总和
private int managerTotalSalary =0;
public void totalSalary() {
System.out.println("本公司的月工资总额是" + (this.commonTotalSalary + this.managerTotalSalary));
}
//访问普通员工,计算工资总额
public void visit(CommonEmployee commonEmployee) {
this.commonTotalSalary = this.commonTotalSalary + commonEmployee.getSalary() *COMMONEMPLOYEE_COEFFICIENT;
}
//访问部门经理,计算工资总额
public void visit(Manager manager) {
this.managerTotalSalary = this.managerTotalSalary + manager.getSalary() *MANAGER_COEFFICIENT ;
}
}

最后看我们的场景类如何计算出工资总额,如代码清单25-24所示。

代码清单25-24 场景类

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) {
//展示报表访问者
IShowVisitor showVisitor = new ShowVisitor();
//汇总报表的访问者
ITotalVisitor totalVisitor = new TotalVisitor();
for(Employee emp:mockEmployee()){
emp.accept(showVisitor);
//接受展示报表访问者
emp.accept(totalVisitor);
//接受汇总表访问者
}
//展示报表
showVisitor.report();
//汇总报表
totalVisitor.totalSalary();
}
}

运行结果如下所示:

1
2
3
4
姓名:张 三 性别:男 薪水:1800 工作:编写Java程序,绝对的蓝领、苦工加搬运工
姓名:李 四 性别:女 薪水:1900 工作:页面美工,审美素质太不流行了!
姓名:王 五 性别:男 薪水:18750 业绩:基本上是负值,但是我会拍马屁啊
本公司的月工资总额是101150

大家可以再深入地想象,一堆数据从几个角度来分析,那是什么?即数据挖掘(Data Mining),数据的上切、下钻等处理,大家有兴趣看可以翻看数据挖掘或者商业智能(BI) 的书。

25.4.3 双分派

说到访问者模式就不得不提一下双分派(double dispatch)问题,什么是双分派呢?我们先来解释一下什么是单分派(single dispatch)和多分派(multiple dispatch),单分派语言处理一个操作是根据请求者的名称和接收到的参数决定的,在Java中有静态绑定和动态绑定之说,它的实现是依据重载(overload)和覆写(override)实现的,我们来说一个简单的例子。

例如,演员演电影角色,一个演员可以扮演多个角色,我们先定义一个影视中的两个角色:功夫主角和白痴配角,如代码清单25-25所示。

代码清单25-25 角色接口及实现类

1
2
3
4
5
6
7
8
9
public interface Role {
//演员要扮演的角色
}
public class KungFuRole implements Role {
//武功天下第一的角色
}
public class IdiotRole implements Role {
//一个弱智角色
}

角色有了,我们再定义一个演员抽象类,如代码清单25-26所示。

代码清单25-26 抽象演员

1
2
3
4
5
6
7
8
9
10
public abstract class AbsActor {
//演员都能够演一个角色
public void act(Role role){
System.out.println("演员可以扮演任何角色");
}
//可以演功夫戏
public void act(KungFuRole role){
System.out.println("演员都可以演功夫角色");
}
}

很简单,这里使用了Java的重载,我们再来看青年演员和老年演员,采用覆写的方式来细化抽象类的功能,如代码清单25-27所示。

代码清单25-27 青年演员和老年演员

1
2
3
4
5
6
7
8
9
10
11
12
public class YoungActor extends AbsActor {
//年轻演员最喜欢演功夫戏
public void act(KungFuRole role){
System.out.println("最喜欢演功夫角色");
}
}
public class OldActor extends AbsActor {
//不演功夫角色
public void act(KungFuRole role){
System.out.println("年龄大了,不能演功夫角色");
}
}

覆写和重载都已经实现,我们编写一个场景,如代码清单25-28所示。

代码清单25-28 场景类

1
2
3
4
5
6
7
8
9
10
11
public class Client {
public static void main(String[] args) {
//定义一个演员
AbsActor actor = new OldActor();
//定义一个角色
Role role = new KungFuRole();
//开始演戏
actor.act(role);
actor.act(new KungFuRole());
}
}

猜猜看运行结果是什么?很简单,运行结果如下所示。

1
2
演员可以扮演任何角色
年龄大了,不能演功夫角色

重载在编译器期就决定了要调用哪个方法,它是根据role的表面类型而决定调用act(Role role)方法,这是静态绑定;而Actor的执行方法act则是由其实际类型决定的,这是动态绑定。

一个演员可以扮演很多角色,我们的系统要适应这种变化,也就是根据演员、角色两个对象类型,完成不同的操作任务,该如何实现呢?很简单,我们让访问者模式上场就可以解决该问题,只要把角色类稍稍修改即可,如代码清单25-29所示。

代码清单25-29 引入访问者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface Role {
//演员要扮演的角色
public void accept(AbsActor actor);
}
public class KungFuRole implements Role {
//武功天下第一的角色
public void accept(AbsActor actor){
actor.act(this);
}
}
public class IdiotRole implements Role {
//一个弱智角色,由谁来扮演
public void accept(AbsActor actor){
actor.act(this);
}
}

场景类稍有改动,如代码清单25-30所示。

代码清单25-30 场景类

1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
//定义一个演员
AbsActor actor = new OldActor();
//定义一个角色
Role role = new KungFuRole();
//开始演戏
role.accept(actor);
}
}

运行结果如下所示。

1
年龄大了,不能演功夫角色

看到没?不管演员类和角色类怎么变化,我们都能够找到期望的方法运行,这就是双反派。双分派意味着得到执行的操作决定于请求的种类和两个接收者的类型,它是多分派的一个特例。从这里也可以看到Java是一个支持双分派的单分派语言。