设计模式-访问者模式
访问者(Visitor)模式是对象的行为模式。访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改的话,接受这个操作的数据则可以保持不变。 (学习)
分派的概念
变量被声明时的类型叫做变量的静态类型(Static Type),有些人又把静态类型叫做明显类型(Apparent Type);而变量所引用的对象的真实类型又叫做变量的实际类型(Actual Type)。比如:
var list: List<String>? = null list = ArrayList()
声明了一个变量list,它的静态类型(也中明显类型)是List,而它的实际类型是ArrayList。
根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派。
静态分派(Static Dispatch)发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。
动态分派发生在运行时期,动态分派动态地置换掉某个方法。
静态分派
java通过方法重载支持静态分派。用墨子骑马的故事作为例子,墨子可以骑白马或者黑马。墨子与白马、黑马和马的类图如下所示:
在这个系统中,墨子由Mozi类代表
package Visitor.example1 class Mozi { fun ride(horse: Horse){ println("horse riding") } fun ride(blackHorse: BlackHorse){ println("black horse riding") } fun ride(whiteHorse: WhiteHorse){ println("white horse riding") } }
package Visitor.example1 open class Horse { open fun eat() { println("eating glass") } }
package Visitor.example1 class BlackHorse : Horse() { override fun eat() { println("Black Horse eating glass") } }
package Visitor.example1 class WhiteHorse : Horse() { override fun eat() { println("White Horse eating glass") } }
val whiteHorse:Horse = WhiteHorse() val blackHorse:Horse = BlackHorse() val mozi = Mozi() mozi.ride(whiteHorse) mozi.ride(blackHorse)
显然,Mozi类的ride()方法是由三个方法重载而成的。这个方法分别接受马、白马、黑马等类型的参数。
那么在运行时,程序会打印出什么结果呢?结果是程序会打印出相同的两行“horse riding”。换言之,墨子发现他所骑的都是马。
为什么呢?两次对ride()方法的调用传入的是不同的参数,也就是whiteHouse和blackHouse。它们虽然具有不同的真实类型,但是它的静态类型都是一样的,均是Horse类型。
重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。
动态分派
java通过方法的重写支持动态分派。用马吃草的故事作为例子,代码如下:
package Visitor.example1 open class Horse { open fun eat() { println("Horse eating glass") } }
package Visitor.example1 class BlackHorse : Horse() { override fun eat() { println("Black Horse eating glass") } }
val horse: Horse = BlackHorse()
horse.eat()
变量horse的静态类型是Horse,而真实类型是BlackHorse。如果上面最后一行的eat()方法调用的是BlackHorse类的eat()方法,那么上面打印的就是"Black horse eating glass";相反,如果上面的eat()方法调用的是Horse类的eat()方法,那么打印的就是"Horse eating glass"。
所以,问题的核心就是编译在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。这样一来,上面最后一行的eat()方法调用的是BlackHorse类的eat()方法,打印的是"Black Horse eating glass"。
分派的类型
一个方法所属的对象叫做方法的接收者,方法的接收者与方法的参数统称做方法的宗量。比如下面例子中的Test类
public class Test { public void print(String str){ System.out.println(str); } }
在上面的类中, print()方法属于Test对象,所以它的接收者也就是Test对象了。print()方法有一个参数是str,它的类型是String。
根据分派可以基于多少种宗量,可以将面向对象的语言划分为单分派语言(Uni-Dispatch)和多分派语言(Mulit-Dispatch)。单分派语言根据一个宗量的类型进行对方汉的选择,多分派语言根据多于一个的宗量的类型对方法进行选择。
c++和java均是单分派语言,多分派语言的例子包括CLOS和Cecil。按照这样的区分,java就是动态的单分派语言,因为这种语言的动态分派仅仅会考虑到方汉的接收者的类型,同时又是静态的多分派语言,因为这种语言对重载方法的分派会考试到方法的接收者的类型以及方法的所有参数的类型。
在一个支持动态单分派的语言里面,有两个条件决定了一个请求会调用哪一个操作:一是请求的名字,二是接收者的真实类型。单分派限制了方法的选择过程,使得只有一个宗量可以被考虑到,这个宗量通常就是方法的接收者。在java语言里面,如果一个操作是作用于某个类型不明的对象上面,那么对这个对象的真实类型测试仅会发生一次,这就是动态的单分派的特征。
访问者模式的结构
访问者模式适用于数据结构相对未定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由地演化。访问者模式的简略图如下所示:
数据结构的每一个节点都可以接受一个访问者的调用,此节点向访问者对象传入节点对象,而访问者对象则反过来执行节点对象的操作。这样的过程叫做“双重分派”。节点调用访问者,将它自已传入,访问者则将某算法针对此节点执行。访问者模式的示意性类图如下所示:
访问者模式涉及到的角色如下:
- 抽象访问者(Vistor)角色:
声明了一个或者多个方法操作,形成所有的具体访问者必顺实现的接口。
- 具体访问者(ConcreteVistor)角色:
实现抽象访问者所声明的接口,也就是抽象访问者所声明的各个访问操作。
- 抽象节点(Node)角色:
声明一个接受操作,接受一个访问者对象作为一个参数。
- 具体节点(ConcreteNode)角色:
实现了抽象节点所规定的接受操作。
- 结构对象(ObjectStructure)角色:
有如下的责任,可以遍历结构中的所有元素;如果需要,提供一个高层次的接口让访问者对象可以访问每一个元素;如果需要,可以设计成一个复合对象或者一个聚集,如List或Set。
源代码
可以看到,抽象访问者角色为每一个具体节点都准备了一个访问操作。由于有两个节点,因此,对应就有两个访问操作。
package Visitor.example2 interface Visitor { /** * 对于NodeA的访问操作 * */ fun visit(nodeA: NodeA) /** * 对于NodeB的访问操作 * */ fun visit(nodeB: NodeB) }
package Visitor.example2 /** * 具体访问者VisitorA类 * */ class VisitorA : Visitor { /** * 对于nodeA的访问操作 * */ override fun visit(nodeA: NodeA) { println(nodeA.operationA()) } /** * 对于nodeB的访问操作 * */ override fun visit(nodeB: NodeB) { println(nodeB.operationB()) } }
package Visitor.example2 /** * 具体访问者VisitorB类 * */ class VisitorB : Visitor { /** * 对于nodeA的访问操作 * */ override fun visit(nodeA: NodeA) { println(nodeA.operationA()) } /** * 对于nodeB的访问操作 * */ override fun visit(nodeB: NodeB) { println(nodeB.operationB()) } }
package Visitor.example2 /** * 抽象节点类 * */ abstract class Node { /** * 接受操作 * */ abstract fun accept(visitor: Visitor) }
package Visitor.example2 /** * 具体节点类NodeA * */ class NodeA : Node(){ override fun accept(visitor: Visitor) { visitor.visit(this) } /** * 节点A特有的方法 * */ fun operationA():String{ return "NodeA" } }
package Visitor.example2 /** * 具体节点类NodeB * */ class NodeB : Node(){ override fun accept(visitor: Visitor) { visitor.visit(this) } /** * 节点B特有的方法 * */ fun operationB():String{ return "NodeB" } }
package Visitor.example2 /** * 具体对象角色类,这个结构对象角色持有一个聚集类,并向外界提供add()方法作为对聚集的管理操作。 * 通过调用这个方法,可以动态地增加一个新的节点。 * */ class ObjectStructure { val nodes = ArrayList<Node>() /** * 执行方法的操作 * */ fun action(visitor: Visitor) { for (node in nodes) { node.accept(visitor) } } /** * 添加一个新元素 * */ fun add(node: Node) { nodes.add(node) } }
代码测试
//创建一个结构对象 val objectStructure = ObjectStructure() //给结构增加一个节点 objectStructure.add(NodeA()) //给结构增加一个节点 objectStructure.add(NodeB()) //创建一个访问者 val visitor:Visitor = VisitorA() objectStructure.action(visitor)
运行结果
NodeA
NodeB
虽然在这个示意性里并没有出现一个复杂的具有多个树枝节点的对象树结构,但是,在实际系统中访问者模式通常是用来处理复杂的对象树结构的,而且访问者模式可以用来处理跨越多个等级结构的树结构问题。这正是访问者模式的功能强大之处。
访问者模式的优点
- 好的扩展性
能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
- 好的复用性
可以通过访问者来定义整个对象结构通用的功能,从而提高复用程度。
- 分离无关行为
可以通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。
访问者模式的缺点
- 对象结构变化很困难
不适用于对象结构中的类经常变化的情况,因为对象结构发生了改变,访问者的接口和访问者的实现都要发生相应的改变,代价太高。
- 破坏封装
访问者模式通常需要对象结构开放内部数据给访问者和ObjectStructure,这破坏了对象的封装性。