访问者模式

Published on 2016 - 09 - 01

应用示例

我们在前面讲过了组合模式和迭代器模式。通过组合模式能够把一个公司的人员组织机构树搭建起来,给管理带来非常大的便利,通过迭代器模式把每一个员工都遍历一遍,看看是不是 “有人去世了还在领退休金”,“拿高工资而不干活的尸位素餐”等情况,我们今天要做的就是把这些情况统计成一个报表呈报上去,让领导看看这种恶劣的情况有多严重。

我们公司有700名多技术人员,分布在全国各地,组织架构在组合模式中已经介绍过了,是很常见的家长领导型模式,每个技术人员的岗位都是固定的,你在组织机构的哪棵树下,充当的角色是什么,叶子节点都是非常明确的,每一个员工的信息(如名字、性别、薪水等)都是记录在数据库中,现在有这样一个需求,我要把公司中的所有人员信息都打印汇报上去。我们来看类图,如图1所示。

这个类图还是比较简单的,我们定义每个员工都有薪水salary、名称name、性别sex这3个属性,然后提供了一个抽象方法getOtherInfo由子类进行扩展,同时通过report方法打印出每一个员工的信息,这里使用模板方法模式。我们先来看一下抽象类,如代码清单1所示。

public abstract class Employee {
     public final static int MALE = 0;  //0代表是男性
     public final static int FEMALE = 1; //1代表是女性
     //甭管是谁,都有工资
     private String name;
     //只要是员工那就有薪水
     private int salary;
     //性别很重要
     private int sex;
     //以下是简单的getter/setter
     public String getName() {
             return name;
     }
     public void setName(String name) {
             this.name = name;
     }
    public int getSalary() {
             return salary;
     }
     public void setSalary(int salary) {
             this.salary = salary;
     }
     public int getSex() {
             return sex;
     }
     public void setSex(int sex) {
             this.sex = sex;
     }
     //打印出员工的信息
     public final void  report(){
             String info = "姓名:" + this.name + "\t";
             info = info + "性别:" + (this.sex == FEMALE?"女":"男") + "\t";
             info = info + "薪水:" + this.salary  + "\t";     
             //获得员工的其他信息
             info = info + this.getOtherInfo();
             System.out.println(info);
     }
     //拼装员工的其他信息
     protected abstract String getOtherInfo();
}

先看小兵的实现类,越卑微的人物越能引起共鸣,因为我们有共同的经历、思维和苦难。请看实现类,如代码清单2所示。

public class CommonEmployee extends Employee {
     //工作内容,这非常重要,以后的职业规划就是靠它了
     private String job;
     public String getJob() {
             return job;
     }
     public void setJob(String job) {
             this.job = job;
     }
     protected String getOtherInfo(){
             return "工作:"+ this.job + "\t";
     }
}

每个实现类都必须实现getOtherInfo信息,通过它获得用户个性信息,我们再来看管理阶层,如代码清单3所示

public class Manager extends Employee {
     //这类人物的职责非常明确:业绩
     private String performance;
     public String getPerformance() {
             return performance;
     }
     public void setPerformance(String performance) {
             this.performance = performance;
     }   
     protected String getOtherInfo(){
             return "业绩:"+ this.performance + "\t";
     }
}

Performance这个单词在技术人员的眼里就代表性能,在实际商务英语中可以有Sales Performance(销售业绩)、performance evaluation(业绩评估)等。系统的框架都已经具备了,那我们来模拟一下这个过程,如代码清单4所示。

public class Client {
     public static void main(String[] args) {
             for(Employee emp:mockEmployee()){
                     emp.report();
             }
     }
     //模拟出公司的人员情况,我们可以想象这个数据是通过持久层传递过来的
     public static List<Employee> mockEmployee(){
             List<Employee> empList = new ArrayList<Employee>();
             //产生张三这个员工
             CommonEmployee zhangSan = new CommonEmployee();
             zhangSan.setJob("编写Java程序,绝对的蓝领、苦工加搬运工");
             zhangSan.setName("张三");
             zhangSan.setSalary(1800);
             zhangSan.setSex(Employee.MALE);
             empList.add(zhangSan);     
             //产生李四这个员工
             CommonEmployee liSi = new CommonEmployee();
             liSi.setJob("页面美工,审美素质太不流行了!");
             liSi.setName("李四");
             liSi.setSalary(1900);
             liSi.setSex(Employee.FEMALE);
             empList.add(liSi);
             //再产生一个经理
             Manager wangWu = new Manager();
             wangWu.setName("王五");
             wangWu.setPerformance("基本上是负值,但是我会拍马屁呀");
             wangWu.setSalary(18750);
             wangWu.setSex(Employee.MALE);
             empList.add(wangWu);
             return empList;
     }
}

