本博客大部分内容来于免费在线学习设计模式
1:访问者模式
访问者模式是一种行为设计模式, 它能将算法与其所作用的对象隔离开来。
2:访问者模式问题
假如你的团队开发了一款能够使用巨型图像中地理信息的应用程序。图像中的每个节点既能代表复杂实体(例如一座城市),也能代表更精细的对象(例如工业区和旅游景点等)。如果节点代表的真实对象之间存在公路,那么这些节点就会相互连接。在程序内部,每个节点的类型都由其所属的类来表示,每个特定的节点则是一个对象。
一段时间后,你接到了实现将图像导出到 XML 文件中的任务。这些工作最初看上去非常简单。你计划为每个节点类添加导出函数,然后递归执行图像中每个节点的导出函数。解决方案简单且优雅:使用多态机制可以让导出方法的调用代码不会和具体的节点类相耦合。
但是架构师拒绝批准对已有节点类进行修改。他认为这些代码已经是产品了,不想冒险对其进行修改,因为修改可能会引入潜在的缺陷。
他还质疑在节点类中包含导出 XML 文件的代码是否有意义。这些类的主要工作是处理地理数据。导出 XML 文件的代码放在这里并不合适。
还有另一个原因,那就是在此项任务完成后, 营销部门很有可能会要求程序提供导出其他类型文件的功能,或者提出其他奇怪的要求。 这样你很可能会被迫再次修改这些重要但脆弱的类。
在现实生活中,有些集合对象存在多种不同的元素,且每种元素也存在多种不同的访问者和处理方式。例如,公园中存在多个景点,也存在多个游客,不同的游客对同一个景点的评价可能不同;医院医生开的处方单中包含多种药元素,査看它的划价员和药房工作人员对它的处理方式也不同,划价员根据处方单上面的药品名和数量进行划价,药房工作人员根据处方单的内容进行抓药。
3:访问者模式解决方案
访问者模式建议将新行为放入一个名为访问者的独立类中,而不是试图将其整合到已有类中。现在,需要执行操作的原始对象将作为参数被传递给访问者中的方法,让方法能访问对象所包含的一切必要数据。
如果现在该操作能在不同类的对象上执行会怎么样呢?比如在我们的示例中,各节点类导出 XML 文件的实际实现很可能会稍有不同。因此,访问者类可以定义一组(而不是一个)方法,且每个方法可接收不同类型的参数,如下所示:
1 | class ExportVisitor implements Visitor is |
但我们究竟应该如何调用这些方法(尤其是在处理整个图像方面)呢?这些方法的签名各不相同,因此我们不能使用多态机制。为了可以挑选出能够处理特定对象的访问者方法,我们需要对它的类进行检查。这是不是听上去像个噩梦呢?
1 | foreach (Node node in graph) |
你可能会问,我们为什么不使用方法重载呢?就是使用相同的方法名称,但它们的参数不同。不幸的是,即使我们的编程语言(例如 Java 和 C#)支持重载也不行。由于我们无法提前知晓节点对象所属的类,所以重载机制无法执行正确的方法。方法会将节点基类作为输入参数的默认类型。
但是,访问者模式可以解决这个问题。它使用了一种名为双分派的技巧,不使用累赘的条件语句也可执行正确的方法。与其让客户端来选择调用正确版本的方法,不如将选择权委派给作为参数传递给访问者的对象。由于该对象知晓其自身的类,因此能更自然地在访问者中选出正确的方法。 它们会 “接收” 一个访问者并告诉其应执行的访问者方法。
1 | // 客户端代码 |
虽然最终还是修改了节点类,但毕竟改动很小,且使得我们能够在后续进一步添加行为时无需再次修改代码。
现在,如果我们抽取出所有访问者的通用接口,所有已有的节点都能与我们在程序中引入的任何访问者交互。如果需要引入与节点相关的某个行为,你只需要实现一个新的访问者类即可。
4:访问者模式结构
- 访问者(Visitor)接口声明了一系列以对象结构的具体元素为参数的访问者方法。如果编程语言支持重载,这些方法的名称可以是相同的,但是其参数一定是不同的。
- 具体访问者(Concrete Visitor)会为不同的具体元素类实现相同行为的几个不同版本。
- 元素(Element)接口声明了一个方法来 “接收” 访问者。该方法必须有一个参数被声明为访问者接口类型。
- 具体元素(Concrete Element)必须实现接收方法。该方法的目的是根据当前元素类将其调用重定向到相应访问者的方法。请注意,即使元素基类实现了该方法,所有子类都必须对其进行重写并调用访问者对象中的合适方法。
- 客户端(Client)通常会作为集合或其他复杂对象(例如一个组合树)的代表。客户端通常不知晓所有的具体元素类,因为它们会通过抽象接口与集合中的对象进行交互。
代码示例:
通过访问者对象将各种类型的对象导出为 XML 格式文件。
1 | /** |
输出如下:
1 | 导出点 |
5:访问者模式适用场景
- 如果你需要对一个复杂对象结构 (例如对象树) 中的所有元素执行某些操作, 可使用访问者模式。
- 可使用访问者模式来清理辅助行为的业务逻辑。
- 当某个行为仅在类层次结构中的一些类中有意义, 而在其他类中没有意义时, 可使用该模式。
6:访问者模式优缺点
优点 | 缺点 |
---|---|
开闭原则。 你可以引入在不同类对象上执行的新行为, 且无需对这些类做出修改。 | 每次在元素层次结构中添加或移除一个类时, 你都要更新所有的访问者。 |
单一职责原则。 可将同一行为的不同版本移到同一个类中。 | 在访问者同某个元素进行交互时, 它们可能没有访问元素私有成员变量和方法的必要权限。 |
访问者对象可以在与各种对象交互时收集一些有用的信息。 当你想要遍历一些复杂的对象结构 (例如对象树), 并在结构中的每个对象上应用访问者时, 这些信息可能会有所帮助。 |
8:访问者模式与其他模式关系
- 你可以将访问者模式视为命令模式的加强版本,其对象可对不同类的多种对象执行操作。
- 你可以使用访问者对整个组合模式树执行操作。
- 可以同时使用访问者和迭代器模式来遍历复杂数据结构,并对其中的元素执行所需操作,即使这些元素所属的类完全不同。
9:访问者模式举例
这里用客户和公司代替元素和访问者:公司是用来处理业务的,不同部门负责不同的业务,所以不同部门就是具体访问者。而办理不同业务的各种客户则是多种多样的具体元素。将公司地址告诉给客户,客户就按照自己办理业务自信寻找对应部门。