Visitor
访问者模式
访问者就是作用于一个对象结构的组成元素之上的操作。你不需要改变这些组成元素的类结构就可以完成访问者操作的定义。
设想一个编译器,把程序看作是一个抽象的语法树。编译器需要对抽象语法树执行“静态语义学”分析,包括检查所有的变量是否都有定义。编译器还需要生成可执行代码。所以还需要定义以下这些操作:类型检查,代码优化,流程分析,检查变量在使用之前是否已经被赋值,等等。另外,我们还可能在以下时候使用抽象语法树:打印效果美化(英文是pretty-pringing,糊涂熊没有这方面经验,先想当然这样翻吧;回头查清楚了再改也不迟。),程序重组(program restructuring),代码导入(code instrumentation),程序各种度量数据的计算。
以上这些操作都需要将代表赋值语句的节点跟代表变量或算术表达式的节点区别对待。这样,就需要有一个类针对赋值语句,另一个类针对变量访问,算术表达式也要有一个专门的类,等等。当然,这一套节点类依赖于被编译的语言,但是不会因为语言不同而有太大的差别。
上图是节点类继承树的一部分。这里的困难在于如何分发这些操作,因为这些操作横跨不同的节点类, 很容易导致一个难于理解,维护和变更的系统。将类型检查代码和打印美化代码或者流程分析代码混合在一起也极易导致混乱。此外每当需要添加一个新的操作的时候,通常都需要重新编译所有这些类。这时候糊涂熊想:上帝,要是每一个新的操作都可以单独添加进来,而且节点类于应用于其上的操作相对独立的话,那该多好啊!
上帝回答:办法很简单:你可以把作用于不同节点类的相关的操作打包在一个单独的对象里,该对象我们叫它“访问者(visitor)”;然后把这个对像作为参数分别传递给抽象语法树的节点类,相当于这个对象遍历了这棵树。当一个节点元素“接受(accept)”访问者时,它就会发送给访问者一个请求,要求对自己进行处理。访问者本身也有一个代表节点元素的参数。于是访问者就会为该元素执行被请求的操作—在最开始的笨方法中,这个操作的定义是放在节点类里的。.
例如,一个编译器如果不使用访问者模式,可能就要通过电用抽象语法树里的TypeCheck操作来完成type-check动作。每一个子节点都需要实现不同的TypeCheck方法。而如果该编译器采用访问者模式,它就可以创建一个TypeCheckingVisitor 对象,调用抽象语法树里的Accept操作,把自己作为参数传入。每一个子节点将实现回调访问者的Accept方法:不同的子节点类回调不同的访问者方法。例如:赋值节点回调VisitAssignment方法;变量引用节点回调VisitVariableReference方法。过去放在AssignmentNode类里的TypeCheck方法的定义现在放在TypeCheckingVisitor类的VisitAssignment里面。
如果需要访问者支持多种操作,而不仅是type-checking一种, 我们可以使用一个抽象父类NodeVisitor, 从该类派生出抽象语法树的所有访问者。NodeVisitor 类必须为每一个子节点类声明一个操作。需要计算程序度量数据的应用程式定义NodeVisitor的新的子类, 而且不再需要在节点类中添加特定于应用程式的代码。访问者模式把每一个不同编译阶段的操作封装到了跟该阶段有关的一个访问者类里面。
使用访问者模式,你至少需要定义两棵继承树:一棵树用于被访问和操作的元素(节点树),另一棵树用于这些操作的访问者(节点访问者树)。创建新的操作,等同于在节点访问者树中添加新的子类。只要编译器接受的静态语法规则(糊涂熊对grammar的理解和翻译,不知道是否准确?)不变(也就是说,我们不需要在节点树中添加新的子类),我们再增加新的功能的时候,需要做的一切就是在节点访问者树中添加新的子类这样的简单工作。
在下列情况下考虑使用或者不使用访问者模式:
使用:
1。一个对像结构内部包含很多以不同接口表示的对象,你想对这些对象执行依赖于具像类的操作。
2。需要在对象结构内部针对不同类型的对象执行区别性的无关的操作,而且你想避免”污染”被操作的类。访问者使你把相关的操作集中到一个类里。如果对象结构被很多应用程式(糊涂熊的注解:program和application的中文翻译中都有程序二字,糊涂熊认为容易混淆视听;多年来一直觉得非常别扭;所以糊涂熊在此正式主张并呼吁:program翻译成程序,appliation翻译成应用程式。前者从developer的角度看问题,有一行一行,一句一句的意境;后者从user或analyzer的角度看问题, 有整体的满足功能的意境;概念空间和侧重点应该有所不同。)共享,请在需要的应用程式中使用访问者。
不使用:
3。定义对象结构的类很少变更(糊涂熊补充:静态意义上),但是你经常想定义作用于该结构之上的新的操作。变更对象结构类需要重定义对所有访问者的接口,所以有潜在的成本。如果对象结构类经常变更,那么与其采用访问者模式,还不如直接将操作定义到这些对象类内部呢。