先通过mockEmployee来模拟出一个数组,初始化两个员工和一个经理,当然在实际项目中这个数组应该由持久层产生。运行结果如下所示:

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

结果出来了,非常正确。我们来想一想实际的情况,人力资源部门拿这份表格会给谁看呢?那当然是大老板了!大老板关心的是什么?关心部门经理的业绩!小兵的情况不是他要了解的,就像战争时期一位将军说:“我一想到我的士兵也有孩子、妻子、父母,我就痛心疾首……但是这是战场,我只能认为他们是一群机器……”是啊,其实我们也一样啊,那问题就出来了:

  • 大老板就看部门经理的报表,小兵的报表可看可不看。
  • 多个大老板的“嗜好”是不同的,主管销售的,则主要关心营销的情况;主管会计的,则主要关心企业的整体财务运行状态;主管技术的,则主要看技术的研发情况。

综合成一句话,这个报表会修改:数据的修改以及报表的展现修改,按照开闭原则,项目分析的时候已经考虑到这些可能引起变更的因素,就需要在设计时考虑通过扩展来避开未来需求变更而引起的代码修改风险。我们来想一想,每个普通员工类和经理类都用一个方法report(从父类继承过来的),他无法为每一个子类定制特殊的属性,简化类图如图2所示。

我们思考一下,如何提供一个能够为每个子类定制报表的方法呢?可以这样思考,普通员工和管理层员工是两个不同的对象,例如,我邀请一个人过来参观我的家,参观者参观完毕后分别进行描述,那参观的对象不同,描述的结果也当然不同。好,按照这思路,我们把方法report提取到另外一个类Visitor中来实现,如图3所示。

两个子类的report方法都不需要了,只有Visitor类来实现了report的方法,这个猛一看还真有点委托(intergration)的意味,我们实现出来你就知道这和委托有非常大的差距。详细类图如图4所示。

在抽象类Employee中增加了accept方法,该方法是一个抽象方法,由子类实现,其意义就是说我这个类可以允许谁来访问,也就是定义一类访问者,在具体的实现类中调用访问者的方法。我们先看访问者接口IVisitor程序,如代码清单5所示。

public interface IVisitor {     
     //首先,定义我可以访问普通员工
     public void visit(CommonEmployee commonEmployee);
     //其次,定义我还可以访问部门经理
     public void visit(Manager manager);
}

该接口的意义是:该接口可以访问两个对象,一个是普通员工,一个是高层员工。我们来看其具体实现类,如代码清单6所示。

public class Visitor implements IVisitor {
     //访问普通员工,打印出报表
     public void visit(CommonEmployee commonEmployee) {
             System.out.println(this.getCommonEmployee(commonEmployee));
     }
     //访问部门经理,打印出报表
     public void visit(Manager manager) {
             System.out.println(this.getManagerInfo(manager));
     }
    //组装出基本信息
     private String getBasicInfo(Employee employee){
             String info = "姓名:" + employee.getName() + "\t";
             info = info + "性别:" + (employee.getSex() == Employee.FEMALE?"女":"男") + "\t";
             info = info + "薪水:" + employee.getSalary()  + "\t";
             return info;
     }
     //组装出部门经理的信息
     private String getManagerInfo(Manager manager){
             String basicInfo = this.getBasicInfo(manager);
             String otherInfo = "业绩:"+manager.getPerformance() + "\t";
             return basicInfo + otherInfo;
     }
     //组装出普通员工信息
     private String getCommonEmployee(CommonEmployee commonEmployee){
             String basicInfo = this.getBasicInfo(commonEmployee);
             String otherInfo = "工作:"+commonEmployee.getJob()+"\t";
             return basicInfo + otherInfo;
   }
}

