设计模式 | 访问者模式(二十七)_java《侠客行》是当代作家金庸创作的长篇武侠小说,新版电视剧《侠客行》中,开篇有一段独白: 茫茫海外,传说有座侠客岛,岛上赏善罚恶二使,每隔十年必到中原武林,向各大门派下发放赏善罚恶令,强邀掌门人赴岛喝腊八粥,拒接令者,皆造屠戮,无一幸免,接令而去者,杳无音讯,生死未仆,侠客岛之行,已被视为死亡之旅。”
不过话说电视剧,我总是觉得老版的好看。
 1 

意图

表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。
 2 

意图解析

我们以代码描述一下《侠客行》中的这个场景假定赏善罚恶二使,一个叫做张三,一个叫做李四,面对一众掌门:张三负责赏善,对好人赏赐,坏人他不处理;相反,李四负责罚恶,好人不处理,对坏人惩罚;

侠客行代码示例

定义了“掌门人”接口

package visitor.侠客行;public interface 掌门人 {
}

“掌门人”有两种类型没做过坏事的掌门做过坏事的掌门

package visitor.侠客行;public class 没做过坏事的掌门 implements 掌门人 {
}
package visitor.侠客行;public class 做过坏事的掌门 implements 掌门人 {
}

定义了侠客岛,侠客岛管理维护“江湖的掌门人”,使用List;提供了掌门人的添加方法  “add掌门人(掌门人 某掌门)”;定义了“赏善罚恶(String 处理人)”方法,用于赏善罚恶,接受参数为处理人; 如果是赏善大使张三,他会赏赐好人,不管坏人;如果是罚恶大使李四,他会惩罚坏人,不管好人;

package visitor.侠客行;
import java.util.ArrayList;import java.util.List;
public class 侠客岛 {
 private List<掌门人> 掌门人List = new ArrayList<>();
 public void add掌门人(掌门人 某掌门) {    掌门人List.add(某掌门);  }
 public void 赏善罚恶(String 处理人) {
   if (处理人.equals("张三")) {
     for (掌门人 某掌门X : 掌门人List) {
       if (某掌门X instanceof 没做过坏事的掌门) {
         System.out.println("好掌门, 张三: 赏赐没做过坏事的掌门");
       } else if (某掌门X instanceof 做过坏事的掌门) {
         System.out.println("坏掌门, 张三: 不管做过坏事的掌门");        }
       System.out.println();      }    } else if (处理人.equals("李四")) {
     for (掌门人 某掌门X : 掌门人List) {
       if (某掌门X instanceof 没做过坏事的掌门) {
         System.out.println("好掌门, 李四: 不管没做过坏事的掌门");
       } else if (某掌门X instanceof 做过坏事的掌门) {
         System.out.println("坏掌门, 李四: 惩罚做过坏事的掌门");        }        System.out.println();    }  }  }}
