现在能找到的Visitor模式的讲解大都非常跳跃,问题列出来出后,讲着讲着就突然给出了生涩的UML类图和accept()、visit()等奇怪的方法名,让人非常费解; 读者即使理解了,也有囫囵吞枣的感觉,不能领会其精妙之处。
本文试图以问题为驱动,以代码重构的方式,展示我们是怎么样一步一步地解决问题,并选择visitor模式作为重构的终点。
#1.问题域
visitor模式用于遍历一群对象,这些对象按某种结构组织在一起(List, Set, Tree等)。这种场景常常面临的问题是:
组织里的对象的类型彼此不同,遍历者要根据不同的类型使用不同的逻辑,导致代码里频繁使用if语句,可读性、可维护性都会比较差
//这个方法逐个打印每个Employee的称谓 private static void printTitle(Team team) { //一个Team里面的Employee有两种类型:Manager, 或Worker for (Employee employee : team.getEmployees()) { if (employee instanceof Manager) { System.out.println("Manager " + employee.getName()); } if (employee instanceof Worker) { System.out.println("Worker " + employee.getName()); } } }
要消除if,最常见的方式就是把各个if里面的逻辑塞入到对象的各个子类中
#2.通过多态解决对象类型不同的问题
public class Manager extends Employee { @Override //让子类实现各自的printTitle()逻辑 public void printTitle() { System.out.println("Manager " + this.getName()); } }
private static void printTitle(Team team) { for (Employee employee : team.getEmployees()) { employee.printTitle(); } } //...
这样就好多了。但又有新的问题:
a. 对Team里所有Employee的遍历除了printTitle(),还有别的一些行为(如“喝饮料”);如果把这些互不相关的行为都塞到Employee类里,那么Employee及其子类就会“虚胖”,内聚性会比较弱
b. 进一步说,这些行为都不应算做Employee的内在职责。如果Employee类要负责“喝饮料”的逻辑,那“吃东西”、“去银行”、“世界和平”岂不是也都要往里塞?
所以我们要从被访问对象中剥离出这些方法,但我们又不能丢失前面的多态带来的好处。解决办法就是:
遍历时仍然调用被访问对象的多态方法,但这个方法:
a.
使用暖味的方法名,如 doAction(),以表示不负责任何具体的行为
b.
方法的实现委托给一个delegate,以表示自己不关心具体的实现
#3. 将被访问对象的服务契约暧昧化
private static void drink(Team team) throws Exception { for (Employee employee : team.getEmployees()) { //使用暖味的doAction()方法名,接收行为的名称作为参数 employee.doAction("drink"); } } private static void printTitle(Team team) throws Exception { for (Employee employee : team.getEmployees()) { //使用暖味的doAction()方法名,接收行为的名称作为参数 employee.doAction("printTitle"); } } public class Manager extends Employee { @Override public void doAction(String action) throws Exception { Method method = ActionDelegate.class.getMethod(action + "Manager", Manager.class); method.invoke(actionDelegate, this); } }
问题解决了吗? 只是部分解决了。访问者仍需要显式地传入行为的名称,如"printTitle"; 被访问对象还需要根据行为的名称找到处理它的delegate类或者delegate类中处理它的方法。
从某种意义上说,if/else仍有残余:“若行为是printTitle,则调用delegate的printTitleXXX()方法".
为了消除这种残余, 我们应该再次引入多态:
a.
构建“行为”对象,如"Action"
b.
把“行为名称”的区别变成子类类型的区别
c.
把行为的逻辑作为多态的方法来实现
#4. 构建多态的“行为”对象
public class Manager extends Employee { @Override //被访问对象只接受虚的行为对象,并不知道传进来的具体是什么行为 public void doAction(Action action) throws Exception { //被访问对象简单地回调行为对象,实现行为的逻辑 action.handleManager(this); } }
private static void drink(Team team) throws Exception { DrinkAction action = new DrinkAction(); for (Employee employee : team.getEmployees()) { //把虚的"行为“对象传给被访问对象 employee.doAction(action); } } private static void printTitle(Team team) throws Exception { PrintTitleAction action = new PrintTitleAction(); for (Employee employee : team.getEmployees()) { employee.doAction(action); } }
问题解决了! 被访问对象不再受行为逻辑的任何污染,只须提供一个暖味的钩子(doAction())而已
说了这么多,那这跟visitor模式又有什么关系呢? 其实上面的解决方案就是visitor模式的解决方案。doAction() 就是visitor术语里的accept(),而handleManager就是visitor术语里的visitManager()
#5. 使用visitor风格的方法命名,完事
private static void drink(Team team) throws Exception { DrinkVisitor visitor = new DrinkVisitor(); for (Employee employee : team.getEmployees()) { employee.accept(visitor); } } private static void printTitle(Team team) throws Exception { PrintTitleVisitor visitor = new PrintTitleVisitor(); for (Employee employee : team.getEmployees()) { employee.accept(visitor); } }