一、简述
访问者模式是一种将数据操作和数据结构分离的设计模式,是23种设计模式中非常复杂的一种,而且使用频率并不高。
定义:封装一些作用于某种数据结构中的各元素的操作(访问),可以在不改变这个数据的前提下定义作用于这些元素的新操作。
顾名思义,某些不能改变的数据,对于不同的访问者有不同的访问(或者操作),为不同的访问者提供相对应的操作。例如:公司CEO就能看到公司所有的真实财报数据,而作为一个员工可能就只能知道同比去年的增长比例。
- Visitor:访问者抽象类(或者接口),它定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素;理论上,它的方法个数与元素个数是一样的,因此,访问者模式要求元素的类族要稳定,不能频繁的添加、移除元素。如果出现频繁修改Visitor接口的情况,说明可能并不适合使用访问者模式。
- ConcreteVisitor:具体的访问者,需要实现每一个元素类访问时所产生的具体行为。
- Element:元素接口(或抽象类),它定义了一个接收访问者的方法(
accept()
方法),意义在于每一个元素都要刻意被访问者访问。 - ElementA、ElementB:具体的元素类,提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
- ObjectStructure:定义当中所提到的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素共访问者访问。
访问者模式的最大优点就是增加访问者非常容易,新创建一个实现了Visitor
接口的类,然后实现两个visit()
方法对不同的元素进行不同的操作,从而达到数据与数据操作分离的目的。如果不实用访问者模式,必定需要使用if-else
和类型转换,这便是代码的维护难度升级了。由此可以看出访问者模式的作用。
PS:访问者模式违反了迪米特原则(对访问者公布元素细节)以及依赖倒置原则(依赖了具体类,没有依赖抽象),由此可见,此模式需要应用在特定的情况中。
二、案例实现
这里就以公司为例,公司员工暂且分为开发人员和运营人员,而公司的CEO和CTO对于不同员工的KPI关注点不同,因此我们需要做出不同的处理,接着看看代码实现
员工基类
很简单,名字初始化和一个抽象的accept()
方法
public abstract class Staff {
public String name;
public int kpi;
public Staff(String name){
this.name = name;
kpi = new Random().nextInt(10);
}
//接受Visitor访问
public abstract void accept(Visitor visitor);
}
具体员工类
具体的员工,根据各自不同的职责添加了不同的方法,开发人员的KPI和代码产量相关,于是添加了获取代码行数的方法,而运营人员的KPI和新增用户量相关,于是添加了获取新增用户数的方法。
/**
* 开发人员
*/
public class Developer extends Staff {
public Developer(String name) {
super(name);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
//代码量
public int getCodeLines(){
return new Random().nextInt(10 * 1000);
}
}
/**
* 运营人员
*/
public class Operator extends Staff {
public Operator(String name) {
super(name);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
//新增用户数
public int getNewUserNum(){
return new Random().nextInt(10 * 10000);
}
}
访问者
接下来看看访问者类的定义
public interface Visitor {
//访问开发人员
public void visit(Developer developer);
//访问运营人员
public void visit(Operator operator);
}
这里可以看到,直接从方法上就区分Developer
和Operator
,这里主要考虑到的是,如果使用基类Staff
作为参数的话代码就会是这个样子
public void visit(Staff staff){
if(staff instanceof Developer){
Developer developer = (Developer)staff;
System.out.print("开发" + developer.name
+ ",KPI:" + developer.kpi + ",代码" + developer.getCodeLines() + "行");
}else if(staff instanceof Operator){
Operator operator = (Operator) staff;
System.out.print("运营" + operator.name + ",KPI:" + operator.kpi);
}
}
可以看到,在visit()
方法中,我们就需要判断参数的类型以及类型强制转换,这样的代码难以扩展和维护。
这是访问者模式的一个优点,也是一个缺点,优点在于代码清晰,某种程度上代码的维护和扩张更好;而缺点也是一样,如果需要添加一类
Staff
,所有的Visitor
都需要在实现一个新的visit()
方法。
接下来是具体的访问者代码,这里设定CTO更加关注开发人员,CEO更加关注运营人员。
public class CTOVisitor implements Visitor {
@Override
public void visit(Developer developer) {
System.out.print("开发" + developer.name
+ ",KPI:" + developer.kpi + ",代码" + developer.getCodeLines() + "行");
}
@Override
public void visit(Operator operator) {
System.out.print("运营" + operator.name + ",KPI:" + operator.kpi);
}
}
public class CEOVisitor implements Visitor {
@Override
public void visit(Developer developer) {
System.out.print("开发" + developer.name + ",KPI:" + developer.kpi);
}
@Override
public void visit(Operator operator) {
System.out.print("运营" + operator.name
+ ",KPI:" + operator.kpi + "新增用户:" + operator.getNewUserNum());
}
}
对象结构
这里的对象结构,直接就设定成了公司,集合就是员工们
public class Company {
private List<Staff> staffList = new ArrayList<>();
public void action(Visitor visitor){
for(Staff staff:staffList){
staff.accept(visitor);
}
}
/**
*
* @param staff
*/
public void addStaff(Staff staff){
staffList.add(staff);
}
}
客户端代码
public class Client {
public static void main(String[] agrs){
Company company = new Company();
company.addStaff(new Developer("Bruce Wayne"));
company.addStaff(new Developer("ClarkKent"));
company.addStaff(new Developer("Barry Allen"));
company.addStaff(new Operator("Diana Prince"));
company.addStaff(new Operator("Oliver Queen"));
company.addStaff(new Operator("Dinah Lance"));
CEOVisitor ceo = new CEOVisitor();
company.action(ceo);
CTOVisitor cto = new CTOVisitor();
company.action(cto);
}
}
具体输出如下:
CEO所看到的======
开发Bruce Wayne,KPI:6
开发ClarkKent,KPI:2
开发Barry Allen,KPI:8
运营Diana Prince,KPI:4,新增用户:46642
运营Oliver Queen,KPI:1,新增用户:7687
运营Dinah Lance,KPI:3,新增用户:67382
CTO所看到的======
开发Bruce Wayne,KPI:6,代码8285行
开发ClarkKent,KPI:2,代码8351行
开发Barry Allen,KPI:8,代码658行
运营Diana Prince,KPI:4
运营Oliver Queen,KPI:1
运营Dinah Lance,KPI:3
三、分派
变量被声明时的类型叫做变量的静态类型(Static Type),静态变量类型又可以叫做明显类型(Apparent Type);而变量所引用的对象的正式类型叫做变量的实际类型(Actual Type)。
List list = new ArrayList();
在Java代码中很常见的一种写法,声明父类对象创建子类对象;声明是List
类型(也就是静态类型即明显类型),创建的是ArrayList
的对象(实际类型)。
这里就需要提到一个词,分派(Dispatch)。当使用上述形式声明并创建对象,根据对象的类型对方法进行选择,这就是分派,而分派有可以分为静态分派(Static Dispatch)和动态分派(Dynamic Dispatch)。
- 静态分派,对应的就是编译时,根据静态类型信息发生的分派。方法重载就属于静态分派
- 动态分派,对应的就是运行时,动态地置换掉某个方法。方法重写就属于动态分派
静态分派
简化三个类之间的关系
public class Staff {
}
public class Developer extends Staff {
}
public class Operator extends Staff {
}
执行类,execute()方法有三个重载方法,方法的参数分别上面对应的三个类型Staff
、Developer
、Operator
的对象。
public class Execute {
public void execute(Staff staff){
System.out.println("员工");
}
public void execute(Developer developer){
System.out.println("开发人员");
}
public void execute(Operator operator){
System.out.println("运营人员");
}
}
测试代码以及测试结果
public class Client {
public static void main(String[] agrs){
System.out.println("运行结果:");
Staff staff = new Staff();
Staff staff1 = new Developer();
Staff staff2 = new Operator();
Execute execute = new Execute();
execute.execute(staff);
execute.execute(staff1);
execute.execute(staff2);
}
}
运行结果:
员工
员工
员工
可以推断出,传入三个对象,最后执行的方法都是参数类型是Staff
的方法,即使三个对象有不同的真实类型
方法重载中实际起作用的是它们静态类型,也就是在编译时期就完成了分派,即静态分派。
动态分派
三个类自带execute()
方法,Developer
和Operator
继承Staff
,并重写了execute()
方法
public class Staff {
public void execute(){
System.out.println("员工");
}
}
public class Developer extends Staff {
@Override
public void execute() {
System.out.println("开发人员");
}
}
public class Operator extends Staff {
@Override
public void execute() {
System.out.println("运营人员");
}
}
测试代码以及结果
public class Client {
public static void main(String[] agrs) {
System.out.println("运行结果:");
Staff staff = new Staff();
staff.execute();
Staff staff1 = new Developer();
staff1.execute();
Staff staff2 = new Operator();
staff2.execute();
}
}
运行结果:
员工
开发人员
运营人员
测试时的情况相同,三个对象,其静态类型都是Staff
,而实际类型分别是Staff
、Developer
和Operator
。可以看到重写execute()
方法都生效了,各自输出了对应的内容。
Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。
单分派与多分派
首先需要了解一个叫宗量的概念。一个方法所属的对象叫做方法的接收者,方法的接收者与方法的参量统称做方法的宗量。而根据分派可以基于多少种宗量,可以将面向对象的语言划分为单分派语言和多分派语言。
单分派语言根据一个宗量的类型(真实类型)进行对方法的选择
多分派语言根据多个的宗量的类型对方法进行选择
那Java
属于什么类型呢?
我们可以分析一下,Java
中静态分派时决定方法的选择的宗量包括方法的接收者和方法参数的静态类型,所以是多分派;而在动态分派时,方法的选择只会考虑方法的接收者的实际类型,所以是单分派。其实Java
语言是支持静态多分派和动态单分派的语言。
双(重)分派
那双重分派又是什么呢?分派和访问者模式又有什么关系呢?接下来就会解释这些问题
Java
支持静态多分派和动态单分派,并不支持动态多分派;于是就有了两次单分派组成的双重分派来替代动态多分派。而访问者模式正好就用到了双重分派的技术。
双重分派技术就是在选择一个方法的时候,不仅仅要根据方法的接收者的运行时区别,还要根据参数的运行时区别(这样达到两次分派的效果)。
在访问者模式中,客户端将具体的对象传递给访问者,也就是staff.accept(visitor);
方法的调用,完成第一次分派;然后具体的访问者作为参数传入到具体的对象的方法中,也就是这句代码visitor.visit(this);
,将this
作为参数传递进去完成第二次分派。双分派意味着得到的执行操作决定于请求的种类和接受者的类型。双重分派的核心就是this
对象。
从访问者模式可以看出,双重分派就是在冲在方法委派的前面加上了继承的重写,使得从某种角度来说重载变成了动态。
Android源码中的访问者模式
相信注解应该不会陌生,现在很多出名框架的使用方式都是使用注解,例如:ButterKnife
、Dagger
、Retrofit
等等,都是以注解的方式使用,已达到简化代码或者降低耦合度的目的。而注解又可以分为运行时注解和编译时注解,运行时注解由于性能问题也一直被人诟病,编译时注解的核心原理依赖APT(Annotation Processing Tools)实现,之前提到的框架也是基于APT实现的。
而对于注解的解析过程就是遵从访问者模式的,其元素就是包、类、方法、方法参数等(其实就是可以被添加注解那些元素),对于元素的访问者支持所有的元素访问,通过继承一个抽象的元素访问者实现针对不同类型进行不同的处理。
注解相关具体的内容我不是很了解,只是简单的说明一下
四、总结
访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。访问者模式适用于数据结构相对稳定算法又易变化的系统。因为访问者模式使得算法操作增加变得容易。若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。
优点
- 扩展性好: 在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
- 复用性好: 通过访问者来定义整个对象结构通用的功能,从而提高复用程度。
- 分离无关行为: 通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。
缺点
- 对象结构变化很困难: 不适用于对象结构中的类经常变化的情况,因为对象结构发生了改变,访问者的接口和访问者的实现都要发生相应的改变,代价太高。
- 破坏封装: 访问者模式通常需要对象结构开放内部数据给访问者和
ObjectStructrue
,这破坏了对象的封装性。