测试代码上面的测试代码中,我们创造了侠客岛的“赏善罚恶二使”,并且将几个“掌门人”交于他们处理。打印结果分别展示了对于这几个“掌门人”,张三和李四的不同来访,产生的不同结果。 如果我们想增加来访者怎么办?比如这次是龙木岛主亲自出岛处理,好人赏赐,坏人直接处理怎么办?
我们可以直接新增赏善罚恶方法的处理逻辑如下图所示,新增加了一个else if,可以通过测试代码看到结果。 如果有些掌门人既没有做什么好事,也没有做什么坏事怎么处理?也就是新增一种掌门人?你会发现,所有的判断的地方,也还是都需要新增加一个else if ...... ̄□ ̄||
因为上面的示例,使用的是两层判断逻辑,每一层都跟具体的类型有关系!!!
不管是增加新的来访者,还是增加新的种类的成员,都不符合开闭原则,而且判断逻辑复杂混乱。 上面的过程在程序世界中, 也会经常出现。
实际开发中,经常用到集合框架,集合框架中也经常会保存不同的类型(此处指的是不同的最终类型,如果抬杠,还不都是Object    ̄□ ̄||)
比如多个不同的子类,像上面示例中的好掌门和坏掌门,都是掌门人类型,但是具体子类型不同。
对于集合中的元素,可能会有不同的处理操作。
比如上面示例中的,张三和李四的到来,处理肯定不一样,没干过坏事的和干过坏事的处理也不一样。
比如去体检,不同的项目的医生会有不同的行为操作,你和跟你一起排队体检的人也不一样,但是你还是你,他还是他。 在上面的《侠客行》的示例中,我们使用了双重判断来确定下面两层问题:一层是来访者是谁?另外一层是当前的掌门人是什么类型?
如果有X种来访者,Y种类型掌门人,怕是要搞出来X*Y种组合了,所以才会逻辑复杂,扩展性差。所以,那么根本问题就是灵活的确定这两个维度,来访者和当前类型,进而确定具体的行为,对吧? 再回头审视一下《侠客行》的示例,对于访问者,有张三、李四、龙木岛主,还可能会有其他人。显然,我们应该尝试将访问者进行抽象,张三,李四,龙木岛主,他们都是具体的访问者。而且,而且,而且,他们都会访问不同类型的掌门人,既然是访问  不同类型掌门人也就是方法名一样,类型不一样?这不就是方法重载么?

新版代码示例

掌门人相关角色不变

package visitor.新版侠客行;public interface 掌门人 {}package visitor.新版侠客行;public class 没做过坏事的掌门 implements 掌门人 {}package visitor.新版侠客行;public class 做过坏事的掌门 implements 掌门人 {}

新增加访问者角色,访问者既可能访问好人,也可能访问坏人,使用方法的重载解决 。方法都是拜访,有两种类型的重载版本:

package visitor.新版侠客行;public interface 访问使者 {  void 拜访(做过坏事的掌门 坏人);  void 拜访(没做过坏事的掌门 好人);}
张三负责赏善,当他访问到好人时,赏赐,坏人不处理;
package visitor.新版侠客行;
public class 张三 implements 访问使者 {    @Override    public void 拜访(没做过坏事的掌门 好人) {        System.out.println("好掌门, 张三: 赏赐没做过坏事的掌门");    }
   @Override    public void 拜访(做过坏事的掌门 坏人) {        System.out.println("坏掌门, 张三: 不管做过坏事的掌门");    }}

李四负责罚恶,访问到好人时不处理,遇到坏人时,就惩罚!

package visitor.新版侠客行;
public class 李四 implements 访问使者 {
   @Override    public void 拜访(没做过坏事的掌门 好人) {        System.out.println("好掌门, 李四: 不管没做过坏事的掌门");    }
   @Override    public void 拜访(做过坏事的掌门 坏人) {        System.out.println("坏掌门, 李四: 惩罚做过坏事的掌门");    }}
引入了访问使者角色,我们就不需要对使者进行判断了借助了使者的多态性,不管是何种使者都有访问不同类型掌门人的方法所以可以去掉了一层逻辑判断,代码简化如下
package visitor.新版侠客行;
import java.util.ArrayList;import java.util.List;
public class 侠客岛 {  private List<掌门人> 掌门人List = new ArrayList<>();
 public void add掌门人(掌门人 某掌门) {    掌门人List.add(某掌门);  }
 public void 赏善罚恶(访问使者 使者) {      for (掌门人 某掌门X : 掌门人List) {         if (某掌门X instanceof 没做过坏事的掌门) {             使者.拜访((没做过坏事的掌门)某掌门X);         } else if (某掌门X instanceof 做过坏事的掌门) {             使者.拜访((做过坏事的掌门)某掌门X);         }         System.out.println();      }  }}
