这个模式的基本想法:首先我们拥有一个由许多对象构成的对象结构, 这些对象的类都拥有一个 accept
方法用来接受访问者对象;
访问者是一个接口,它拥有一个 visit
方法,这个方法对访问到的对象结构中不同类型的元素作出不同的反应;在对象结构的一次访问过程中,我们遍历整个对象结构,对每一个元素都使用 accept
方法。
在每一个元素的 accept
方法中回调访问者的 visit
方法,从而使访问者得以处理对象结构的每一个元素。我们可以针对对象结构设计不同的访问者类来完成不同的操作。
下面是一个示例
public class VisitorDemo {
static public void main(String[] args) {
Car car = new Car();
Visitor visitor = new PrintVisitor();
car.accept(visitor);
}
}
// 访问者接口,提供访问对象的方法,利用java重载特性,根据参数的不同执行不同的逻辑
interface Visitor {
void visit(Wheel wheel);
void visit(Engine engine);
void visit(Body body);
void visit(Car car);
}
// 数据结构,车轮
class Wheel {
private String name;
Wheel(String name) { this.name = name; }
String getName() { return this.name; }
// 接受访问的方法
void accept(Visitor visitor) { visitor.visit(this); }
}
// 数据结构 汽车引擎
class Engine {
// 接受访问的方法
void accept(Visitor visitor) { visitor.visit(this); }
}
//数据结构 汽车主体
class Body {
// 接受访问的方法
void accept(Visitor visitor) { visitor.visit(this); }
}
//数据结构 汽车
class Car {
private Engine engine = new Engine();
private Body body = new Body();
private Wheel[] wheels
= {new Wheel("front left"), new Wheel("front right"),
new Wheel("back left"), new Wheel("back right")};
// 接受访问的方法
void accept(Visitor visitor) {
visitor.visit(this);
engine.accept(visitor);
body.accept(visitor);
for (int i = 0; i < wheels.length; ++i)
wheels[i].accept(visitor);
}
}
//通过实现Visitor接口,可以设计不同的访问者类完成不同的操作,这里PrintVisitor类主要用与打印
class PrintVisitor implements Visitor {
//处理汽车车轮类的visit方法
public void visit(Wheel wheel) { System.out.println("Visiting " + wheel.getName() + " wheel"); }
//处理汽车引擎类的visit方法
public void visit(Engine engine) { System.out.println("Visiting engine"); }
//处理汽车主体类的visit方法
public void visit(Body body) { System.out.println("Visiting body"); }
//处理汽车类的visit方法
public void visit(Car car) { System.out.println("Visiting car"); }
}
Visitor模式提供了一种multi-dispatch(多分派) 中的double dispatch(双分派)的实现方式, 扩展对于像Java,C++这样的单分派语言的一种模拟 multi-dispatch(多分派) 技术
什么是双分派和多分派
双分派是多分派的特例,实际上双分派是一种很经典的技术,但是当前的主流的面向对象程序设计语言(例如C++/Java/C#等)都并不支持双分派或多分派,仅仅支持单分派(single dispatch)。
单分派的含义比较好理解,单分派就是说我们在选择一个方法的时候仅仅需要根据消息接收者(receiver)的运行时类型。实际上这也就是我们经常提到的多态的概念。举一个简单的例子,我们有一个基类A,A有一个虚方法f(可被子类override),D1和D2是A的两个子类,在D1和D2中我们覆写(override)了方法f。这样我们对消息f的调用,需要根据接收者A或者A的子类D1/D2的具体型别才可以确定具体是调用A的还是D1/D2的f方法。
双分派则在选择一个方法的时候,不仅仅要根据消息接收者(receiver)
的运行时类型,还要根据参数的运行时类型。当然如果所有参数都考虑的话就是多分派。也举一个简单的例子,同于上面单分派中例子,A的虚方法f带了一个C型别的参数,C也是一个基类,C有也有两个具体子类E1和E2。这样,当我们在调用消息f的时候,我们不但要根据接收者的具体型别(A、D1、D2),还要根据参数的具体型别(C、E1、E2),才可以最后确定调用的具体是哪一个方法f。
通过下面示例继续了解访问者模式
首先定义好数据结构,在这个示例中,工厂下有仓库和商品两个子类,所以我们将数据结构抽象为下面三个类
/** 数据结构基类,拥有公共属性和方法 */
public abstract class Flow {
private String name;
public abstract void accept(FlowVisitor visitor);
public void setName(String name) { this.name = name; }
public String getName() { return name; }
}
/** 商品信息 */
public class Goods extends Flow {
public Goods(String name) { super.setName(name); }
@Override
public void accept(FlowVisitor visitor) {
visitor.visit(this);
}
}
/** 仓库信息 */
public class Warehouse extends Flow {
private List<Flow> children;
public Warehouse(String name) {
children = new ArrayList<>();
setName(name);
}
@Override
public void accept(FlowVisitor visitor) {
visitor.visit(this);
for (Flow child : children) {
child.accept(visitor);
}
}
/** 添加商品 */
public void add(Flow flow) { children.add(flow); }
}
接下来是Visitor的抽象以及两个实现类,实际上Visitor所做的操作即封装上述两个数据结构对象
public interface FlowVisitor {
void visit(Warehouse warehouse);
void visit(Goods goods);
}
public class SearchVisitor implements FlowVisitor{
private Logger LOGGER = LoggerFactory.getLogger(WarehouseVisitor.class);
private Pattern fileNamePattern;
public SearchVisitor(String fileNamePattern) {
this.fileNamePattern = Pattern.compile(fileNamePattern);
}
@Override
public void visit(Warehouse warehouse) {
if (fileNamePattern.matcher(warehouse.getName()).find()) {
LOGGER.info(warehouse.getName());
}
}
@Override
public void visit(Goods goods) {
if (fileNamePattern.matcher(goods.getName()).find()) {
LOGGER.info("搜索商品:"+goods.getName());
}
}
}
public class WarehouseVisitor implements FlowVisitor {
private Logger LOGGER = LoggerFactory.getLogger(WarehouseVisitor.class);
@Override
public void visit(Warehouse warehouse) {
LOGGER.info(warehouse.getName());
}
@Override
public void visit(Goods goods) {
LOGGER.info(goods.getName());
}
}
下面main方法中,传入不同类型的Visitor实现不同的功能
public class App {
public static Warehouse create() {
// 实例化一批商品
Warehouse warehouse = new Warehouse("[1_仓库]");
warehouse.add(new Goods("[1_仓库]-1商品"));
warehouse.add(new Goods("[1_仓库]-2商品"));
Warehouse area = new Warehouse("[1_仓库]-[1_库区]");
warehouse.add(area);
return warehouse;
}
public static void main(String[] args) {
Warehouse warehouse = create();
warehouse.accept(new WarehouseVisitor());
warehouse.accept(new SearchVisitor("2商品"));
}
}
执行结果如下
[main] INFO com.company.goods.WarehouseVisitor - [1_仓库]
[main] INFO com.company.goods.WarehouseVisitor - [1_仓库]-1商品
[main] INFO com.company.goods.WarehouseVisitor - [1_仓库]-2商品
[main] INFO com.company.goods.WarehouseVisitor - [1_仓库]-[1_库区]
[main] INFO com.company.goods.WarehouseVisitor - 搜索商品:[1_仓库]-2商品
访问者模式使用了多态性来实现过程的动态绑定,相较于其他设计模式要复杂一些。访问者模式同时利用了override
和overload
实现了双分派,及同时利用方法的静态绑定和动态绑定,通过这样的设计实现异常强大的特性。对于数据结构的操作可以自由扩充,只需实现FolwVisitor
接口,添加操作数据结构的新功能,而不用对数据结构本身做任何改动。
不过这个模式也有一些缺点,首先最明显的是它的复杂度。一旦处理的业务逻辑变的复杂,维护起来也是相当有难度的。其次是如果对数据结构本身的类继承体系进行大改动的话,相应的其他改动也是非常大的。
所以访问者模式适用于在数据结构比较稳定的实际应用中。