设计模式之访问者模式 _ JAVA
这个模式的基本想法:首先我们拥有一个由许多对象构成的对象结构, 这些对象的类都拥有一个 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 Listchildren; 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
接口,添加操作数据结构的新功能,而不用对数据结构本身做任何改动。
不过这个模式也有一些缺点,首先最明显的是它的复杂度。一旦处理的业务逻辑变的复杂,维护起来也是相当有难度的。其次是如果对数据结构本身的类继承体系进行大改动的话,相应的其他改动也是非常大的。
所以访问者模式适用于在数据结构比较稳定的实际应用中。