访问者模式是一种将数据操作与数据结构分离的设计模式。它是《设计模式》23种设计中较复杂的一种。但它的使用频率并不高,正如作者GOF对访问者的描述:大多数情况下,你并不需要使用访问者,但当你使用到它的时候,那你就真的需要它了。
访问者的基本想法是:软件系统拥有一个由许多对象构成的、比较稳定的对象结构,这些对象的类都拥有一个accept方法用来接收对访问者对象的访问,而访问者是一个接口,他拥有一个visit方法,这个方法对访问到的对象结构中不同类型的元素做出不同的处理。在对象结构的一次访问过程中,我们遍历整个对象结构,每一个元素都实施accept方法,而又在每一个元素的accept方法中会调用访问者模式visit方法,从而是访问者得以处理对象结构中的每一个元素,而且能够对不同的元素分别做不同的处理。说了这么多,可能有些人还是一头雾水,说的嘛东西!听不懂没关系,你看完了这篇博客回头再来体会或许就一目了然了。
举个例,如果我们有一个数据结构(List),而这个List中存放的数据(Employee)并非一种类型,比如有工程师、销售、项目经理等,现在集团CEO要给他们发年终奖,而这三类员工的奖金制度都不一样,所以需要对着几种类型的员工分别进行相应制度的奖金评审。这个时候就满足访问者模式的思想了。到了这里我们先停一下,就刚刚所诉的案例,如果我们还不会访问者模式,你此时会如何解决呢?我猜初学者可能会这样设计,对于装有不同数据类型的List,我们遍历的时候会对取出的每一个类型的员工并进行对应的奖金计算方法调用。而计算的方法会在所有员工的父类里边声明:
public class Employee{
int result = 0;
int employeeType //标注员工的职位类型 工程师、销售、项目经理
public int calculatePrizeForEngineer(){
/**
* 工程师具体的评审方法
*/
return result;
}
public int calculatePrizeForSalesman(){
/**
* 销售具体的评审方法
*/
return result;
}
public int calculatePrizeForManager(){
/**
* 经理具体的评审方法
*/
return result;
}
}
调用的时候根据employeeType使用if、else语句来分开评审调用。这不很简单吗,轻松搞定。相信很多人都会这么做,功能实现了,但是这种写法会使你的程序非常臃肿,有很大的隐患!如果日后制度改变了,公司整体只提升了对工程师的奖金评审规则,那怎么办?你肯定会重头查看代码,最后花了很多时间,终于找到了是在Employee类的calculateForEngineer方法中修改一下代码,完工。好的,如果这个时候有一个大的改变,现在人资需要在这个系统计算每个员工的月薪,你说WTF!这需求为什么不早说!没办法,我只能在这个Employee类里边再添加四个方法,分别计算不同类型员工的薪资技术操作。假设你以后离职了,现在公司规模扩充,又有新的一类职务(售后服务)作为员工需要添加到奖金评审系统,然后新来的程序员就会重头到尾先梳理一下业务逻辑,然后花了半天最后找到在Employee类添加一个calculatePrizeForServer()和calculateSalaryForServer()两个方法,如果这个程序员的能力在你之上,而素质在你之下,那你可能就要时常打一下喷嚏了!这明明能使用访问者模式来把“数据操作”和“数据结构”分离嘛!这人却偏偏揉到一起。太TM难维护了,而现在项目已步入成熟稳定的运营阶段,想改都改不了了。(而他日后估计改一次,心里就一次)。其实在这里我们的CEO算作一个访问者,负责计算奖金的业务逻辑,而人资HR又算作一个访问者,负责处理另外一项业务计算。对访问到的对象结构(员工列表)中不同类型的元素(具体的某类员工)做出不同的处理(奖金还是薪资)。这不正是访问者模式的宗旨吗?好了 说到这里,我们就来看看访问者模式是如何实现的呢?话不多说,先看类图:
从类图的结构上来说,应该算是比较复杂的了。其实本身也是最难的一种设计模式。
下面介绍一下这五种角色:
抽象元素(Employee):一个抽象类,它定义了接受访问者(Visitor)的操作accept()。
具体元素(Engineer):抽象元素的子类,一般有多个,代表不同的数据类型,比如:工程师、经理、销售员。
抽象访问者(Visitor):一个接口,定义了操作来自不同数据的所有方法。
具体访问者(CEOVisitor):实现Visitor接口的类,实现了针对不同类型的数据进行不同类型的计算。
对象结构(Object Structure):一个集合,用于存放Element对象(元素),并提供遍历元素的方法,这里通常我们使用的就是系统自带的List集合类。
看了类图,应该大致明白如何编码了吧!下面结合代码进行说明:
/**
* 员工的基类 定义了员工的基本工资和接受访问者的方法
*/
public abstract class Employee{
int baseSalary;
public abstract void accept(Visitor visitor);
}
具体元素:三个职位 基础工资分别为 一万、五千、两万。
public class Engineer extends Employee{
@Override
public void accept(Visitor visitor) {
visitor.visitorEngineer(this);
}
public int getBaseSalary() {
return 10000;
}
}
public class SalesMan extends Employee{
@Override
public void accept(Visitor visitor) {
visitor.visitorSalesMan(this);
}
public int getBaseSalary() {
return 5000;
}
}
public class Manager extends Employee{
@Override
public void accept(Visitor visitor) {
visitor.visitorManager(this);
}
public int getBaseSalary() {
return 20000;
}
}
抽象访问者:对每种类型的元素定义相关的操作
public interface Visitor {
public void visitorManager(Manager user);
public void visitorEngineer(Engineer user);
public void visitorSalesMan(SalesMan user);
}
具体访问者:CEO评定年终奖、HR评定工资
public class CEOVisitor implements Visitor{
@Override
public void visitorManager(Manager user) {
System.out.println("经理的年终奖为:"+ user.getBaseSalary() * 3 + "元"); //三倍月薪的奖金
}
@Override
public void visitorEngineer(Engineer user) {
System.out.println("工程师的年终奖为:"+ user.getBaseSalary() * 2+ "元"); //两倍月薪的奖金
}
@Override
public void visitorSalesMan(SalesMan user) {
System.out.println("销售的年终奖为:"+ user.getBaseSalary() + "元"); //一个月的工资作为奖金
}
}
public class HRVisitor implements Visitor{
@Override
public void visitorManager(Manager user) {
System.out.println("经理的实发工资为:"+ (user.getBaseSalary() * 0.75 - 1500) + "元"); //扣除税和五险一金
}
@Override
public void visitorEngineer(Engineer user) {
System.out.println("工程师的实发工资为:"+ (user.getBaseSalary() * 0.8 - 1000) + "元"); //扣除税和五险一金
}
@Override
public void visitorSalesMan(SalesMan user) {
System.out.println("销售的实发工资为:"+ (user.getBaseSalary() * 0.9 - 500) + "元"); //扣除税和五险一金
}
}
对象结构:这里的对象结构我们就用List集合下的ArrayList。
下面是测试类:
public class Main {
public static void main(String args[]){
Employee engineer = new Engineer();
Employee manager = new Manager();
Employee salesMan = new SalesMan();
List<Employee> list = new ArrayList(); //创建一个对象结构并把元素都放入集合中
list.add(engineer);
list.add(manager);
list.add(salesMan);
Visitor ceoVisitor = new CEOVisitor(); //实例化一个访问者(CEO)
for (Employee employee : list){ //对ObjectStructure进行迭代
employee.accept(ceoVisitor); //给每个元素添加一个具体访问者,HR分别评定他的奖金
}
System.out.println("-----------");
Visitor hrVisitor = new HRVisitor(); //实例化一个访问者(HR)
for (Employee employee : list){ //对ObjectStructure进行迭代
employee.accept(hrVisitor); //给每个元素添加一个具体访问者,HR分别评定他的实发工资
}
}
}
打印出的结果为:
这就完成了一个简单的访问者模式。
现在再去体会文章开头“访问者的基本想法”是不是很清楚了呢。每种设计模式自然有他存在的道理,虽然这种模式我们日常开发中并不常见,但你学会了,以后肯定有机会使用到。到那时整个项目层次感就显得非常清晰,比较访问者模式的最大优点就是把“数据操作”和“数据结构”分离。如果用传统的编程方式去实现,那代码的逻辑会显得非常复杂,试想一下,一个对象结构里有无数个对象,还需要对每一种对象进行对应的数据操作!好繁琐啊,尤其是针对大项目还交接给别人后,想想改起来的那个感觉。。。!
最后我们再总结一下:
一、访问者模式在不改变类的情况下可有效的增加其上的操作,为了达到这样的效果,使用了一种称为“双重分派”的技术:被访问者(Manger)首先调用accept(Visitor visitor)方法“接受”访问者,而被接受的访问者(CEOVisitor)在调用visitorManager方法访问当前的element对象,并对返回的数据进行操作。
二、可以在不改变一个集合中元素的类情况下,增加新的施加于该元素上的新操作,比如HR对他的实发工资的计算,日后还可以再添加一个访问者,用于对员工绩效的统计。对元素的操作可以无限增加,而元素本身却没有时候的改变。相当于对元素自身的功能操作转嫁到访问者中帮它处理了。从整个ObjectStructure对象结构(包括无数元素对象的集合)中来看,巧妙的实现了“数据操作”和“数据结构”的分离。
三、可以将集合中个元素的某些操作几种到访问者中,不仅便于集合的维护,也有利于集合中元素的复用。
使用场景:当一个对象结构(比如List或它的子类)包含若干个不同种类的元素,针对不同种类的元素需要有不同的操作,且需要遍历每一个元素的时候,我们就可以使用到访问者模式了!
比如上面案例中:
对象结构就是List
不同种类的元素就是经理、工程师和销售员
不同的操作就是针对三种元素的操作,例如奖金评定或薪资结算
当然前提是需要对象结构需要有迭代器来遍历它自己的元素。