0%

译-设计模式-行为模式之Visitor

意图

Visitor是行为模式的一种,允许你在不改变要操作对象类的情况下定义一个新操作。

问题

你的团队正在开发一款地理信息结构地图的app。图的节点不仅表示城镇也有其他诸如景点,行业等信息。节点间通过道路关联。在引擎中,每个节点都是一个对象,他们的类型由他们自己的类来表示。

你接到一个任务,要把地图导出为XML。乍看之下很容易实现。你需要为每个类型的节点添加一个导出方法,然后遍历地图并为每个节点执行导出方法。这个方法不仅简单而且优雅,因为你可以使用多态来避免和具体的节点类耦合。

但不幸的是,系统架构师不允许修改已存在的node类。这些代码已经在生产环境,没有人希望冒着风险修改他。

另外,他质疑节点类中的XML导出是否有意义。这些类的主要工作是和地理数据协作。导出行为放在这里看起来很不合适。

还有另一个拒绝的原因。在此之后,市场部门的人可能会要求你导出其他格式或添加其他一些奇怪的功能。这会迫使你再次修改那些珍贵的代码。

解决

Visitor模式建议你把新的行为放在一个单独的类中,而不是把它继承在已存在的类中。关联到对象的行为,不会被对象本身调用。对象被作为visitor对象的方法参数传递。

对于不同类型的对象,行为的代码可能有点不同。因此,visitor类必须为不同类型的参数提供不同的行为方法。

1
2
3
4
5
class ExportVisitor implements Visitor is
method doForCity(City c) { ... }
method doForIndustry(Industry f) { ... }
method doForSightSeeing(SightSeeing ss) { ... }
// ...

但是,我们怎么为整个地图调用这些方法呢?这些方法有不同的签名,这不允许我们使用多态。为了找到合适的方法来执行给定的对象,我们需要检查它的类。这听起来就是个噩梦。

1
2
3
4
5
6
7
foreach (Node node : graph)
if (node instanceof City)
exportVisitor.doForCity((City) node);
if (node instanceof Industry)
exportVisitor.doForIndustry((Industry) node);
// ...
}

