一、介绍
访问者模式是一种将数据操作与数据结构分离的设计模式,它是《设计模式》中23种设计模式中最复杂的一个,但它的使用频率并不高,正如《设计模式》的作者GOF对访问者模式的描述:大多数情况下,你不需要使用访问者模式,但是当你一旦需要使用它时,那你就是真的需要它了。
访问者模式的基本想法是,软件系统中拥有一个由许多对象构成的、比较稳定的对象结构,这些对象的类都拥有一个accept方法用来接受访问者对象的访问。访问者是一个接口,它拥有一个visit方法,这个方法对访问到的对象结构中不同类型的元素作出不同的处理。在对象结构的一次访问过程中,我们遍历整个对象结构,对每一个元素都实施accept方法,在每一个元素的accept方法中会调用访问者的visit方法,从而使访问者得以处理对象结构的每一个元素,我们可以针对对象结构设计不同的访问者类来完成不同的操作,达到区别对待的效果。
二、定义
封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。
三、使用场景
对象结构比较稳定,但经常需要在此对象结构上定义新的操作。
需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免这些操作”污染“这些对象的类,也不希望在增加新操作时修改这些类。
四、访问者模式的UML类图
UML类图:
角色介绍:
Visitor:接口或抽象类,定义了对每一个元素的访问行为,参数就是可访问的元素,方法个数理论上是个元素个数一样的。因此,访问者模式要求被访问的对象结构要稳定,如果经常增删元素,必然会导致频繁修改Visitor接口,就不适合用访问者模式了。
ConcreteVisitor:具体的访问者,定义具体的对每一个元素的具体访问行为。
Element:抽象的元素接口或抽象类,定义了一个接待访问者的方法,让每个元素都可以被访问者访问。
ElementA,ElementB:具体的元素类,提供接收访问方法的具体实现。这个具体实现通常是调用访问者提供的访问该元素的方法。
ObjectStructure:定义对象结构,里面维护了一个元素的集合,并且迭代这些元素供访问者访问。
五、简单示例
情景:年终了,公司会给员工进行业绩考核。但是,不同领域的管理人员对于员工的评定标准不一样。现在员工有工程师和经理,评定者有CEO和CTO,我们假定CTO只关注工程师的代码量、经理的新产品数量,而CEO关注的是工程师的KPI和经理的KPI以及新产品数量。
员工基类:
/*** 员工基类(Element) */
public abstract class Staff {//员工姓名public String name;//员工KPIpublic int kpi;public Staff(String name) {super();this.name = name;this.kpi = new Random().nextInt(10);}//接受Visitor的访问public abstract void accept(Visitor visitor);}
工程师:
/*** 工程师 */
public class Engineer extends Staff{private int codeLines;//代码数量public Engineer(String name) {super(name);codeLines = new Random().nextInt(10 * 10000);}@Overridepublic void accept(Visitor visitor) {visitor.visit(this);}//工程师这一年写的代码数量public int getCodeLines(){return codeLines;}
}
经理:
/*** 经理*/
public class Manager extends Staff{private int products;//产品数量public Manager(String name) {super(name);products = new Random().nextInt(10);}@Overridepublic void accept(Visitor visitor) {visitor.visit(this);}//一年内做的产品数量public int getProducts(){return products;}
}
Visitor类:
public interface Visitor {/*** 访问工程师类型*/public void visit(Engineer engineer);/*** 访问经理类型*/public void visit(Manager manager);
}
CEO访问者:
public class CEOVisitor implements Visitor {@Overridepublic void visit(Engineer engineer) {System.out.println("工程师:" + engineer.name + ", KPI:" + engineer.kpi);}@Overridepublic void visit(Manager manager) {System.out.println("经理:" + manager.name + ", KPI:" + manager.kpi+ ", 新产品数量 :" + manager.getProducts());}}
CTO访问者:
public class CTOVisitor implements Visitor {@Overridepublic void visit(Engineer engineer) {System.out.println("工程师:" + engineer.name + ", 代码数量:" + engineer.getCodeLines());}@Overridepublic void visit(Manager manager) {System.out.println("经理:" + manager.name +", 产品数量 :" + manager.getProducts());}}
员工报表:
//员工业务报表类(ObjectStructure)
public class BusinessReport {List<Staff> mStaffs = new LinkedList<Staff>();public BusinessReport() {mStaffs.add(new Manager("王经理"));mStaffs.add(new Engineer("工程师-A"));mStaffs.add(new Engineer("工程师-B"));mStaffs.add(new Manager("李经理"));mStaffs.add(new Engineer("工程师-C"));}/*** 为访问者展示报表 * @param visitor 如CEO、CTO*/public void showReport(Visitor visitor){for(Staff staff : mStaffs){staff.accept(visitor);}}
}
Client访问:
public class Client {public static void main(String[] args) {//构建报表BusinessReport report = new BusinessReport();System.out.println("===== 给CEO看报表 =====");//设置访问者CEOreport.showReport(new CEOVisitor());System.out.println("===== 给CTO看报表 =====");//设置访问者CTOreport.showReport(new CTOVisitor());}
}
结果:
===== 给CEO看报表 =====
经理:王经理, KPI:2, 新产品数量 :5
工程师:工程师-A, KPI:5
工程师:工程师-B, KPI:7
经理:李经理, KPI:9, 新产品数量 :8
工程师:工程师-C, KPI:1
===== 给CTO看报表 =====
经理:王经理, 产品数量 :5
工程师:工程师-A, 代码数量:26238
工程师:工程师-B, 代码数量:8282
经理:李经理, 产品数量 :8
工程师:工程师-C, 代码数量:47927
从上面代码中可以看出,如果要增加一个访问者,你新创建一个实现了Visitor接口的类,然后实现两个visit方法来对不同的元素进行不同的操作,从而达到数据对象与数据操作相分离的效果。如果不使用访问者模式,而又想对不同元素进行不同的操作,那么必定会使用if-else和类型转换,这使得代码难以升级维护。
六、Android中的访问者模式
安卓中的著名开源库ButterKnife、Dagger、Retrofit都是基于APT(Annotation Processing Tools)实现。而编译注解核心依赖APT。当我们通过APT处理注解时,最终会将获取到的元素转换为相应的Element元素,以便获取到它们对应信息。那么元素基类的源码如下:(路径:javax.lang.model.element.Element)
public interface Element extends javax.lang.model.AnnotatedConstruct {/*** Returns the {@code kind} of this element.** @return the kind of this element*/ElementKind getKind();//获取元素类型//代码省略/*** Applies a visitor to this element.** @param <R> the return type of the visitor's methods* @param <P> the type of the additional parameter to the visitor's methods* @param v the visitor operating on this element* @param p additional parameter to the visitor* @return a visitor-specified result*/<R, P> R accept(ElementVisitor<R, P> v, P p);//接受访问者的访问
}
ElementVisitor就是访问者类型,ElementVisitor源码如下:
public interface ElementVisitor<R, P> {/*** Visits an element.* @param e the element to visit* @param p a visitor-specified parameter* @return a visitor-specified result*/R visit(Element e, P p);/*** A convenience method equivalent to {@code v.visit(e, null)}.* @param e the element to visit* @return a visitor-specified result*/R visit(Element e);/*** Visits a package element.* @param e the element to visit* @param p a visitor-specified parameter* @return a visitor-specified result*/R visitPackage(PackageElement e, P p);/*** Visits a type element.* @param e the element to visit* @param p a visitor-specified parameter* @return a visitor-specified result*/R visitType(TypeElement e, P p);/*** Visits a variable element.* @param e the element to visit* @param p a visitor-specified parameter* @return a visitor-specified result*/R visitVariable(VariableElement e, P p);/*** Visits an executable element.* @param e the element to visit* @param p a visitor-specified parameter* @return a visitor-specified result*/R visitExecutable(ExecutableElement e, P p);/*** Visits a type parameter element.* @param e the element to visit* @param p a visitor-specified parameter* @return a visitor-specified result*/R visitTypeParameter(TypeParameterElement e, P p);/*** Visits an unknown kind of element.* This can occur if the language evolves and new kinds* of elements are added to the {@code Element} hierarchy.** @param e the element to visit* @param p a visitor-specified parameter* @return a visitor-specified result* @throws UnknownElementException* a visitor implementation may optionally throw this exception*/R visitUnknown(Element e, P p);
}
在ElementVisitor中定义了多种visit接口,每个接口处理一种元素类型,那么这就是典型的访问者模式。
七、总结
正如本节开头引用GOF的话所说:大多数情况下,你不需要使用访问者模式,但是,当你一旦需要使用它时,那你就是真的需要它了。在现实情况下,我们要根据具体的情况来评估是否适合使用访问者模式,例如,我们的对象结构是否足够稳定,使用访问者模式是否能够优化我们的代码,而不是使我们的代码变得更复杂。在使用一个模式之前,我们应该明确它的使用场景、它能解决什么问题等,以此来避免滥用设计模式的现象。
优点:
各角色职责分离,符合单一职责原则。
具有优秀的扩展性。
使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化。
灵活性。
缺点:
具体元素对访问者公布细节,违反了迪米特原则。
具体元素变更时导致修改成本大。
违反了依赖倒置原则,为了达到“区别对待”而依赖了具体类,没有依赖抽象。