测试代码也稍作调整定义了两个访问者,传递给“赏善罚恶”方法
package visitor.新版侠客行;
public class Test {
public static void main(String[] args){
   侠客岛 善善罚恶二使 = new 侠客岛();
   善善罚恶二使.add掌门人(new 做过坏事的掌门());    善善罚恶二使.add掌门人(new 没做过坏事的掌门());    善善罚恶二使.add掌门人(new 没做过坏事的掌门());    善善罚恶二使.add掌门人(new 做过坏事的掌门());
   访问使者 张三 = new 张三();    访问使者 李四 = new 李四();
   善善罚恶二使.赏善罚恶(李四);    善善罚恶二使.赏善罚恶(张三);    }}

可以看到,《新版侠客行》和老版本的功能的一样的,但是代码简化了。而且,最重要的是能够很方便的扩展使者,比如我们仍旧增加“龙木岛主”这一访客。

package visitor.新版侠客行;public class 龙木岛主 implements 访问使者 {    @Override    public void 拜访(做过坏事的掌门 坏人) {        System.out.println("龙木岛主,惩罚坏人");    }    @Override    public void 拜访(没做过坏事的掌门 好人) {        System.out.println("龙木岛主,赏赐好人");    }}
新增加了"龙木岛主“访客后,客户端可以直接使用了,不需要修改”侠客岛“的代码了测试代码增加如下两行,查看下面结果。但是如果增加新的掌门人类型呢?因为我们仍旧有具体类型的判断,如下图所示所以,想要增加新的掌门人,又完蛋了   ̄□ ̄|| 而且,现在的判断逻辑也还是交织着,复杂的。
对于访问者的判断,我们借助于多态以及方法的重载,去掉了一层访问者的判断,通过多态可以将请求路由到真实的来访者,通过方法重载,可以调用到正确的方法。 如果能把这一层的if else if判断也去掉,是不是就可以灵活扩展掌门人了呢?
使者只知道某掌门X,但是他最终的具体类型,是不知道的,所以,没办法直接调用拜访方法的,因为我们的确没有这种参数类型的方法 。
ps:有人觉得“拜访”方法的类型使用 掌门人  不就好了么但是对于不同的具体类型有不同的行为,那你在“拜访”方法中还是少不了要进行判断,只是此处判断还是“拜访”方法内判断的问题 前面的那段if else if判断逻辑,访问的方法都是  使者.拜访,只不过具体类型不同但是如何确定类型?问题也就转换为”到底怎么判断某掌门X的类型“或者”到底谁知道某掌门X的类型“那谁知道他的类型呢?如果不借助外力,比如 instanceof 判断的话,还有谁知道?某掌门X 他自己知道!!!他自己知道!!!所以,如果是在  某掌门X自己内部的方法,就可以获取到this了,这就是当前对象的真实类型,把这个类型在回传给来访使者不就可以了么?
所以给掌门人定义一个“接受拜访”方法,不管何种类型的掌门人,都能够接受各种访客的拜访。接受拜访(访问使者 赏善罚恶使者){赏善罚恶使者.拜访(this); 

最新版侠客行代码示例

说起来有点迷惑,我看看代码《最新版侠客行》掌门人都增加了”接受拜访“的方法

package visitor.最新版本侠客行;public interface 掌门人 {void 接受拜访(访问使者 赏善使者);}

package visitor.最新版本侠客行;public class 没做过坏事的掌门 implements 掌门人 {  @Override  public void 接受拜访(访问使者 赏善罚恶使者) {    赏善罚恶使者.拜访(this);  }}
package visitor.最新版本侠客行;public class 做过坏事的掌门 implements 掌门人 {  @Override  public void 接受拜访(访问使者 赏善罚恶使者) {    赏善罚恶使者.拜访(this);  }
}

访问使者相关角色与《新版侠客行》中一样

package visitor.最新版本侠客行;public interface 访问使者 {    void 拜访(做过坏事的掌门 坏人);    void 拜访(没做过坏事的掌门 好人);}

package visitor.最新版本侠客行;public class 张三 implements 访问使者 {    @Override    public void 拜访(没做过坏事的掌门 好人) {        System.out.println("好掌门, 张三: 赏赐没做过坏事的掌门");    }    @Override    public void 拜访(做过坏事的掌门 坏人) {        System.out.println("坏掌门, 张三: 不管做过坏事的掌门");    }}
package visitor.最新版本侠客行;public class 李四 implements 访问使者 {    @Override    public void 拜访(没做过坏事的掌门 好人) {        System.out.println("好掌门, 李四: 不管没做过坏事的掌门");    }    @Override    public void 拜访(做过坏事的掌门 坏人) {        System.out.println("坏掌门, 李四: 惩罚做过坏事的掌门");    }}

此时的侠客岛轻松了,不再需要来回的判断类型了

package visitor.最新版本侠客行; import java.util.ArrayList;import java.util.List;public class 侠客岛 {    private List<掌门人> 掌门人List = new ArrayList<>();    public void add掌门人(掌门人 某掌门) {        掌门人List.add(某掌门);    }    public void 赏善罚恶(访问使者 使者) {        for (掌门人 某掌门X : 掌门人List) {            某掌门X.接受拜访(使者);            System.out.println();        }    }}

从结果看跟上一个版本一样,但是很显然,我们的侠客岛轻松了 接下来我们看一下新增加访客和新增加掌门人的场景。扩展龙木岛主


package visitor.最新版本侠客行;
public class 龙木岛主 implements 访问使者 {@Overridepublic void 拜访(做过坏事的掌门 坏人) {    System.out.println("龙木岛主,惩罚坏人");  }
@Overridepublic void 拜访(没做过坏事的掌门 好人) {    System.out.println("龙木岛主,赏赐好人");  }}
测试代码如下,显然因为拜访使者的抽象,才得以能够更好的扩展访问者,所以此处肯定跟《新版侠客行》一样便于扩展 看看如果扩展一个新的掌门人
package visitor.最新版本侠客行;public class 不好不坏的掌门 implements 掌门人 {@Overridepublic void 接受拜访(访问使者 赏善罚恶使者) {    赏善罚恶使者.拜访(this);  }}
但是,”访问使者“里面没有能够拜访”不好不坏的掌门“方法啊?怎么办?只能添加呗,如下图所示,完蛋了........

代码演化小结

看得出来,《最新版侠客行》解决了复杂判断的问题,也解决了访问者扩展的问题但是对于被访问者的类型的扩展,显然是没有扩展性的,不符合开闭原则。这一点体现出来了这种解决方法的倾向性,倾向于扩展行为,可以自如的增加新的行为,但是不能轻松的增加元素类型。 测试代码Test类不需要修改看一下打印结果

最新版侠客行结构


 3 

回首意图

再回头看下访问者模式的意图:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。就是上面示例中,对于来访者的扩展嘛 最初的动机就是处理《侠客行》中类似的问题集合容器中保存了不同类型的对象,他们又可能有多种不同场景的操作。比如一份名单,班长可能拿过去收作业,班主任拿过去可能点名名单里面都有你也有他,你就是那个你,他还是那个他,但是你的作业是你的作业,他的作业是他的作业。所以对于班长和班主任两个访问者,同学们的行为是不一样的,对同一来访者,不同的同学的行为又是不一样的。
 4 

结构

 抽象元素角色Element抽象元素一般是抽象类或者接口,通常它定义一个 accept(抽象访问者) 方法,用来将自身传递给访问者。
具体的元素角色ConcreateElement具体元素实现了accept方法,在accept方法中调用访问者的访问方法以便完成对一个元素的操作。
抽象访问者Visitor定义一个或者多个访问操作,抽象访问者需要面向具体的被访问者元素类型,所以有几个具体的元素类型需要被访问,就有几个重载方法。
具体的访问者ConcreateVisitor具体的访问者封装了不同访问者,不同类型对象的具体行为,也就是最终的分情况的处理逻辑。
对象结构ObjectStructure对象结构是元素的集合,用于存放元素的对象,并且一般提供遍历内部元素的方法。
客户端角色Client组织被访问者,然后通过访问者访问。 访问者模式有两个主要层次,访问者以及被访问元素访问者有不同的类型,被访问元素有不同的类型
每一种访问者对于每一种被访问元素都有一种不同的行为,这不同的行为是封装在访问者的方法中
所以访问者需要进行访问方法visit的重载,被访问元素有几种类型,就有几种重载版本。
面向细节的逻辑既然被封装在访问者中,被访问元素就不需要面向细节了,只需要把自己的类型传递给访问者即可。所以,所有的被访问元素都只有一个版本的accept方法。

 5 

概念示例代码

我们可以抽象化的看下下面的例子下面的代码很简单,A有三种子类型,B有三种子类型不同的A和不同的B,将会擦出不一样的火花,也就是会出现9种可能的场景将A定义为访问者,那么A就要借助方法的重载实现不同类型被访问者B的不同行为而将方法的调用转变为被访问者的反向调用----this传递给访问者

package visitor;
public class example {
 public static void main(String[] args) {
   A1 a1 = new A1();    A2 a2 = new A2();    A3 a3 = new A3();
   B1 b1 = new B1();    B2 b2 = new B2();    B3 b3 = new B3();
   b1.accept(a1);    b1.accept(a2);    b1.accept(a3);    b2.accept(a1);    b2.accept(a2);    b2.accept(a3);    b3.accept(a1);    b3.accept(a2);    b3.accept(a3);  }}

abstract class A {
 abstract void visit(B1 b1);
 abstract void visit(B2 b2);
 abstract void visit(B3 b3);}
class A1 extends A {
 @Override  void visit(B1 b1) {    System.out.println("A1 play with B1");  }
 @Override  void visit(B2 b2) {    System.out.println("A1 play with B2");  }
 @Override  void visit(B3 b3) {    System.out.println("A1 play with B3");  }}
class A2 extends A {
 @Override  void visit(B1 b1) {    System.out.println("A2 play with B1");  }
 @Override  void visit(B2 b2) {    System.out.println("A2 play with B2");  }
 @Override  void visit(B3 b3) {    System.out.println("A2 play with B3");  }}
class A3 extends A {
 @Override  void visit(B1 b1) {    System.out.println("A3 play with B1");  }
 @Override  void visit(B2 b2) {    System.out.println("A3 play with B2");  }
 @Override  void visit(B3 b3) {    System.out.println("A3 play with B3");  }}

abstract class B {
 abstract void accept(A a);}
class B1 extends B {
 @Override  void accept(A a) {    a.visit(this);  }}
class B2 extends B {
 @Override  void accept(A a) {    a.visit(this);  }}
class B3 extends B {
 @Override  void accept(A a) {    a.visit(this);  }}

这种重载和回传自身的形式,完全可以当作一个套路来使用,对于这种组合形式的场景,非常受用。
访问者的自身借助多态特性,又依赖方法重载,然后再借助于this回传达到反向确定类型调用,真心精巧。
 6 

总结

访问者模式灵活的处理了不同类型的元素,面对不同的访问者,有不同的行为的场景。
这种组合场景,判断逻辑复杂繁琐,访问者模式可以做到灵活的扩展增加更多的行为,而不需要改变原来的类。
访问者模式倾向于扩展元素的行为,当扩展元素行为时,满足开闭原则。但是对于扩展新的元素类型时,将会产生巨大的改动,每一个访问者都需要变动,所以在使用访问者模式是要考虑清楚元素类型的变化可能。因为访问者依赖的是具体的元素,而不是抽象元素,所以才难以扩展。 访问者依赖的是具体元素,而不是抽象元素,这破坏了依赖倒置原则,特别是在面向对象的编程中,抛弃了对接口的依赖,而直接依赖实现类,扩展比较难。 当业务规则需要遍历多个不同的对象时,而且不同的对象在不同的场景下又有不同的行为,你就应该考虑使用访问者模式。
如果对象结构中的对象不常变化,但是他们的行为却经常变化时,也可以考虑使用,访问者模式可以很灵活的扩展新的访客。

https://mp.weixin.qq.com/s/Lk3P3T1U9N37Bnx7EbrgWg