即使给定的编程语言支持重载(比如:Java或者C#),方法的重载也不会有帮助。因为无法事先知道给定节点的精确类,即使使用了重载也不一定能正确找到执行方法。

但是Visitor模式对整个问题有一个解决方案。它使用Double Dispatch技术来保持多态性。如果我们把确定正确visitor方法的工作委托给我们传递给visitor的对象会怎么样?这些对象自己知道自己的类,所以他们可以挑选一个合适的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Client code
foreach (Node node : graph)
node.accept(exportVisitor);

// City
class City is
method accept(Visitor v) is
v.doForCity(this);
// ...

// Industry
class Industry is
method accept(Visitor v) is
v.doForIndustry(this);
// ...

我必须承认,我们必须改变node类。但是至少这个改动很小,并且具有可扩展性。

我们接下来要为所有的个性visitor抽离出通用的接口。现在,如果你需要给程序添加一个新的行为,你要做的仅仅是增加一个visitor类。所有已存在的类仍然可以不受影响的很的工作。

真实世界的类比

保险代理

想象下一个刚入行的保险员,迫切需要新的客户。他随机访问附近的邻居,为他们提供服务。但是不同类型的邻居,需要不同的保险服务。

  • 在住宅,他兜售医疗保险。

  • 在银行,他兜售防盗保险。

  • 在公司,他兜售自然灾害险。

结构

structure

  1. Visitor为所有类型的visitor声明了通用的接口。他声明了一系列把Context Components当作参数的参观方法。这些方法的名字在支持重载的语言中可以一样,但是参数类型不能一样。

  2. Concrete Visitor实现通用接口描述的操作。每个具体的visitor都表示一个独立的行为。

  3. Component声明了一个用来接收Visitor参数的方法。这个方法以Visitor接口作为参数。

  4. Concrete Component实现这个验收方法。这个方法的目的是为了用来为当前组件重定向到一个正确的visitor方法。

  5. Client表示一个集合或者其他复杂对象(比如,一个Composite树)。Client通常不知道其组件的具体类别。

伪代码

在这个例子中,Visitor模式将XML导出添加到几何图形的层次结构中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// A complex hierarchy of components.
interface Shape is
method move(x, y)
method draw()
method accept(v: Visitor)

// It is crucial to implement the accept() method in every
// single component, not just a base class. It helps the
// program to pick a proper method on the visitor class in
// case if a given component's type is unknow.
class Dot extends Shape is
// ...
method accept(v: Visitor) is
v.visitDot(this)

class Circle extends Dot is
// ...
method accept(v: Visitor) is
v.visitCircle(this)

class Rectangle extends Shape is
// ...
method accept(v: Visitor) is
v.visitRectangle(this)

class CompoundShape implements Shape is
// ...
method accept(v: Visitor) is
v.visitCompoundShape(this)


// Visitor interface must have visiting methods for the
// every single component. Note that each time you add a new
// class to the component history, you will need to add a
// method to the visitor classes. In this case, you might
// consider avoiding visitor altogether.
interface Visitor is
method visitDot(d: Dot)
method visitCircle(c: Circle)
method visitRectangle(r: Rectangle)
method visitCompoundShape(cs: CompoundShape)

// Concrete visitor adds a single operation to the entire
// hierarchy of components. Which means that if you need to
// add multiple operations, you will have to create
// several visitor.
class XMLExportVisitor is
method visitDot(d: Dot) is
Export dot's id and center coordinates.

method visitCircle(c: Circle) is
Export circle's id, center coordinates and radius.

method visitRectangle(r: Rectangle) is
Export rectangle's id, left-top coordinates, width and height.

method visitCompoundShape(cs: CompoundShape) is
Export shape's id and the list of children ids.


// Application can use visitor along with any set of
// components without checking their type first. Double
// dispatch mechanism guarantees that a proper visiting
// method will be called for any given component.
class Application is
field allShapes: array of Shapes

method export() is
exportVisitor = new XMLExportVisitor()

foreach shape in allShapes
shape.accept(exportVisitor)

如果你不知道为什么这里需要accapt方法,你需要了解一下二次分派。在Java 8之后,接口允许有默认实现,所以本例子可以利用重载和default实现的更加精简。

适用性

  • 当你需要对复杂对象结构(例如树)的所有元素执行操作时,并且所有元素都是异构的。

    Visitor模式允许你为一系列不同类型的对象执行一个操作。

  • 当你需要能够在一个复杂的对象结构上运行几个不相关的行为,但是你不想用这些行为的代码来“阻塞”结构的类。

    Visitor模式允许你从一堆构成对象结构的类中提取和统一相关的行为,并将其集成到一个visitor类中。这些转型与虚拟在不同的app中重用这些类,而不用关心和它不相关的行为。

  • 当一个新行为只对现有层次结构中的某些类有意义。

    Visitor模式允许你制作一个特殊的visitor类实现某些对象的行为,但不为其他对象。

如何实现

  1. 为程序中的具体组件创建Visitor接口并且声明一个“visiting”方法。

  2. 在组件的基类中添加抽象的accept方法。

  3. 具体的组件实现抽象的accept方法。他们必须把请求重定向到适合当前组件类的特定visitor方法。

  4. 组件层级结构只需要关心Visitor接口。这样visitor就不必和具体的组件耦合。

  5. 对于每个新行为,创建一个新的Concrete Visitor类并实现所有的访问方法。

  6. 客户端创建visitor对象,并把他们当作参数传给组建的accept方法。

优点

  • 简化类在复杂的对象结构上添加新的操作。

  • 将相关的行为移动到一个类中。

  • visitor可以在对对象结构的工作过程中积累状态。

缺点

  • 如果组件的层次经常改变,不适合使用该模式。

  • 违反组件的封装。

和其他模式的关系

  • Visitor模式像加强版的Command模式,它可以在任何类型的对象上执行一个操作。

  • Visitor可以对整个Composite树应用一个操作。

  • Visitor可以和Iterator模式协作来遍历复杂的数据结构,并对所有元素执行一些操作,即使它们有不同的类型。

小结

Visitor模式的结构比较简单,其中比较巧妙的是“二次分派”技术,这里不做展开,大家可自行问度娘或者谷哥。给个简单的例子,自行体会下

参考

翻译整理自:https://refactoring.guru/design-patterns/visitor