定义
封装一些作用于某种数据结构中的各元素的操作,他可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
我们来看几个角色的职责。
- Visitor——抽象访问者
抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是visit方法的参数定义哪些对象是可以被访问的。
- ConcreteVisitor——具体访问者
他影响访问者访问到一个类后该怎么干,要做什么事情。
- Element——抽象元素
接口或者抽象类,声明接受哪一类访问者访问,程序上是通过accept方法中的参数来定义的。
- ConcreteElement——具体元素
实现accept方法,通常是visitor.visit(this),基本上都形成了一种模式了。
- ObjectStruture——结构对象
元素产生者,一般容纳在多个不同类、不同接口的容器,如List、Set、Map等,在项目中,一般很少抽象出这个角色。
大家可以这样理解访问者模式,我作为一个访客(Visitor)到朋友家(Visited Class)去拜访,朋友之间聊聊天,喝喝酒,再相互吹捧吹捧,炫耀炫耀,这都正常。聊天的时候,朋友告诉我,他今年加官进爵了,工资也涨了30%,准备再买套房子,那我就在心里盘算(Visitor-self-method)“你这么有钱,我去年要借10万你都不借”,我根据朋友的信息,执行了自己的一个方法。
通用源码
我们来看看访问者模式的通用源码,先看抽象元素,如下所示。
public abstract class Element {
/**
* 定义业务逻辑
*/
public abstract void doSomething();
/**
* 允许谁来访问
*
* @param visitor
*/
public abstract void accept(IVisitor visitor);
}
抽象元素有两类方法:一是本身的业务逻辑,也就是元素作为一个业务处理单元必须完成的职责;另外一个是允许哪一个访问者来访问。我们来看具体元素,如下所示。
public class ConcreteElement1 extends Element {
@Override
public void doSomething() {
// 业务处理
}
@Override
public void accept(IVisitor visitor) {
visitor.visit(this);
}
}
public class ConcreteElement2 extends Element {
@Override
public void doSomething() {
// 业务处理
}
@Override
public void accept(IVisitor visitor) {
visitor.visit(this);
}
}
他定义了两个具体元素,我们再来看抽象访问者,一般是有几个具体元素就有几个访问方法,如下所示。
public interface IVisitor {
/**
* 访问element1元素
*
* @param element1
*/
void visit(ConcreteElement1 element1);
/**
* 访问element2元素
*
* @param element2
*/
void visit(ConcreteElement2 element2);
}
具体访问者如下所示。
public class Visitor implements IVisitor {
@Override
public void visit(ConcreteElement1 element1) {
element1.doSomething();
}
@Override
public void visit(ConcreteElement2 element2) {
element2.doSomething();
}
}
结构对象是产生出不同的元素对象,我们使用工厂方法模式来模拟,如下所示。
public class ObjectStruture {
/**
* 对象生成器,这里通过一个工厂方法模式模拟
*
* @return
*/
public static Element createElement() {
Random random = new Random();
if (random.nextInt(100) > 50) {
return new ConcreteElement1();
} else {
return new ConcreteElement2();
}
}
}
进入了访问者角色后,我们对所有的具体元素的访问就非常简单了,我们通过一个场景类模拟这种情况,如下所示。
public class Client {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
// 获得元素对象
Element element = ObjectStruture.createElement();
// 接受访问者访问
element.accept(new Visitor());
}
}
}
通过增加访问者,只要是具体元素就非常容易访问,对元素的遍历就更加容易了,甭管他是什么对象,只要他在一个容器中,都可以通过访问者来访问,任务集中化。这就是访问者模式。
优点
- 符合单一职责原则
具体元素角色也就是Employee抽象类的两个子类负责数据的加载,而Visitor类则负责报表的展现,两个不同的职责非常明确的分离开来,各自演绎变化。
- 优秀的扩展性
由于职责分开,继续增加对数据的操作是非常快捷的,直接在Visitor中增加一个方法,传递数据后进行处理。
- 灵活性非常高
通过访问者模式,把数据扔给访问者,由访问者来进行计算。
缺点
- 具体元素对访问者公布细节
访问者要访问一个类就必然要求这个类公布一些方法和数据,也就是说访问者关注了其他类的内部细节,这时迪米特法则所不建议的。
- 具体元素变更比较困难
具体元素角色的增加、删除、修改都是比较困难的。
- 违背了依赖倒置原则
访问者依赖的是具体元素,而不是抽象元素,这破坏了依赖倒置原则,特别是在面向对象的编程中,抛弃了对接口的依赖,而直接依赖实现类,扩展比较难。
使用场景
- 一个对象结构包含很多类对象,他们有不同的接口,而你想对这些丢想实施一些依赖于其具体类的操作,也就是说用迭代器模式已经不能胜任的情景。
- 需要对一个对象结构中的对象进行很多不同并且不想管的操作,而你想避免让这些操作“污染”这些对象的类。
总结一下,在这种地方你一定要考虑使用访问者模式:业务规则要求遍历多个不同的对象。这本身也是访问者模式出发点,迭代器模式只能访问同类或同接口的数据(当然了,如果你使用instanceof,那么能访问所有的数据,这没有争论),而访问者模式是对迭代器模式的扩充,可以遍历不同的对象,然后执行不同的操作,也就是针对访问的对象不同,执行不同的操作。访问者模式还有一个用途,就是充当拦截器角色。
扩展
统计功能
多个访问者
双分派
说到访问者模式就不得不提一下双分派问题,什么是双分派呢?我们先来解释一下什么是单分派和多分派,单分派语言处理一个操作是根据请求者的名称和接收到的参数决定的,在Java中有静态绑定和动态绑定之说,他的实现是依据重载和覆写实现的。我们来说一个简单的例子。
例如,演员演电影角色,一个演员可以扮演多个角色,我们先定义一个影视中的两个角色:功夫主角和白痴配角,如下所示。
public interface Role {
}
public class KungFuRole implements Role {
}
public class IdiotRole implements Role {
}
角色有了,我们再定义一个演员抽象类,如下所示。
public abstract class AbsActor {
/**
* 演员都能够扮演一个角色
*
* @param role
*/
public void act(Role role) {
System.out.println("演员可以扮演任何角色");
}
/**
* 可以演功夫戏
*
* @param role
*/
public void act(KungFuRole role) {
System.out.println("演员都可以演功夫角色");
}
}
很简单,这里使用了Java的重载,我们再来看青年演员和老年演员,采用覆写的方式来细化抽象类的功能,如下所示。
public class YoungActor extends AbsActor {
@Override
public void act(KungFuRole role) {
System.out.println("最喜欢演功夫角色");
}
}
public class OldActor extends AbsActor {
@Override
public void act(KungFuRole role) {
System.out.println("年龄大了,不能演功夫角色");
}
}
覆写和重载都已经实现,我们编写一个场景,如下所示。
public class Client {
public static void main(String[] args) {
// 定义一个演员
AbsActor actor = new OldActor();
// 定义一个角色
Role role = new KungFuRole();
// 开始演戏
actor.act(role);
actor.act(new KungFuRole());
}
}
重载在编译器就决定了要调用哪个方法,他是根据role的表面类型而决定调用act(Role role)方法,这时静态绑定;而Actor的执行方法act则是由其实际类型决定的,这时动态绑定。
一个演员可以扮演很多角色,我们的系统要适用这种变化,也就是根据演员、角色两个对象类型,完成不同的操作任务,该如何实现呢?很简单,我们让访问者模式上场就可以解决该问题,只要把角色类稍稍修改即可,如下所示。
public interface Role {
/**
* 演员要扮演的角色
*
* @param actor
*/
void accept(AbsActor actor);
}
public class KungFuRole implements Role {
@Override
public void accept(AbsActor actor) {
actor.act(this);
}
}
public class IdiotRole implements Role {
@Override
public void accept(AbsActor actor) {
actor.act(this);
}
}
场景类稍有改动,如下所示。
public class Client {
public static void main(String[] args) {
// 定义一个演员
AbsActor actor = new OldActor();
// 定义一个角色
Role role = new KungFuRole();
// 开始演戏
role.accept(actor);
}
}
不管演员类和角色类怎么变化,我们都能够找到期望的方法运行,这就是双反派。双反派意味着得到执行的操作决定于请求的种类和两个接收者的类型,他是多分派的一个特例。从这里也可以看到Java是一个支持多分派的单分派语言。
最佳实践
访问者模式是一种集中规整模式,特别适用于大规模重构的项目,在这一个阶段需求已经非常清晰,原系统的功能点也已经明确,通过访问者模式可以很容易把一些功能进行梳理,达到最终目的——功能集中化,如一个统一的报表运算、UL展现,我们还可以与其他模式混编建立一套自己的过滤器或者拦截器。