首页 » 设计模式之禅(第2版) » 设计模式之禅(第2版)全文在线阅读

《设计模式之禅(第2版)》第25章 访问者模式

关灯直达底部

25.1 员工的隐私何在

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

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

图25-1 员工信息类图

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

代码清单25-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;}  

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

代码清单25-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信息,通过它获得用户个性信息,我们再来看管理阶层,如代码清单25-3所示。

代码清单25-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(业绩评估)等。系统的框架都已经具备了,那我们来模拟一下这个过程,如代码清单25-4所示。

代码清单25-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(从父类继承过来的),他无法为每一个子类定制特殊的属性,简化类图如图25-2所示。

图25-2 简化类图

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

图25-3 改造后的简化类图

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

图25-4 改造后的详细类图

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

代码清单25-5 访问者接口

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

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

代码清单25-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稍有修改,如代码清单25-7所示。

代码清单25-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方法。它的实现由访问者来处理,因为访问者对被访问的对象是“心知肚明”的,非常了解被访问者。

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

代码清单25-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类,如代码清单25-9所示。

代码清单25-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);     }}  

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

代码清单25-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修改一下就行了,其他的都不用修改了!这就是访问者模式的优势所在。