在具体的实现类中,定义了两个私有方法,作用就是产生需要打印的数据和格式,然后在访问者访问相关的对象时产生这个报表。抽象员工Employee稍有修改,如代码清单7所示。

public abstract class Employee {
     public final static int MALE = 0;  //0代表是男性
     public final static int FEMALE = 1; //1代表是女性
     //甭管是谁,都有工资
     private String name;
     //只要是员工那就有薪水
     private int salary;
     //性别很重要
     private int sex;
     //以下是简单的getter/setter
     public String getName() {
             return name;
     }
     public void setName(String name) {
             this.name = name;
     }
     public int getSalary() {
             return salary;
     }
     public void setSalary(int salary) {
             this.salary = salary;
     }
     public int getSex() {
             return sex;
     }
     public void setSex(int sex) {
             this.sex = sex;
     }
     //我允许一个访问者访问
     public abstract void accept(IVisitor visitor);
}

抽象员工类有3个变动:

  • 删除了report方法。
  • 增加了accept方法,接受访问者的访问。
  • 删除了getOtherInfo方法。它的实现由访问者来处理,因为访问者对被访问的对象是“心知肚明”的,非常了解被访问者。

我们继续来看员工实现类,普通员工代码清单8所示。

public class CommonEmployee extends Employee {
     //工作内容,这非常重要,以后的职业规划就是靠它了
     private String job;
     public String getJob() {
             return job;
     }
     public void setJob(String job) {
             this.job = job;
     }
     //我允许访问者访问
     @Override
     public void accept(IVisitor visitor){
          visitor.visit(this);
     }
}

上面是普通员工的实现类,该类的accept方法很简单,这个类就把自身传递过去,也就是让访问者访问本身这个对象。再看Manager类,如代码清单9所示。

public class Manager extends Employee {
     //这类人物的职责非常明确:业绩
     private String performance;
     public String getPerformance() {
             return performance;
     }
     public void setPerformance(String performance) {
             this.performance = performance;
     }
     //部门经理允许访问者访问
     @Override
    public void accept(IVisitor visitor){
             visitor.visit(this);
     }
}

所有的业务定义都已经完成,我们来看看怎么模拟这个逻辑,如代码清单10所示。

public class Client {
     public static void main(String[] args) {
             for(Employee emp:mockEmployee()){
                     emp.accept(new Visitor());
             }
     }
     //模拟出公司的人员情况,我们可以想象这个数据是通过持久层传递过来的
     public static List<Employee> mockEmployee(){
             List<Employee> empList = new ArrayList<Employee>();
             //产生张三这个员工
             CommonEmployee zhangSan = new CommonEmployee();
             zhangSan.setJob("编写Java程序,绝对的蓝领、苦工加搬运工");
             zhangSan.setName("张三");
             zhangSan.setSalary(1800);
             zhangSan.setSex(Employee.MALE);
             empList.add(zhangSan);     
             //产生李四这个员工
             CommonEmployee liSi = new CommonEmployee();
             liSi.setJob("页面美工,审美素质太不流行了!");
             liSi.setName("李四");
             liSi.setSalary(1900);
             liSi.setSex(Employee.FEMALE);
             empList.add(liSi);
             //再产生一个经理
             Manager wangWu = new Manager();
             wangWu.setName("王五");
             wangWu.setPerformance("基本上是负值,但是我会拍马屁呀");
             wangWu.setSalary(18750);
             wangWu.setSex(Employee.MALE);
             empList.add(wangWu);     
             return empList;
     }
}

改动非常少,就黑体那么一行的改动,运行结果如下:

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

运行结果也完全相同,那回过头来看看这个程序是怎么实现的:

  • 第一,通过循环遍历所有元素。
  • 第二,每个员工对象都定义了一个访问者。
  • 第三,员工对象把自己作为一个参数调用访问者visit方法。
  • 第四,访问者调用自己内部的计算逻辑,计算出相应的数据和表格元素。
  • 第五,访问者打印出报表和数据。

事情的经过就是这个样子。那我们再来看看上面提到的数据和报表格式都会改变的情况。首先是数据的改变,数据改了当然都要改,说不上两个方案有什么优劣;其次是报表格式的修改,这个方案绝对是有优势的,我只要再产生一个IVisitor的实现类就可以产生一个新的报表格式,而其他的类都不用修改,如果你用Spring开发,那就更好了,在Spring的配置文件中使用的是接口注入,我只要把配置文件中的 ref修改一下就行了,其他的都不用修改了!这就是访问者模式的优势所在。

