一步一步地理解Visitor模式

现在能找到的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());
            }
        }
    }

   
代码详见
https://github.com/chenjianjx/learn-visitor-pattern/blob/master/1st-PlainSolution/src/learn/visitor/research/client/EmployeeClient.java

  要消除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();
        }
    }
    //...

   
代码详见
https://github.com/chenjianjx/learn-visitor-pattern/blob/master/2nd-AddPolyform/src/learn/visitor/research/client/EmployeeClient.java

   这样就好多了。但又有新的问题:

    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);
    }
  }

   
代码详见
https://github.com/chenjianjx/learn-visitor-pattern/blob/master/3rd-SingleActionMethod/src/learn/visitor/research/client/EmployeeClient.java

    问题解决了吗? 只是部分解决了。访问者仍需要显式地传入行为的名称,如"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);
        }
    }



   
代码详见
https://github.com/chenjianjx/learn-visitor-pattern/blob/master/4th-ActionPolyform/src/learn/visitor/research/client/EmployeeClient.java

问题解决了! 被访问对象不再受行为逻辑的任何污染,只须提供一个暖味的钩子(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);
        }
    }

   
代码详见
https://github.com/chenjianjx/learn-visitor-pattern/blob/master/5th-VisitorStyle/src/learn/visitor/research/client/EmployeeClient.java

Leave a Comment

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.