引子
访问者模式在23种设计模式中应该算是最复杂也是最难以理解的一种模式了,因此在解释的时候我不打算从定义说起,以实际的例子带入可能会比较好吧。
例子
我们的例子是交通局需要对外公布所有交通工具的票价,假设现在交通局只负责管理公交车和火车的事儿,实际上也许他们不止管理这点东西
已有系统代码
代码枯燥但是也是必要的,交通工具抽象成接口如下
public interface Vehicle {
int getBasePrice();
}
两个子类
public class Bus implements Vehicle {
@Override
public int getBasePrice() {
return 50;
}
}
public class Train implements Vehicle {
@Override
public int getBasePrice() {
return 100;
}
}
系统的正事不要忘记,交通局要公布票价的
public class Manager {
List<Vehicle> vehicles = new ArrayList<Vehicle>();
public void add(Vehicle vehicle) {
vehicles.add(vehicle);
}
public void remove(Vehicle vehicle) {
vehicles.remove(vehicle);
}
public void printAllPrice() {
for (Vehicle vehicle : vehicles) {
System.out.println(vehicle.getBasePrice());
}
}
public static void main(String[] args) {
// 构造交通局
Manager manager = new Manager();
// 初始化,即添加交通局要管理的所有交通工具
manager.add(new Bus());
manager.add(new Train());
// 公布票价
manager.printAllPrice();
}
}
新需求
新年到了,票价变了,交通局要公布新年票价了,每个交通工具涨价幅度不一样,对原系统要做修改了。
首先是交通工具接口,新增一个方法
public interface Vehicle {
int getBasePrice();
int getNewYearPrice();
}
然后子类要修改,去实现这个方法
public class Bus implements Vehicle {
@Override
public int getBasePrice() {
return 50;
}
@Override
public int getNewYearPrice() {
return getBasePrice() * 2;
}
}
public class Train implements Vehicle {
@Override
public int getBasePrice() {
return 100;
}
@Override
public int getNewYearPrice() {
return getBasePrice() * 3;
}
}
好像涨价涨的有点凶,暂时不去吐槽这个问题,来看看交通局怎么办
public class Manager {
List<Vehicle> vehicles = new ArrayList<Vehicle>();
public void add(Vehicle vehicle) {
vehicles.add(vehicle);
}
public void remove(Vehicle vehicle) {
vehicles.remove(vehicle);
}
public void printNewYearPrice() {
for (Vehicle vehicle : vehicles) {
System.out.println(vehicle.getNewYearPrice());
}
}
public static void main(String[] args) {
// 构造交通局
Manager manager = new Manager();
// 初始化,即添加交通局要管理的所有交通工具
manager.add(new Bus());
manager.add(new Train());
// 公布新年票价
manager.printNewYearPrice();
}
}
大功告成
新需求+1+2+N
春节到了,交通局要公布春节票价了,这回可能要涨的更多了,毕竟春运嘛。。。儿童节到了,要公布儿童专属票价……
public interface Vehicle {
int getBasePrice();
int getNewYearPrice();
int getXxxPrice();
int getYyyPrice();
int getZzzPrice
// ......
}
我们发现,这种修改方法会导致接口内的方法需要被修改,然后每一个子类也都需要做相应改动,如果子类不止Bus和Train的话,这种体力劳动我觉得应该是能让人喝一壶的,这时候我们就可以考虑使用访问者来重构这个系统了。
访问者
现在我依然不去解释什么是访问者模式,但是我要说的是为了使用访问者模式,我们需要在交通工具接口里新增一个方法accept,该方法接受访问者作为参数,子类实现时则通过这个访问者参数访问自身
这个过程的子类通用的实现就是Visitor.visit(this)
访问者重构系统
还是通过代码来看吧,下面是接口,可以看到增加了一个以Visitor作为参数的accept方法
public interface Vehicle {
void accept(IVisitor visitor);
int getBasePrice();
}
两个子类实现
public class Bus implements Vehicle {
@Override
public void accept(IVisitor visitor) {
visitor.visit(this);
}
@Override
public int getBasePrice() {
return 50;
}
}
public class Train implements Vehicle {
@Override
public void accept(IVisitor visitor) {
visitor.visit(this);
}
@Override
public int getBasePrice() {
return 100;
}
}
那这个
visitor.visit(this)
到底是什么东西呢,来看看访问者的的实现就明白了
访问者有一个接口,它定义了在访问不同的交通工具时可以有的不同方法,我们有两个交通工具,因此这个接口里有两个方法
public interface IVisitor {
void visit(Bus bus);
void visit(Train train);
}
管理局要打印票价,那么作为交通工具的访问者,可以满足这个需求,我们实现一个访问者类
public class BasePricePrinter implements IVisitor {
@Override
public void visit(Bus bus) {
System.out.println(bus.getBasePrice());
}
@Override
public void visit(Train train) {
System.out.println(train.getBasePrice());
}
}
这个访问者类在访问不同的交通工具时就是简单的打印票价而已,那交通局要做的事情就简单啦
public class Manager {
List<Vehicle> vehicles = new ArrayList<Vehicle>();
public void add(Vehicle vehicle) {
vehicles.add(vehicle);
}
public void remove(Vehicle vehicle) {
vehicles.remove(vehicle);
}
public void printPrice(IVisitor visitor) {
for (Vehicle vehicle : vehicles) {
vehicle.accept(visitor);
}
}
public static void main(String[] args) {
// 构造交通局
Manager manager = new Manager();
// 初始化,即添加交通局要管理的所有交通工具
manager.add(new Bus());
manager.add(new Train());
// 构造一个基础票价的打印器访问者
IVisitor basePricePrinter = new BasePricePrinter();
// 公布票价
manager.printPrice(basePricePrinter);
}
}
交通局通过一个小弟基础票价访问者搞定了自己要做的所有事情,看看这个过程都在printPrice方法中,遍历了所有交通工具,对每一个交通工具都使用基础票价访问者去访问他们,基础票价访问者负责处理交通局派给它的任务——公布基础票价
新需求
新年到了,交通局需要公布新年票价,我们要做的仅仅是添加一个新年票价访问者的类就可以了
public class NewYearPricePrinter implements IVisitor {
@Override
public void visit(Bus bus) {
System.out.println(bus.getBasePrice() * 2);
}
@Override
public void visit(Train train) {
System.out.println(train.getBasePrice() * 3);
}
}
交通局的大爷不需要任何代码改动,派出新年票价访问者小弟解决所有事情
public static void main(String[] args) {
// 构造交通局
Manager manager = new Manager();
// 初始化,即添加交通局要管理的所有交通工具
manager.add(new Bus());
manager.add(new Train());
// 构造一个新年票价的打印器访问者
IVisitor newYearPricePrinter = new NewYearPricePrinter();
// 公布新年票价
manager.printPrice(basePricePrinter);
}
可以看到,这种情况下我们的改动量很少,当需要公布春节票价,公布啥时候的票价都非常简单,只需要添加一个访问者就可以实现了。
定义
例子好长,终于讲完了,回头可以看看定义了,《设计模式》一书对于访问者模式给出的定义是
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
从定义可以看出结构对象是使用访问者模式必须条件,而且这个结构对象必须存在遍历自身各个对象的方法。
在我们的访问者模式案例中,对象结构就是交通局管理的一堆交通工具,我们在使用访问者模式的时候的确是在新增对这些交通工具的操作,而且这些操作丝毫没有改变这些交通工具的类。
组成结构
访问者模式中一共有五个角色(除去Client)
- 抽象元素角色(Vehicle):定义一些Accept操作,它以一个访问者为参数,指定元素可以被哪些访问者访问。在我们的例子中就是Vehicle
- 具体元素角色(Bus,Train):包含的方法分为两部分,一部分是Accept操作,这部分是实现抽线元素角色所必须的,指定自身可以被哪些访问者访问,其代码实现往往是简单的一句话(visitor.visit(this)),另一部分是包含自身的业务逻辑方法,具体元素不同,这部分也可以各不相同。在我们的例子中就是Bus和Train了
- 对象结构角色(Manager):这是使用访问者模式必备的角色。它要具备以下特征:能枚举它的元素;可以提供一个高层的接口以允许该访问者访问它的元素;可以是一个复合(组合模式)或是一个集合,如一个列表或一个无序集合。我们可以把交通局当做一个对象结构角色。
- 抽象访问者角色(IVisitor):为对象结构角色的每一类对象(Bus,Train)都声明一个访问操作结构,该操作接口的名字和参数标识了发送访问请求给具体访问者的具体元素角色。
- 具体访问者角色(BasePricePrinter,NewYearPricePrinter):实现具体访问时要做的操作
类图
这个我就略过了,我想网络上应该有很多就不去画了。
优缺点
首先可以看到,使用访问者模式之后,对于原来的元素增加新的操作仅仅只需要实现一个新的访问者角色就行,而不比修改整个元素结构体(不需要去修改交通工具这个结构体所有实现类了),这样符合『开闭原则』的要求。而且由于每个访问者都对应于一个相关操作,所以如果这个操作变了,那么仅仅只需要修改这个具体访问者就行,比如例子里涨价幅度实在是太大了,群众闹变扭,就可以把价格调低一点。
访问者模式最适用的是元素结构变动不大的情况,即在可以预见的未来交通局还只能管理到汽车和火车的情况,如果哪天交通局权力变大了,要管UFO了,可以想象我们需要添加一个UFO类继承于Vehicle,这倒还好,关键是所有的访问者麻烦了,需要在抽象访问者中添加对UFO的处理,所有实现访问者也需要相应增加处理,感觉似乎回到了一开始我们面临的问题。除此之外,访问者模式需要让元素暴露自己的内部属性,也是他的缺点之一。