1.18 访问者模式
访问者模式(Visitor Pattern) 是一种虽然知名但却并不经常用到的模式。我认为这很奇怪,因为它实在是一种非常出色的模式。
该模式的目标就是将算法与对象结构中分离出来。通过这种分离,可以为已有的对象结构添加新的操作,而不更改这些结构。
1.18.1 简单范例
该范例处理的是如何计算图形(或图形集合)边界的问题。我们首先尝试利用传统的访问者模式来实现,然后再用更为 Groovy 风格的方式来实现。
abstract class Shape { }
class Rectangle extends Shape {
def x, y, width, height
Rectangle(x, y, width, height) {
this.x = x; this.y = y; this.width = width; this.height = height
}
def union(rect) {
if (!rect) return this
def minx = [rect.x, x].min()
def maxx = [rect.x + width, x + width].max()
def miny = [rect.y, y].min()
def maxy = [rect.y + height, y + height].max()
new Rectangle(minx, miny, maxx - minx, maxy - miny)
}
def accept(visitor) {
visitor.visit_rectangle(this)
}
}
class Line extends Shape {
def x1, y1, x2, y2
Line(x1, y1, x2, y2) {
this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2
}
def accept(visitor){
visitor.visit_line(this)
}
}
class Group extends Shape {
def shapes = []
def add(shape) { shapes += shape }
def remove(shape) { shapes -= shape }
def accept(visitor) {
visitor.visit_group(this)
}
}
class BoundingRectangleVisitor {
def bounds
def visit_rectangle(rectangle) {
if (bounds)
bounds = bounds.union(rectangle)
else
bounds = rectangle
}
def visit_line(line) {
def line_bounds = new Rectangle(line.x1, line.y1, line.x2-line.y1, line.x2-line.y2)
if (bounds)
bounds = bounds.union(line_bounds)
else
bounds = line_bounds
}
def visit_group(group) {
group.shapes.each { shape -> shape.accept(this) }
}
}
def group = new Group()
group.add(new Rectangle(100, 40, 10, 5))
group.add(new Rectangle(100, 70, 10, 5))
group.add(new Line(90, 30, 60, 5))
def visitor = new BoundingRectangleVisitor()
group.accept(visitor)
bounding_box = visitor.bounds
println bounding_box.dump()
呃,代码的确有点多。
下面来简化一下,使用 Groovy 的闭包让代码量减半:
abstract class Shape {
def accept(Closure yield) { yield(this) }
}
class Rectangle extends Shape {
def x, y, w, h
def bounds() { this }
def union(rect) {
if (!rect) return this
def minx = [ rect.x, x ].min()
def maxx = [ rect.x + w, x + w ].max()
def miny = [ rect.y, y ].min()
def maxy = [ rect.y + h, y + h ].max()
new Rectangle(x:minx, y:miny, w:maxx - minx, h:maxy - miny)
}
}
class Line extends Shape {
def x1, y1, x2, y2
def bounds() {
new Rectangle(x:[x1, x2].min(), y:[y1, y2].min(), w:(x2 - x1).abs(), h:(y2 - y1).abs())
}
}
class Group {
def shapes = []
def leftShift(shape) { shapes += shape }
def accept(Closure yield) { shapes.each{it.accept(yield)} }
}
def group = new Group()
group << new Rectangle(x:100, y:40, w:10, h:5)
group << new Rectangle(x:100, y:70, w:10, h:5)
group << new Line(x1:90, y1:30, x2:60, y2:5)
def bounds
group.accept{ bounds = it.bounds().union(bounds) }
println bounds.dump()
1.18.2 复杂范例
interface Visitor {
void visit(NodeType1 n1)
void visit(NodeType2 n2)
}
interface Visitable {
void accept(Visitor visitor)
}
class NodeType1 implements Visitable {
Visitable[] children = new Visitable[0]
void accept(Visitor visitor) {
visitor.visit(this)
for(int i = 0; i < children.length; ++i) {
children[i].accept(visitor)
}
}
}
class NodeType2 implements Visitable {
Visitable[] children = new Visitable[0]
void accept(Visitor visitor) {
visitor.visit(this)
for(int i = 0; i < children.length; ++i) {
children[i].accept(visitor)
}
}
}
class NodeType1Counter implements Visitor {
int count = 0
void visit(NodeType1 n1) {
count++
}
void visit(NodeType2 n2){}
}
如果在一个树上使用 NodeType1Counter
:
NodeType1 root = new NodeType1()
root.children = new Visitable[2]
root.children[0] = new NodeType1()
root.children[1] = new NodeType2()
那么一个 NodeType1
对象成为根,而其中的一个子节点是一个 NodeType1
实例。另一个子节点是 NodeType2
实例。这意味着使用 NodeType1Counter
应该计为 2 个 NodeType1
对象。
为什么要使用
访问者拥有一个状态,而对象树也没有改变。这一点在很多不同场合都非常有用,比如用一个访问者来计算所有节点类型,或使用了多少不同的类型,或者可以使用该节点专有方法来收集树的信息,等等。
如果加入新类型会出现什么情况?
这种情况下的确需要做更多的工作。必须改变访问者,使其接受新类型,当然必须编写类型本身,必须改变每一个已经实现了的访问者。经过一番改动之后,将所有的访问者修改为扩展自一个访问者的默认实现,所以每当添加一个新的类型时,不需要改变每一个访问者了。
如果想实现不同的迭代模式呢?
这就会出现问题。既然节点描述了如何迭代,那么我们就无法控制在某一点处停止迭代或改变迭代次序。所以我们或许可以稍加改动:
interface Visitor {
void visit(NodeType1 n1)
void visit(NodeType2 n2)
}
class DefaultVisitor implements Visitor{
void visit(NodeType1 n1) {
for(int i = 0; i < n1.children.length; ++i) {
n1.children[i].accept(this)
}
}
void visit(NodeType2 n2) {
for(int i = 0; i < n2.children.length; ++i) {
n2.children[i].accept(this)
}
}
}
interface Visitable {
void accept(Visitor visitor)
}
class NodeType1 implements Visitable {
Visitable[] children = new Visitable[0]
void accept(Visitor visitor) {
visitor.visit(this)
}
}
class NodeType2 implements Visitable {
Visitable[] children = new Visitable[0];
void accept(Visitor visitor) {
visitor.visit(this)
}
}
class NodeType1Counter extends DefaultVisitor {
int count = 0
void visit(NodeType1 n1) {
count++
super.visit(n1)
}
}
小变动产生巨大的效果。访问者现在是递归的,能清晰地告诉我们如何迭代。节点上的实现被简化为 visitor.visit(this)
, DefaultVisitor
现在可以捕获新类型了,从而不必委托给超类,即可停止迭代。当然,现在最大的缺点还是在于不再迭代了,但这是正常的:你无法获得所有的好处,不是吗?
使其 Groovy 化
现在的问题是,如何让它变得更 Groovy 化。你不觉得 visitor.visit(this)
很奇怪吗?这里为什么要出现它?其实,这是对双重分发的一种模拟。Java 使用了编译时类型,因此当 visitor.visit(children[i])
,编译器不会发现正确的方法,因为 Visitor
并不包含方法 visit(Visitable)
。甚至如果是包含了该方法,我们更乐于利用 NodeType1
或 NodeType2
访问更为特殊的方法。
Groovy 并没有使用静态类型,而是运行时类型。这意味着可以直接执行 visitor.visit(children[i])
。唔……因为简化了接收方法,使其只执行双重分发部分,而且 Groovy 的运行时类型系统已经涉及到了这一点……难道我们还需要接收方法吗?我觉得你肯定会认为答案是不需要。但还需要详细地解释一下。不知道如何处理未知的树元素,这的确是一个缺点。为此,我们必须扩展 Visitor
接口,从而造成 DefaultVisitor
的更改,继而必须提供一个更有用的默认行为,比如迭代节点或根本什么都不做。添加一个什么都不实现的 visit(Visitable)
方法,从而可以利用 Groovy 来捕获这种情况。顺便说一句,这和 Java 中的做法完全一样。
继续说,我们需要 Visitor
接口吗?如果没有接收方法,那么当然也不需要 Visitor
接口了。因此新代码如下:
class DefaultVisitor {
void visit(NodeType1 n1) {
n1.children.each { visit(it) }
}
void visit(NodeType2 n2) {
n2.children.each { visit(it) }
}
void visit(Visitable v) { }
}
interface Visitable { }
class NodeType1 implements Visitable {
Visitable[] children = []
}
class NodeType2 implements Visitable {
Visitable[] children = []
}
class NodeType1Counter extends DefaultVisitor {
int count = 0
void visit(NodeType1 n1) {
count++
super.visit(n1)
}
}
看起来又省去了一些代码。但还可以继续思考下去。 Visitable
节点并不引用任何的 Visitor
类或接口。我认为这是目前所能达到的最好的分离级别。接下来,还可以稍微改变一下 Visitable
接口,使其返回下一步想访问的子节点。从而需要实现一个通用的迭代方法:
class DefaultVisitor {
void visit(Visitable v) {
doIteraton(v)
}
void doIteraton(Visitable v) {
v.children.each {
visit(it)
}
}
}
interface Visitable {
Visitable[] getChildren()
}
class NodeType1 implements Visitable {
Visitable[] children = []
}
class NodeType2 implements Visitable {
Visitable[] children = []
}
class NodeType1Counter extends DefaultVisitor {
int count = 0
void visit(NodeType1 n1) {
count++
super.visit(n1)
}
}
DefaultVisitor
看起来有点奇怪。添加了一个 doIteration
方法,它会获取所有需要迭代的子节点,在每一元素上都调用访问方法。调用将用于迭代该子节点子级的 visit(Visitable)
。改变了 Visitable
,以便来确保任何节点都能返回子级(甚至为空时)。并不一定要改变 NodeType1
和 NodeType2
类,因为子级已被定义的管理方式使其成为一种属性,从而意味着 Groovy 可以非常好地替我们生成一个 get
方法。真正有意思的是 NodeType1Counter
,因为我们未曾修改过它。 super.visit(n1)
将调用 visit(Visitable)
,而后者也将调用开启下一级迭代的 doIteration
。因此上无须改变它。但是,如果 it
类型为 NodeType1
, visit(it)
则将调用 visit(NodeType1)
。实际上,我们并不需要 doIteration
方法,可以利用 visit(Visitable)
来实现,但是我认为前者要略好一些,因为在出现错误时,我们可以编写一个新的 Visitor
来重写 visit(Visitable)
——在这种情况下,不能用 super.visit(n1)
,而只能用 doIteration(n1)
。
小结
最终,我们成功地将代码量削减了大约超过 60%,实现了一种更为健壮而稳定的架构,最终从 Visitable
中清除了访问者。我曾听说,访问者实现可以基于反射,为的是达到更通用的版本。但如你所见,不需要那样做。如果添加了新类型,则不需要改变任何东西。有人认为,访问者模式并不太适用于极限编程技术,因为需要在一个时间段内修改很多类。我认为,出现这种问题的原因在于 Java,模式本身并没有什么好不好的。
访问者模式也存在一些变体形式,像非循环访问者模式,它要解决的问题是如何利用特殊访问者添加新节点类型。我并不是很喜欢这个变体,它基于 Cast,捕获 ClassCastException
和其他一些肮脏的东西。总之,它所试图要解决的问题,我们在 Groovy 版本中根本就碰不到。
另外, NodeType1Counter
也可以用 Java 来实现。Groovy 会识别访问方法,在需要时调用它们,因为 好用的 DefaultVisitor
仍然属于 Groovy。
1.18.3 更多参考资料
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论