访问者模式
一、引子
对于系统中一个已经完成的类层次结构,我们已经给它提供了满足需求的接口。但是面对新增加的需求,我们应该怎么做呢?如果这是为数不多的几次变动,而且你不用为了一个需求的调整而将整个类层次结构统统地修改一遍,那么直接在原有类层次结构上修改也许是个不错 的主意。
但是往往我们遇到的却是:这样的需求变动也许会不停的发生;更重要的是需求的任何变动可能都要让你将整个类层次结构修改个底朝天……。这种类似的操作分布在不同的类里面,不是一个好现象,我们要对这个结构重构一下了。
那么,访问者模式也许是你很好的选择。
二、定义
访问者模式,顾名思义使用了这个模式后就可以在不修改已有程序结构的前提下,通过添加额外的“访问者”来完成对已有代码功能的提升。
《设计模式》一书对于访问者模式给出的定义为:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。从定义可以看出结构对象是使用访问者模式必须条件,而且这个结构对象必须存在遍历自身各个对象的方法。这便类似于java中的collection概念了。
三、结构
以下是访问者模式的组成结构:
1) 访问者角色(Visitor):为该对象结构中具体元素角色声明一个访问操作接口。该操作接口的名字和参数标识了发送访问请求给具体访问者的具体元素角色。这样访问者就可以通过该元素角色的特定接口直接访问它。
2) 具体访问者角色(Concrete Visitor):实现每个由访问者角色(Visitor)声明的操作。
3) 元素角色(Element):定义一个Accept操作,它以一个访问者为参数。
4) 具体元素角色(Concrete Element):实现由元素角色提供的Accept操作。
5) 对象结构角色(Object Structure):这是使用访问者模式必备的角色。它要具备以下特征:能枚举它的元素;可以提供一个高层的接口以允许该访问者访问它的元素;可以是一个复合(组合模式)或是一个集合,如一个列表或一个无序集合。
四、例子
首先实现一个人员信息展示,然后增加一个访问者,实现工作工资统计功能。
(本例子中为了和上面的设计模式结构图一致,代码实现中去掉了下面类图中的IVisitor的两个子接口,具体访问者直接继承IVisitor因此场景类稍微改了一下,直接使用具体访问者,和设计模式之禅原例子稍微不同)
Employee基类
/** *在一个单位里谁都是员工,甭管你是部门经理还是小兵 */ 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); }
普通员工类(被访问的具体类)
/** * 普通员工,也就是最小的小兵 */ 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); } }
经理类(被访问的具体类)
/** * 经理级人物 */ 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); } }
IVisitor接口
public interface IVisitor { // 首先定义我可以访问普通员工 public void visit(CommonEmployee commonEmployee); // 其次定义,我还可以访问部门经理 public void visit(Manager manager); }
ShowVisitor
public class ShowVisitor implements IVvisitor{ 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; } }
场景类
import java.util.ArrayList; import java.util.List; public class Client { public static void main(String[] args) { // 展示报表访问者 ShowVisitor showVisitor = new ShowVisitor(); for (Employee emp : mockEmployee()) { emp.accept(showVisitor); // 接受展示报表访问者 } // 展示报表 showVisitor.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; } }
运行结果
姓名:张三 性别:男 薪水:1800 工作:编写Java程序,绝对的蓝领、苦工加搬运工 姓名:李四 性别:女 薪水:1900 工作:页面美工,审美素质太不流行了! 姓名:王五 性别:男 薪水:18750 业绩:基本上是负值,但是我会拍马屁呀 |
多个访问者(增加一个访问者,实现工资统计)
/** * 汇总表,该访问者起汇总作用,把容器中的数据一个一个遍历,然后汇总 */ public class TotalVisitor 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; 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; } }
Client 场景类
import java.util.ArrayList; import java.util.List; public class Client { public static void main(String[] args) { // 汇总报表的访问者 TotalVisitor totalVisitor = new TotalVisitor(); for (Employee emp : mockEmployee()) { emp.accept(totalVisitor);// 接受汇总表访问者 } // 汇总报表 totalVisitor.totalSalary(); } // 模拟出公司的人员情况,我们可以想象这个数据室通过持久层传递过来的 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; } }
运行结果
本公司的月工资总额是101150 |
五、访问者模式与双分派
double dispatch(双分派)则在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时型别(Run time type),还要根据参数的运行时型别(Run time type)。
double dispatch(双分派)是multi-dispatch(多分派)的特例,由于Visitor模式涉及的是double dispatch(双分派)。首先在客户程序中将具体访问者模式作为参数传递给具体元素角色,这便完成了一次分派。进入具体元素角色后,具体元素角 色调用作为参数的具体访问者模式中的visitor方法,同时将自己(this)作为参数传递进去。具体访问者模式再根据参数的不同来选择方法来执行。这便完成了第二次分派。
http://blog.csdn.net/ncepustrong/article/details/9070177
http://blog.chinaunix.net/uid-20665047-id-3269091.html
http://www.iteye.com/topic/1130764
五、优缺点
5.1、访问者模式的优点
·符合单一职责原则:凡是适用访问者模式的场景中,元素类中需要封装在访问者中的操作必定是与元素类本身关系不大且是易变的操作,使用访问者模式一方面符合单一职责原则,另一方面,因为被封装的操作通常来说都是易变的,所以当发生变化时,就可以在不改变元素类本身的前提下,实现对变化部分的扩展。
·扩展性良好:元素类可以通过接受不同的访问者来实现对不同操作的扩展。
5.2、缺点
访问者模式并不是那么完美,它也有着致命的缺陷:增加新的元素类比较困难。通过访问者模式的代码可以看到,在访问者类中,每一个元素类都有它对应的处理方法,也就是说,每增加一个元素类都需要修改访问者类(也包括访问者类的子类或者实现类),修改起来相当麻烦。也就是说,在元素类数目不确定的情况下,应该慎用访问者模式。所以,访问者模式比较适用于对已有功能的重构,比如说,一个项目的基本功能已经确定下来,元素类的数据已经基本确定下来不会变了,会变的只是这些元素内的相关操作,这时候,我们可以使用访问者模式对原有的代码进行重构一遍,这样一来,就可以在不修改各个元素类的情况下,对原有功能进行修改。
六、 访问者模式的适用场景
访问者模式的目的是要把处理从数据结构中分离出来,如果系统有比较稳定的数据结构,又有易于变化的算法的话,使用访问者模式是个不错的选择,因为访问者模式使的算法操作的增加变得容易。相反,如果系统的数据结构不稳定,易于变化,则此系统就不适合使用访问者模式了。
-
假如一个对象中存在着一些与本对象不相干(或者关系较弱)的操作,为了避免这些操作污染这个对象,则可以使用访问者模式来把这些操作封装到访问者中去。
-
假如一组对象中,存在着相似的操作,为了避免出现大量重复的代码,也可以将这些重复的操作封装到访问者中去。
参考
例子 设计模式之禅
双分派
http://blog.chinaunix.net/uid-20665047-id-3269091.html