访问者模式的定义

访问者模式(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. (封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。)

访问者模式的通用类图如图5所示。

看了这个通用类图,大家可能要犯迷糊了,这里怎么有一个ObjectStruture类呢?你刚刚举的例子怎么就没有呢?真没有吗?我们不是定义了一个List了吗?它中间的元素是我们一个一个手动增加上去的,这就是一个ObjectStruture,我们来看这几个角色的职责。

  • Visitor——抽象访问者

抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是visit方法的参数定义哪些对象是可以被访问的。

  • ConcreteVisitor——具体访问者

它影响访问者访问到一个类后该怎么干,要做什么事情。

  • Element——抽象元素

接口或者抽象类,声明接受哪一类访问者访问,程序上是通过accept方法中的参数来定义的。

  • ConcreteElement——具体元素

实现accept方法,通常是visitor.visit(this),基本上都形成了一种模式了。

  • ObjectStruture——结构对象

元素产生者,一般容纳在多个不同类、不同接口的容器,如List、Set、Map等,在项目中,一般很少抽象出这个角色。

大家可以这样理解访问者模式,我作为一个访客(Visitor)到朋友家(Visited Class)去拜访,朋友之间聊聊天,喝喝酒,再相互吹捧吹捧,炫耀炫耀,这都正常。聊天的时候,朋友告诉我,他今年加官晋爵了,工资也涨了30%,准备再买套房子,那我就在心里盘算(Visitor-self-method)“你这么有钱,我去年要借10万你都不借”,我根据朋友的信息,执行了自己的一个方法。

我们来看看访问者模式的通用源码,先看抽象元素,如代码清单11所示。

public abstract class Element {
     //定义业务逻辑
     public abstract void doSomething();
     //允许谁来访问
    public abstract void accept(IVisitor visitor);
}

抽象元素有两类方法:一是本身的业务逻辑,也就是元素作为一个业务处理单元必须完成的职责;另外一个是允许哪一个访问者来访问。我们来看具体元素,如代码清单12所示。

public class ConcreteElement1 extends Element{
     //完善业务逻辑
     public void doSomething(){
             //业务处理
     }
     //允许那个访问者访问
     public void accept(IVisitor visitor){
             visitor.visit(this);
     }
}
public class ConcreteElement2 extends Element{
     //完善业务逻辑
     public void doSomething(){
             //业务处理
     }
     //允许那个访问者访问
     public void accept(IVisitor visitor){
             visitor.visit(this);
     }
}

它定义了两个具体元素,我们再来看抽象访问者,一般是有几个具体元素就有几个访问方法,如代码清单13所示。

public interface IVisitor {
     //可以访问哪些对象
     public void visit(ConcreteElement1 el1);
     public void visit(ConcreteElement2 el2);
}

具体访问者如代码清单14所示。

public class Visitor implements IVisitor {
     //访问el1元素
     public void visit(ConcreteElement1 el1) {
             el1.doSomething();
     }
     //访问el2元素
     public void visit(ConcreteElement2 el2) {
             el2.doSomething();
     }
}

结构对象是产生出不同的元素对象,我们使用工厂方法模式来模拟,如代码清单15所示。

public class ObjectStruture {
     //对象生成器,这里通过一个工厂方法模式模拟
     public static Element createElement(){
             Random rand = new Random();
             if(rand.nextInt(100) > 50){
                     return new ConcreteElement1();
             }else{
                     return new ConcreteElement2();
             }
     }
}

进入了访问者角色后,我们对所有的具体元素的访问就非常简单了,我们通过一个场景类模拟这种情况,如代码清单16所示。

public class Client {
     public static void main(String[] args) {
             for(int i=0;i<10;i++){
                     //获得元素对象
                     Element el = ObjectStruture.createElement();
                     //接受访问者访问
                     el.accept(new Visitor());
             }          
     }
}

通过增加访问者,只要是具体元素就非常容易访问,对元素的遍历就更加容易了,甭管它是什么对象,只要它在一个容器中,都可以通过访问者来访问,任务集中化。这就是访问者模式。

访问者模式的应用

访问者模式的优点

  • 符合单一职责原则

具体元素角色也就是Employee抽象类的两个子类负责数据的加载,而Visitor类则负责报表的展现,两个不同的职责非常明确地分离开来,各自演绎变化。

  • 优秀的扩展性

由于职责分开,继续增加对数据的操作是非常快捷的,例如,现在要增加一份给大老板的报表,这份报表格式又有所不同,直接在Visitor中增加一个方法,传递数据后进行整理打印。

  • 灵活性非常高

例如,数据汇总,就以刚刚我们说的Employee的例子,如果我现在要统计所有员工的工资之和,怎么计算?把所有人的工资for循环加一遍?是个办法,那我再提个问题,员工工资×1.2,部门经理×1.4,总经理×1.8,然后把这些工资加起来,你怎么处理?1.2,1.4,1.8是什么?不是吧?!你没看到领导不论什么时候都比你拿得多,工资奖金就不说了,就是过节发个慰问券也比你多,就是这个系数在作祟。我们继续说你想怎么统计?使用for循环,然后使用instanceof来判断是员工还是经理?这可以解决,但不是个好办法,好办法是通过访问者模式来实现,把数据扔给访问者,由访问者来进行统计计算。

访问者模式的缺点

  • 具体元素对访问者公布细节

访问者要访问一个类就必然要求这个类公布一些方法和数据,也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的。

  • 具体元素变更比较困难

具体元素角色的增加、删除、修改都是比较困难的,就上面那个例子,你想想,你要是想增加一个成员变量,如年龄age,Visitor就需要修改,如果Visitor是一个还好办,多个呢?业务逻辑再复杂点呢?

  • 违背了依赖倒置转原则

访问者依赖的是具体元素,而不是抽象元素,这破坏了依赖倒置原则,特别是在面向对象的编程中,抛弃了对接口的依赖,而直接依赖实现类,扩展比较难。

访问者模式的使用场景

  • 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作,也就说是用迭代器模式已经不能胜任的情景。
  • 需要对一个对象结构中的对象进行很多不同并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。

总结一下,在这种地方你一定要考虑使用访问者模式:业务规则要求遍历多个不同的对象。这本身也是访问者模式出发点,迭代器模式只能访问同类或同接口的数据(当然了,如果你使用instanceof,那么能访问所有的数据,这没有争论),而访问者模式是对迭代器模式的扩充,可以遍历不同的对象,然后执行不同的操作,也就是针对访问的对象不同,执行不同的操作。访问者模式还有一个用途,就是充当拦截器(Interceptor)角色。

访问者模式的扩展

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

统计功能

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

没什么变化?仔细看IVisitor接口,增加了一个getTotalSalary方法,在Visitor实现类中实现该方法。我们先看接口,如代码清单17所示。

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

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

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;
     }
}

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

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

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静态方法没有任何改动,请参考代码清单10,在此不再赘述。运行结果如下所示:

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

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

