访问者模式是一种行为型模式,它能将算法与其所作用的对象隔离开来。
假如你的团队开发了一款能够使用巨型图像中地理信息的应用程序。图像中的每个节点既能代表复杂实体(例如一座城市),也能代表更精细的对象(例如工业区和旅游景点等)。如果节点代表的真实对象之间存在公路,那么这些节点就会相互连接。在程序内部,每个节点的类型都由其所属的类来表示,每个特定的节点则是一个对象。
一段时间后,你接到了实现将图像导出到 XML 文件中的任务。这些工作最初看上去非常简单。你计划为每个节点类添加导出函数,然后递归执行图像中每个节点的导出函数。解决方案简单且优雅:使用多态机制可以让导出方法的调用代码不会和具体的节点类相耦合。
但你不太走运,系统架构师拒绝批准对已有节点类进行修改。他认为这些代码已经是产品了,不想冒险对其进行修改,因为修改可能会引入潜在的缺陷。
此外,他还质疑在节点类中包含导出 XML 文件的代码是否有意义。这些类的主要工作是处理地理数据。导出 XML 文件的代码放在这里并不合适。
还有另一个原因,那就是在此项任务完成后,营销部门很有可能会要求程序提供导出其他类型文件的功能,或者提出其他奇怪的要求。这样你很可能会被迫再次修改这些重要但脆弱的类。
访问者模式建议将新行为放入一个名为访问者的独立类中,而不是试图将其整合到已有类中。现在,需要执行操作的原始对象将作为参数被传递给访问者中的方法,让方法能访问对象所包含的一切必要数据。
如果现在该操作能在不同类的对象上执行会怎么样呢?比如在我们的示例中,各节点类导出 XML 文件的实际实现很可能会稍有不同。因此,访问者类可以定义一组(而不是一个)方法,且每个方法可接收不同类型的参数,如下所示:
12345 | class ExportVisitor implements Visitor is method doForCity(City c) { ... } method doForIndustry(Industry f) { ... } method doForSightSeeing(SightSeeing ss) { ... } // ... |
---|
但我们究竟应该如何调用这些方法(尤其是在处理整个图像方面)呢?这些方法的签名各不相同,因此我们不能使用多态机制。为了可以挑选出能够处理特定对象的访问者方法,我们需要对它的类进行检查。这是不是听上去像个噩梦呢?
1234567 | foreach (Node node in graph) if (node instanceof City) exportVisitor.doForCity((City) node) if (node instanceof Industry) exportVisitor.doForIndustry((Industry) node) // ...} |
---|
你可能会问,我们为什么不使用方法重载呢?就是使用相同的方法名称,但它们的参数不同。不幸的是,即使我们的编程语言(例如 Java 和 C#)支持重载也不行。由于我们无法提前知晓节点对象所属的类,所以重载机制无法执行正确的方法。方法会将节点
基类作为输入参数的默认类型。
但是,访问者模式可以解决这个问题。它使用了一种名为双分派的技巧,不使用累赘的条件语句也可下执行正确的方法。与其让客户端来选择调用正确版本的方法,不如将选择权委派给作为参数传递给访问者的对象。由于该对象知晓其自身的类,因此能更自然地在访问者中选出正确的方法。它们会“接收”一个访问者并告诉其应执行的访问者方法。
123456789101112131415 | // 客户端代码foreach (Node node in graph) node.accept(exportVisitor)// 城市class City is method accept(Visitor v) is v.doForCity(this) // ...// 工业区class Industry is method accept(Visitor v) is v.doForIndustry(this) // ... |
---|
我承认最终还是修改了节点类,但毕竟改动很小,且使得我们能够在后续进一步添加行为时无需再次修改代码。
现在,如果我们抽取出所有访问者的通用接口,所有已有的节点都能与我们在程序中引入的任何访问者交互。如果需要引入与节点相关的某个行为,你只需要实现一个新的访问者类即可。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 | using System;using System.Collections.Generic;namespace RefactoringGuru.DesignPatterns.Visitor.Conceptual{ // The Component interface declares an `accept` method that should take the // base visitor interface as an argument. public interface IComponent { void Accept(IVisitor visitor); } // Each Concrete Component must implement the `Accept` method in such a way // that it calls the visitor's method corresponding to the component's // class. public class ConcreteComponentA : IComponent { // Note that we're calling `VisitConcreteComponentA`, which matches the // current class name. This way we let the visitor know the class of the // component it works with. public void Accept(IVisitor visitor) { visitor.VisitConcreteComponentA(this); } // Concrete Components may have special methods that don't exist in // their base class or interface. The Visitor is still able to use these // methods since it's aware of the component's concrete class. public string ExclusiveMethodOfConcreteComponentA() { return "A"; } } public class ConcreteComponentB : IComponent { // Same here: VisitConcreteComponentB => ConcreteComponentB public void Accept(IVisitor visitor) { visitor.VisitConcreteComponentB(this); } public string SpecialMethodOfConcreteComponentB() { return "B"; } } // The Visitor Interface declares a set of visiting methods that correspond // to component classes. The signature of a visiting method allows the // visitor to identify the exact class of the component that it's dealing // with. public interface IVisitor { void VisitConcreteComponentA(ConcreteComponentA element); void VisitConcreteComponentB(ConcreteComponentB element); } // Concrete Visitors implement several versions of the same algorithm, which // can work with all concrete component classes. // // You can experience the biggest benefit of the Visitor pattern when using // it with a complex object structure, such as a Composite tree. In this // case, it might be helpful to store some intermediate state of the // algorithm while executing visitor's methods over various objects of the // structure. class ConcreteVisitor1 : IVisitor { public void VisitConcreteComponentA(ConcreteComponentA element) { Console.WriteLine(element.ExclusiveMethodOfConcreteComponentA() + " + ConcreteVisitor1"); } public void VisitConcreteComponentB(ConcreteComponentB element) { Console.WriteLine(element.SpecialMethodOfConcreteComponentB() + " + ConcreteVisitor1"); } } class ConcreteVisitor2 : IVisitor { public void VisitConcreteComponentA(ConcreteComponentA element) { Console.WriteLine(element.ExclusiveMethodOfConcreteComponentA() + " + ConcreteVisitor2"); } public void VisitConcreteComponentB(ConcreteComponentB element) { Console.WriteLine(element.SpecialMethodOfConcreteComponentB() + " + ConcreteVisitor2"); } } public class Client { // The client code can run visitor operations over any set of elements // without figuring out their concrete classes. The accept operation // directs a call to the appropriate operation in the visitor object. public static void ClientCode(List<IComponent> components, IVisitor visitor) { foreach (var component in components) { component.Accept(visitor); } } } class Program { static void Main(string[] args) { List<IComponent> components = new List<IComponent> { new ConcreteComponentA(), new ConcreteComponentB() }; Console.WriteLine("The client code works with all visitors via the base Visitor interface:"); var visitor1 = new ConcreteVisitor1(); Client.ClientCode(components,visitor1); Console.WriteLine(); Console.WriteLine("It allows the same client code to work with different types of visitors:"); var visitor2 = new ConcreteVisitor2(); Client.ClientCode(components, visitor2); } }} |
---|
执行结果:
1234567 | The client code works with all visitors via the base Visitor interface:A + ConcreteVisitor1B + ConcreteVisitor1It allows the same client code to work with different types of visitors:A + ConcreteVisitor2B + ConcreteVisitor2 |
---|
参考原文:访问者设计模式