返回介绍

1.18 访问者模式

发布于 2025-01-04 00:44:55 字数 9243 浏览 0 评论 0 收藏 0

访问者模式(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) 。甚至如果是包含了该方法,我们更乐于利用 NodeType1NodeType2 访问更为特殊的方法。

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 ,以便来确保任何节点都能返回子级(甚至为空时)。并不一定要改变 NodeType1NodeType2 类,因为子级已被定义的管理方式使其成为一种属性,从而意味着 Groovy 可以非常好地替我们生成一个 get 方法。真正有意思的是 NodeType1Counter ,因为我们未曾修改过它。 super.visit(n1) 将调用 visit(Visitable) ,而后者也将调用开启下一级迭代的 doIteration 。因此上无须改变它。但是,如果 it 类型为 NodeType1visit(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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文