多个访问者

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

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

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

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

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;
     }
}

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

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

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

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 ;
     }
}

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

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();
     }
}

运行结果如下所示:

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

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

双分派

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

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

public interface Role {
     //演员要扮演的角色
}
public class KungFuRole implements Role {
     //武功天下第一的角色
}
public class IdiotRole implements Role {
     //一个弱智角色     
}

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

public abstract class AbsActor {
     //演员都能够演一个角色
     public void act(Role role){
             System.out.println("演员可以扮演任何角色");
     }     
     //可以演功夫戏
     public void act(KungFuRole role){
             System.out.println("演员都可以演功夫角色");
     }
}

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

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("年龄大了,不能演功夫角色");
     }
}

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

public class Client {
     public static void main(String[] args) {
             //定义一个演员
             AbsActor actor = new OldActor();
             //定义一个角色
             Role role = new KungFuRole();
             //开始演戏
             actor.act(role);
             actor.act(new KungFuRole());
     }
}

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

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

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

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

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);
     }
}

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

public class Client {
     public static void main(String[] args) {
             //定义一个演员
             AbsActor actor = new OldActor();
             //定义一个角色   
             Role role = new KungFuRole();
             //开始演戏
             role.accept(actor);
     }
}

运行结果如下所示。

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

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

参考文档