学习设计模式第二十六 - 访问者模式
示例代码来自DoFactory。
概述
当你想要为一个对象的组合增加新的能力,且封装并不重要时,就使用访问者模式。
意图
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
UML
图1 Visitor模式UML图
参与者
这个模式涉及的类或对象:
-
Visitor
-
定义一个针对对象结构中每一个ConcreteElement类的Visit操作。操作的名字和签名指明将Visit请求发送给visitor的类。其让visitor决定被访问的具体元素类型。这样visitor可以通过其特定的接口直接访问元素。
-
ConcreteVisitor
-
实现Visitor声明的每一个操作。每一个操作实现了用于结构中相应的类或对象的算法片段。ConcreteVisitor提供了算法上下文并存储其本地状态。在遍历结构的过程中,状态常将结果进行累加。
-
Element
-
定义一个接收visitor作为参数的Accept操作。
-
ConcreteElement
-
实现一个接收visitor作为参数的Accept操作。
-
ObjectStructure
-
可以遍历其元素
-
可能提供一个高层接口来允许visitor访问其元素
-
可能同时实现一个组合模式(作为Composite)或作为一个如列表或集一样的集合
适用性
在某些情况下,你会需要更改一个类型集合的功能但是你不能控制层次中的类型。这是Visitor模式出场的地方:Visitor模式的意图是为一个类型的集合定义一个新的操作(如,被访问的类型)而无须更改层次本身。新的逻辑位于一个单独的类,如Visitor中。Visitor模式的棘手的方面在于集合中类的原始开发者必须预计到在将来可能发生的功能调整,而在其中包含可以接收Visitor类对象并允许这个对象定义新操作的方法。
Visitor是一个有争议的模式,有经验的.NET开发者常避免使用它。争议在于其添加了复杂性并创造出与面向对象设计中普遍接受的最佳实践相反的脆弱的代码。当决定使用这种模式时,通常最好的做法是仔细衡量,对于替代方案,如继承或委托,后者更可能给你的问题提供一个更健壮的解决方案。
-
一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。
-
需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作"污染"这些对象的类。Visitor使得你可以将相关的操作集中起来定义在一个类中。当该对象结构被很多应用共享时,用 Visitor 模式让每个应用仅包含需要用到的操作。
-
定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。另外如果对象结构类经常改变,那么还是在这些类中定义这些操作较好。
DoFactory GoF代码
这个Visitor模式的示例中,一个对象遍历一个对象结构并且在结构中每一个节点上执行相同的操作。不同的visitor对象定义不同的操作。
// Visitor pattern // Structural example using System; using System.Collections.Generic; namespace DoFactory.GangOfFour.Visitor.Structural { // MainApp test application class MainApp { static void Main() { // Setup structure ObjectStructure o = new ObjectStructure(); o.Attach(new ConcreteElementA()); o.Attach(new ConcreteElementB()); // Create visitor objects ConcreteVisitor1 v1 = new ConcreteVisitor1(); ConcreteVisitor2 v2 = new ConcreteVisitor2(); // Structure accepting visitors o.Accept(v1); o.Accept(v2); // Wait for user Console.ReadKey(); } } // "Visitor" abstract class Visitor { public abstract void VisitConcreteElementA(ConcreteElementA concreteElementA); public abstract void VisitConcreteElementB(ConcreteElementB concreteElementB); } // "ConcreteVisitor" class ConcreteVisitor1 : Visitor { public override void VisitConcreteElementA(ConcreteElementA concreteElementA) { Console.WriteLine("{0} visited by {1}",concreteElementA.GetType().Name, this.GetType().Name); } public override void VisitConcreteElementB(ConcreteElementB concreteElementB) { Console.WriteLine("{0} visited by {1}",concreteElementB.GetType().Name, this.GetType().Name); } } // "ConcreteVisitor" class ConcreteVisitor2 : Visitor { public override void VisitConcreteElementA(ConcreteElementA concreteElementA) { Console.WriteLine("{0} visited by {1}", concreteElementA.GetType().Name, this.GetType().Name); } public override void VisitConcreteElementB(ConcreteElementB concreteElementB) { Console.WriteLine("{0} visited by {1}", concreteElementB.GetType().Name, this.GetType().Name); } } // "Element" abstract class Element { public abstract void Accept(Visitor visitor); } // "ConcreteElement" class ConcreteElementA : Element { public override void Accept(Visitor visitor) { visitor.VisitConcreteElementA(this); } public void OperationA() { } } // "ConcreteElement" class ConcreteElementB : Element { public override void Accept(Visitor visitor) { visitor.VisitConcreteElementB(this); } public void OperationB() { } } // "ObjectStructure" class ObjectStructure { private List<Element> _elements = new List<Element>(); public void Attach(Element element) { _elements.Add(element); } public void Detach(Element element) { _elements.Remove(element); } public void Accept(Visitor visitor) { foreach (Element element in _elements) { element.Accept(visitor); } } } }
这个现实世界的访问者模式的例子中,两个visitor对象遍历一个Employee列表并在每一个Employee上执行相同的操作。两个visitor对象定义不同的操作 – 一个调整休假日期另一个调整收入。
例子中涉及到的类与访问者模式中标准的类对应关系如下:
-
Visitor – Visitor
-
ConcreteVisitor – IncomeVisitor, VacationVisitor
-
Element – Element
-
ConcreteElement – Employee
-
ObjectStructure – Employees
// Visitor pattern // Real World example using System; using System.Collections.Generic; namespace DoFactory.GangOfFour.Visitor.RealWorld { // MainApp startup application class MainApp { static void Main() { // Setup employee collection Employees e = new Employees(); e.Attach(new Clerk()); e.Attach(new Director()); e.Attach(new President()); // Employees are 'visited' e.Accept(new IncomeVisitor()); e.Accept(new VacationVisitor()); // Wait for user Console.ReadKey(); } } // "Visitor" interface IVisitor { void Visit(Element element); } // "ConcreteVisitor" class IncomeVisitor : IVisitor { public void Visit(Element element) { Employee employee = element as Employee; // Provide 10% pay raise employee.Income *= 1.10; Console.WriteLine("{0} {1}'s new income: {2:C}",employee.GetType().Name, employee.Name, employee.Income); } } // "ConcreteVisitor" class VacationVisitor : IVisitor { public void Visit(Element element) { Employee employee = element as Employee; // Provide 3 extra vacation days Console.WriteLine("{0} {1}'s new vacation days: {2}", employee.GetType().Name, employee.Name, employee.VacationDays); } } // "Element" abstract class Element { public abstract void Accept(IVisitor visitor); } // "ConcreteElement" class Employee : Element { private string _name; private double _income; private int _vacationDays; // Constructor public Employee(string name, double income, int vacationDays) { this._name = name; this._income = income; this._vacationDays = vacationDays; } // Gets or sets the name public string Name { get { return _name; } set { _name = value; } } // Gets or sets income public double Income { get { return _income; } set { _income = value; } } // Gets or sets number of vacation days public int VacationDays { get { return _vacationDays; } set { _vacationDays = value; } } public override void Accept(IVisitor visitor) { visitor.Visit(this); } } // "ObjectStructure" class Employees { private List<Employee> _employees = new List<Employee>(); public void Attach(Employee employee) { _employees.Add(employee); } public void Detach(Employee employee) { _employees.Remove(employee); } public void Accept(IVisitor visitor) { foreach (Employee e in _employees) { e.Accept(visitor); } Console.WriteLine(); } } // Three employee types class Clerk : Employee { // Constructor public Clerk() : base("Hank", 25000.0, 14) { } } class Director : Employee { // Constructor public Director() : base("Elly", 35000.0, 16) { } } class President : Employee { // Constructor public President() : base("Dick", 45000.0, 21) { } } }
这个例子实现了与上述例子相同的功能,更多的使用了更现代的,内置的.NET特性。在这个例子中Visitor模式使用了反射。这种方式让我们可以有选择的选择想要visit哪一个Employee。ReflectiveVisit()查找一个参数兼容的Visit()方法。注意反射的使用使Visitor更灵活但也更慢且更复杂。这个例子将employee的集合存储在一个泛型List<Employee>列表中。这个例子中用到的.NET 3.0的特性包括Employee类中自动属性及Employees类中的Foreach扩展方法。
// Visitor pattern // .NET Optimized example using System; using System.Collections.Generic; using System.Reflection; namespace DoFactory.GangOfFour.Visitor.NETOptimized { class MainApp { static void Main() { // Setup employee collection var e = new Employees(); e.Attach(new Clerk()); e.Attach(new Director()); e.Attach(new President()); // Employees are 'visited' e.Accept(new IncomeVisitor()); e.Accept(new VacationVisitor()); // Wait for user Console.ReadKey(); } } // "Visitor" public abstract class Visitor { // Use reflection to see if the Visitor has a method // named Visit with the appropriate parameter type // (i.e. a specific Employee). If so, invoke it. public void ReflectiveVisit(IElement element) { var types = new Type[] { element.GetType() }; var mi = this.GetType().GetMethod("Visit", types); if (mi != null) { mi.Invoke(this, new object[] { element }); } } } // "ConcreteVisitor" class IncomeVisitor : Visitor { // Visit clerk public void Visit(Clerk clerk) { DoVisit(clerk); } // Visit director public void Visit(Director director) { DoVisit(director); } private void DoVisit(IElement element) { var employee = element as Employee; // Provide 10% pay raise employee.Income *= 1.10; Console.WriteLine("{0} {1}'s new income: {2:C}", employee.GetType().Name, employee.Name, employee.Income); } } // "ConcreteVisitor" class VacationVisitor : Visitor { // Visit director public void Visit(Director director) { DoVisit(director); } private void DoVisit(IElement element) { var employee = element as Employee; // Provide 3 extra vacation days employee.VacationDays += 3; Console.WriteLine("{0} {1}'s new vacation days: {2}", employee.GetType().Name, employee.Name, employee.VacationDays); } } // "Element" public interface IElement { void Accept(Visitor visitor); } // "ConcreteElement" class Employee : IElement { // Constructor public Employee(string name, double income, int vacationDays) { this.Name = name; this.Income = income; this.VacationDays = vacationDays; } // Gets or sets name public string Name { get; set; } // Gets or set income public double Income { get; set; } // Gets or sets vacation days public int VacationDays { get; set; } public virtual void Accept(Visitor visitor) { visitor.ReflectiveVisit(this); } } // "ObjectStructure" class Employees : List<Employee> { public void Attach(Employee employee) { Add(employee); } public void Detach(Employee employee) { Remove(employee); } public void Accept(Visitor visitor) { // Iterate over all employees and accept visitor this.ForEach(employee => employee.Accept(visitor)); Console.WriteLine(); } } // Three employee types class Clerk : Employee { // Constructor public Clerk() : base("Hank", 25000.0, 14) { } } class Director : Employee { // Constructor public Director() : base("Elly", 35000.0, 16) { } } class President : Employee { // Constructor public President() : base("Dick", 45000.0, 21) { } } }
访问者模式解说
访问者必须参观组合内的每个元素,这样的功能是在Traverser对象中,访问者通过Traverser的引导,收集组合中所有对象的状态。一旦收集了状态,客户就可以让访问者对状态进行各种操作。当需要新的功能时,只要加强访问者即可。
访问者模式适用于数据结构相对稳定的系统。它把数据结构和作用于结构上的操作之间的耦合进行分解,使操作集合可以相对自由地演化。
访问者模式的目的是要把处理从数据结构分离出来。如果系统有比较稳定的数据结构,又有易于变化的算法的话,使用访问者模式就比较适合,因为访问者模式使得算法操作的增加变得容易。
.NET中的代理模式
在.NET Framework类库中没有用到Visitor模式。
效果及实现要点
优点:
允许你对组合结构加入新的操作,而无需改变结构本身。
想要加入新的操作,相对容易。
访问者所进行的操作,其代码是集中在一起的。
增加新的操作很容易,因为增加新的操作就意味着增加一个新的访问者。访问者模式将有关的行为集中到一个访问者对象中。
缺点:
当采用访问者模式的时候,就会打破组合类的封装。
因为游走到功能牵涉其中,所以对组合结构的改变就更加困难(增加新的数据结构更困难)。
总结
访问者模式把数据处理从数据结构分离出来,可以在不改变数据结构的情况下,替换现有处理方式或增加新的处理功能。