C#面向对象设计模式纵横谈:Visitor 访问者模式
类层次结构的变化
类层次结构中可能经常由于引入新的操作,从而将类型变得脆弱……
动机(Motivation)
在软件构建过程中,由于需求的改变,某些类层次结构中常常需要增加新的行为(方法),如果直接在基类中做这样的更改,将会给子类带来很繁重的变更负担,甚至破坏原有设计。如何在不更改类层次结构的前提下,在运行时根据需要透明地为类层次结构上的各个类动态添加新的操作,从而避免上述问题?
意图(Intent)
表示一个作用于某对象结构中的各个元素的操作。它可以在不改变各元素的类的前提下定义作用于这些元素的新的操作。
——《设计模式》GoF
例说Visitor模式应用
假设现在Shape需要增加一个新的MoveTo操作,它所有的子类都需要添加一个MoveTo的Override方法。
改进的代码
虽然子类的重载方法都是一样的代码,但是不能放在基类中写成virtual方法。因为首先,Visit方法接收的类型不是多态类型,而是具体类型。传入的this是一种编译时绑定,它是一种静态多态。如果是在基类中写Accept虚方法,里面写上v.Visit(this),编译的时候会报错,因为Visit方法里接收的类型没有Shape类型,因此我们只能写在子类中重写。
现在,给各个子类增加操作,实际上就是给ShapeVisitor增加一个新的子类。
当然,操作可能要传入参数,我们可以为Accept方法添加一个上下文信息
客户程序
我们首先要new一个ShapeVisitor实例MyVisitor,shape.Accept方法先会执行多态辨析,决定执行Line的Accept方法,然后会执行v.Visit方法,然后找到MyVisitor的Visit方法来执行。整个过程有个双重辨析,先调用了Accept,此步骤有一个虚函数的动态辨析;然后调用了Visit,此步骤也是一个虚函数的动态辨析。
这里有一个问题,我们可以不可以把Visit的参数直接写成抽象类Shape呢?
实际上我们也可以这样写,这样写了之后,我们只需要在Shape基类中写上Accept虚方法调用v.Visit(this),即可不需再在子类中重写Accept方法了。但是这样写的问题是,我们在写ShapeVisitor具体类的Visit方法时,就需要判断Shape的类型,来做不同的处理。
这样实际上是换汤不换药,因为本来以前我们的辨析是重载辨析,靠编译器来解决,现在需要靠我们自己写if else来解决辨析。这两种方法都同样要面临MyVisitor子类增加的问题。Shape如果增加子类,这种方式也需要增加一个if else分支。
结构(Structure)
当需要添加新的操作的时候,只需要添加一个ConcreteVisitor类即可。这种结构把扩展操作所带来的改变转嫁给了Visitor的子类。
Visitor模式的几个要点
Visitor模式通过所谓双重分发(double dispatch)来实现在不更改Element类层次结构的前提下,在运行时透明地为类层次结构上的各个类动态添加新的操作。所谓双重分发即Visitor模式中间包括了两个多态分发(注意其中的多态机制):第一个为accept方法的多态辨析;第二个为visit方法的多态辨析。
Visitor模式的最大缺点在于扩展类层次结构(增添新的Element子类),会导致Visitor类的改变。因此Visitor模式适用于“Element类层次结构稳定,而其中的操作却经常面临频繁改动”。
设计模式其实是一种堵漏洞的方式,但是没有一种设计模式能够堵完所有的漏洞,即使是组合各种设计模式也是一样。每个设计模式都有漏洞,都有它们解决不了的情况或者变化。每一种设计模式都假定了某种变化,也假定了某种不变化。Visitor模式假定的就是操作变化,而Element类层次结构稳定。