奇妙的Visitor模式: 从树遍历说起

   最近在项目中反复使用了Visitor模式,觉得它真的很奇妙。

   很多人用Visitor模式可能都和我一样,初衷是为了遍历一棵树,但其实在“树遍历”市场上还有其它方案可以选择,这些方案可能还更简单。那为什么还要用Visitor模式?难道它的方案更优?如果更优,它的优点有更高的意义?

   我的答案是: Visitor是高性能高价格的方案,而且它的优点是“Sepration of Concern”。因为这个优点,它不但可用于树遍历,还可在任何需要多态的场合,提供比“子类直接实现”更低的耦合。

   1.Visitor 可用于树遍历,visitorFather(), visitorSon()什么的,这没什么可说的。

   2.其实不用Visitor也可以实现树遍历,而且还更直观。让Father和Son都实现一个visitTree()方法,Father.visitTree()在visit自己之后再调用所有Son的visitTree()方法,这种方案更符合直觉,而且不需要引入新类,比Visitor模式更简单。我们不妨把这种方案称作
直觉模式

   3.
那为什么说Visitor更优?

       a.
直觉模式的算法是死的,而Visitor模式的算法不是。虽然 Father.visitTree() 跟 Son.visitTree() 的实现可以不一样,但是算法是死的,只能先序遍历,并且不能跳过节点。Visitor类可以任意组合自己的visitXXX()方法和决定这些方法的调用顺序,想先序就先序,想广度优先就广度优选,想跳过节点就跳过节点。

       b.
直觉模式不方便处理与各节点无关的操作,而Visitor模式可以。比如要在访问Father和访问Son之间打印N个空格,用直觉模式就不好做:这个倒底算Father的职责还是Son的职责?用Visitor模式就没有这个问题,因为Visitor本身是肮脏的,你尽可以往里面塞入各种操作。而且Visitor类还可以设定一些成员变量作为上下文,为树遍历提供必要的背景信息。

       c.
最关键的一点,直觉模式只能有一种遍历操作,而Visitor模式可以有多种,并且它们可以互不干涉地同时运行。这是因为Father.visitTree()只能写一个,运行时只能执行这一个方法;而一套Visitor可以有多个Visitor子类,各个子类可以同时实例化,同时运行。

       d.
把这个结论推而广之,可以发现任何多态的场合都可以用Visitor模式。要实现多态,一般是让各子类分别实现指定的抽象方法;但就像前面说的,这种方法只能写一个;
在直觉模式中,父类被多态掉了,而子类却是死的。而在Visitor模式中,由于我们可以在运行时动态选择,以致于子类也多态掉了!

       e.
再深入一步,我们可以发现直觉模式只在实现某种特定契约时使用了多态,契约本身是固定的;而Visitor模式却对契约本身也进行了抽象。比如说在直觉模式中,大家可以有“买菜”这种契约,可以有“去银行”这种契约,契约的种类是具体的,虽然Father.买菜()和Son.买菜()时的还价方式不同;而在Visitor模式中,大家只有一种契约,即“做”,“做”本身是抽象的,直到我们创建“买菜Visitor”,“去银行Visitor”时,这个契约才被具体化。这就是更高层次的“Separation of Concern”,对不对?

    4.
最后,说一下Visitor模式的高代价(如果你还有耐心继续看)

        a.不直观,很古怪的一种思维方式。并且Visitor与Visitable要频繁互调,别人跟你的代码时会很费劲。

        b.要新加一个Visitor接口和N个具体类,并且这个接口要定义对所有“被访问类”的访问方法,这很麻烦

        c.虽然可以引入Context作为Visitor的成员变量,但这个Context只是全局的。如果要在访问Son时拿到Father的相关信息,用Visitor模式就远远不如直觉模式好用。我的做法是为每个visitor方法都引入一个subContext参数,把父结点的信息保存在里面,再调用子结点的visitor方法。这是一种弱类型的方案,使用时要小心

Leave a Comment

Your email address will not be published.

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