访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改的话,接受这个操作的数据结构则可以保持不变。
访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改的话,接受这个操作的数据结构则可以保持不变。
打个比喻,好像一棵树子(某种数据结构),以前上面只能挂一种果实,采用一种操作方法。而现在,上面既可以挂苹果,也可以挂梨子,甚至还可以挂香蕉(不同类型的对象),而我们的操作方法即可以用手摘,也可以用挂钩拉,还可以用钳子夹(作用于结构上的不同的操作),当然以后还可以包括其它任何我们想要采取的办法。那么应对此种变化,我们就引入了访问者模式。
访问者模式适用于数据结构相对未定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由地演化。数据结构的每一个节点都可以接受一个访问者的调用,此节点向访问者对象传入节点对象,而访问者对象则反过来执行节点对象的操作。这样的过程叫做"双重分派"。节点调用访问者,将它自己传入,访问者则将某算法针对此节点执行。
由上可以看到,双重分派是理解访问者模式的重点。而为了理解双重分派,我们要循序渐近地学习以下概念:
1、静态分派 2、动态分派 3、单重分派 4、双重分派
首先,我们的程序如下图:
一、关于分派
1、静态分派
StaticDispatch.cs代码
Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyVisitor
{
public class Bird { };
public class Eagle : Bird { };
public class Swallow : Bird { };
public class Person
{
public void feed(Bird bird)
{
Console.WriteLine("喂鸟!");
}
public void feed(Eagle eaglebird)
{
Console.WriteLine("喂老鹰!");
}
public void feed(Swallow swallowbird)
{
Console.WriteLine("喂燕子!");
}
}
}
客户应用代码
Code
#region 静态分派
//因为重载是静态分派,在编译器执行时,取决于变量的声明类型,
//因为eagle与swallow在声明时都是Bird(如下),所以调用的都是fee(Bird bird)的函数
Console.WriteLine("-----------静态分派---------------");
Bird eagle = new Eagle(); //虽然创建是Eagle,但声明是Bird
Bird swallow = new Swallow(); //虽然创建是Swallow,但声明是Bird
Person p = new Person();
p.feed(eagle);
p.feed(swallow);
Console.ReadKey();
#endregion
运行效果如下:
2、动态分派
DynamicDispatch.cs代码如下:
Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyVisitor
{
public class Dog
{
public virtual void bark()
{
Console.WriteLine("狗在吠!");
}
}
public class WildDog : Dog
{
public override void bark()
{
Console.WriteLine("野狗在吠!");
}
}
}
客户应用代码如下:
Code
#region 动态分派
Console.WriteLine("-----------静态分派---------------");
//在执行时发生了向下转型,就是动态分派,
//所以虽然我们声明的是Dog,但我们new的是WildDog,因此结果不是"狗吠",而是"野狗在吠"
Dog dog = new WildDog();
dog.bark();
Console.ReadKey();
#endregion
运行效果如下:
3、单重分派
SingleDispatch.cs代码如下:
Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyVisitor
{
public class book
{
public virtual void read()
{
Console.WriteLine("Book reading");
}
}
public class LifeBook : book
{
public override void read()
{
Console.WriteLine("LifeBook reading");
}
}
public class ScienceBook : book
{
public override void read()
{
Console.WriteLine("Science reading");
}
}
}
客户应用代码如下:
Code
#region 单重分派
//单分派(single dispatch)就是说我们在选择一个方法的时候仅仅需要根据消息接收者(receiver)的运行时型别(Run time type)。
//实际上这也就是我们经常提到的多态的概念。举一个简单的例子,我们有一个基类book,Book有一个虚方法read(可被子类override),
//LifeBook和ScienceBook是Book的两个子类,在LifeBook和ScienceBook中我们覆写(override)了方法read。
//这样我们对消息read的调用,需要根据接收者Book或者Book的子类LifeBook/ScienceBook的具体型别才可以确定具体是调用Book的还是LifeBook/ScienceBook的read方法。
Console.WriteLine("-----------单重分派---------------");
book bk1 = new book();
bk1.read();
book bk2 = new LifeBook();
bk2.read();
book bk3 = new ScienceBook();
bk3.read();
Console.ReadKey();
#endregion
运行效果如下:
4、双重分派
DoubleDispatch.cs代码如下:
Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyVisitor
{
#region 定义了一个抽象类Animal,它是Human与Cat类的基类
public abstract class Animal
{
public virtual void face(Animal animal) { }
public virtual void face(Human human) { }
public virtual void face(Cat cat) { }
}
#endregion
#region 定义Human类,继承自Animal抽象类
public class Human : Animal
{
#region face(animal)
public override void face(Animal animal)
{
animal.face(this); //*****把此处解除注释实现双重分派*****注意:是此行,而不是其它行
Console.WriteLine("Called:Human--face(Animal animal)");
}
#endregion
#region face(human)
public override void face(Human human)
{
//human.face(this); //把此处解除注释实现"双重/多重"分派
Console.WriteLine("Called:Human--face(Human human)");
}
#endregion
#region face(cat)
public override void face(Cat cat)
{
//cat.face(this); //把此处解除注释实现"双重/多重"分派
Console.WriteLine("Called:Human--face(Cat cat)");
}
#endregion
}
#endregion
#region 定义Cat类,继承自Animal抽象类
public class Cat : Animal
{
#region face(animal)
public override void face(Animal animal)
{
//animal.face(this);//把此处解除注释实现"双重/多重"分派
Console.WriteLine("Called:Cat--face(Cat cat)");
}
#endregion
#region face(human)
public override void face(Human human)
{
// human.face(this); //把此处解除注释实现"双重/多重"分派
Console.WriteLine("Called:Cat--face(Human human)");
}
#endregion
#region face(cat)
public override void face(Cat cat)
{
//cat.face(this); //把此处解除注释实现"双重/多重"分派
Console.WriteLine("Called:Cat--face(Cat cat)");
}
#endregion
}
#endregion
}
客户应用代码如下:
Code
Console.WriteLine("-----------双重分派---------------");
Animal human = new Human();
Animal cat = new Cat();
human.face(cat);
Console.ReadKey();
代码说明:
(a)关于face方法 (方法调用者的类型是运行期确定)
尽管human声明为Animal类型,但human.face调用的是Human类中的override的face函数,这是因为动态分派只会体现在方法的调用者身上
因为此处face方法的调用者是Human类型,虽然声明的是Animal类型,但new的却是Human类型,所以只会调用Human类中override的face方法
因此显示了"动态分派"的特性
(b)关于face方法中的参数 (方法参数的类型是编译期确定)
接上面,我们知道实际调用的是Human类中的face方法,我们也知道在Human类中我们override了三个face方法,分别对应animal,human,cat三种类型的参数
现在的问题是,此处我们定义了Animal cat = new Cat(),并把cat作为参数传入human.face(cat),那么,它将激发Human类中的哪一个override的face方法呢
运行后,我们发现,是调用了public override void face(Animal animal)这个方法。这是因为方法的参数类型会在编译期由编译器决定,所以显示了"静态分派"的特性
针对Animal cat = new Cat()虽然我们new 的是Cat类型,但声明的是Animal,所以在编译时对于human.face(cat)内的参数cat,类型其实确定为Animal,因此,传到了
human.face方法中,调用的是public override void face(Animal animal)方法
根据(a)(b),我们推断:如果参数cat的类型也能够在运行期决定,那么哪个face被调用就由方法调用者和方法参数共同在运行期决定了。
那么如何实现参数类型在运行期绑定呢?既然方法调用者的类型是运行期才确定的,那么我们就可以反客为主了,将方法参数变成方法调用者。如下:
face(Cat arg) {arg.face(this);}
下面,我们把DoubleDispatch类中关于face方法定义代码段中的 //*****把此处解除注释实现双重分派***** 那一排的注释去掉就可以看到双重分派的效果。
至此,我们应该明白了双重分派的涵义:哪个face最终被调用经过两次运行期类型绑定才确定下来,这样的过程就是双重分派了。进一步,自然可以理解多重分派。
运行效果如下:
二、进入访问者模式
访问者模式的UML图如下:
其相关角色如下:
1、抽象访问者(Visitor)角色:声明了一个或者多个访问操作,形成所有的具体元素角色必须实现的接口。
2、具体访问者(ConcreteVisitor)角色:实现抽象访问者角色所声明的接口,也就是抽象访问者所声明的各个访问操作。
3、抽象节点(Element)角色:声明一个接受操作,接受一个访问者对象作为一个参量。
4、具体节点(ConcreteElement)角色:实现了抽象元素所规定的接受操作。
5、结构对象(ObiectStructure)角色:有如下的一些责任,可以遍历结构中的所有元素;如果需要,提供一个高层次的接口让访问者对象可以访问每一个元素;如果需要,可以设计成一个复合对象或者一个聚集,如列(List)或集合(Set)。
(一)、访问者模式基本思路示例
1、抽象访问者(Visitor)角色:
Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyVisitor
{
#region 抽象访问者(Visitor)角色
//声明了一个或者多个访问操作,形成所有的具体元素角色必须实现的接口。
abstract class Visitor
{
public abstract void VisitConcreteElementA(ConcreteElementA concreteElementA);
public abstract void VisitConcreteElementB(ConcreteElementB concreteElementB);
}
#endregion
}
2、具体访问者(ConcreteVisitor)角色:
Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyVisitor
{
//这时定义了不同的具体访问者,它们实现了抽象访问者角色所声明的接口,也就是抽象访问者所声明的各个访问操作。
//每一次具体访问者都可以访问不同类型的元素节点
#region 具体访问者(ConcreteVisitor)角色一
class ConcreteVisitor1: Visitor
{
#region 访问ConcreteElementA类型的元素
public override void VisitConcreteElementA(ConcreteElementA concreteElementA)
{
Console.WriteLine("{0}被{1}访问",concreteElementA.GetType().Name,this.GetType().Name);
}
#endregion
#region 访问ConcreteElementB类型的元素
public override void VisitConcreteElementB(ConcreteElementB concreteElementB)
{
Console.WriteLine("{0}被{1}访问",concreteElementB.GetType().Name,this.GetType().Name);
}
#endregion
}
#endregion
#region 具体访问者(ConcreteVisitor)角色二
class ConcreteVisitor2 : Visitor
{
#region 访问ConcreteElementA类型的元素
public override void VisitConcreteElementA(ConcreteElementA concreteElementA)
{
Console.WriteLine("{0}被{1}访问", concreteElementA.GetType().Name, this.GetType().Name);
}
#endregion
#region 访问ConcreteElementB类型的元素
public override void VisitConcreteElementB(ConcreteElementB concreteElementB)
{
Console.WriteLine("{0}被{1}访问", concreteElementB.GetType().Name, this.GetType().Name);
}
#endregion
}
#endregion
}
3、抽象节点(Element)角色:
Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyVisitor
{
#region 抽象节点(Node)角色
//声明一个接受操作,接受一个访问者对象作为一个参量,但具体是哪个访问者对象则在运行时通过双重分派机制来决定
abstract class Element
{
public abstract void Accept(Visitor visitor);
}
#endregion
}
4、具体节点(ConcreteElement)角色:
Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyVisitor
{
//此处定义了具体节点(Node)角色,它们实现了抽象元素所规定的接受操作。
#region 具体节点角色A
class ConcreteElementA : Element
{
public override void Accept(Visitor visitor)
{
visitor.VisitConcreteElementA(this);
}
public void OperationA()
{
}
}
#endregion
#region 具体节点角色B
class ConcreteElementB : Element
{
public override void Accept(Visitor visitor)
{
visitor.VisitConcreteElementB(this);
}
public void OperationB()
{
}
}
#endregion
}
5、结构对象(ObiectStructure)角色:
Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyVisitor
{
class ObjectStructure
{
//结构对象(ObiectStructure)角色:有如下的一些责任,可以遍历结构中的所有元素;
//如果需要,提供一个高层次的接口让访问者对象可以访问每一个元素;
//如果需要,可以设计成一个复合对象或者一个聚集,如列(List)或集合(Set)。
//它就像一棵大树(某种数据结构),上面可以挂苹果,也可以挂梨子,甚至可以挂香蕉(不同类型的对象)
//而我们即可以用手摘,也可以用挂钩拉,还可以用钳子夹(作用于结构上的不同的操作)
private List<Element> _elements = new List<Element>();
#region 加入新元素到此结构中
//注意:加入的元素可以是不同的具体类型,即:不同类的具体节点角色
public void Attach(Element element)
{
_elements.Add(element);
}
#endregion
#region 移除一个元素到此结构中
public void Detach(Element element)
{
_elements.Remove(element);
}
#endregion
public void Accept(Visitor visitor)
{
//遍历结构中的所有元素
foreach (Element element in _elements)
{
element.Accept(visitor);
}
}
}
}
6、客户应用代码
Code
#region 基本思路示例
Console.WriteLine("-----------基本思路示例---------------");
//建立结构
ObjectStructure o = new ObjectStructure();
o.Attach(new ConcreteElementA());
o.Attach(new ConcreteElementB());
//创建visitor对象
ConcreteVisitor1 v1 = new ConcreteVisitor1();
ConcreteVisitor2 v2 = new ConcreteVisitor2();
o.Accept(v1);
o.Accept(v2);
Console.ReadKey();
#endregion
运行效果如下:
(二)、雇员节点的访问者模式法例
1、抽象访问者(Visitor)角色:IEmpVisitor
Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyVisitor
{
//抽象访问者(Visitor)角色:
interface IEmpVisitor
{
void Visit(EmpElement element);
}
}
2、具体访问者(ConcreteVisitor)角色:EmpConcreteVisitors
Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyVisitor
{
//此处定义了两个具体访问者(ConcreteVisitor)角色:IncomVisitor具体访问者与VacationVisitor具体访问者
#region IncomVisitor具体访问者
class IncomeVisitor : IEmpVisitor
{
public void Visit(EmpElement 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);
}
}
#endregion
#region VacationVisitor具体访问者
class VacationVisitor : IEmpVisitor
{
public void Visit(EmpElement 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);
}
}
#endregion
}
3、抽象节点(Element)角色:EmpElement
Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyVisitor
{
//抽象节点(Element)角色:
abstract class EmpElement
{
public abstract void Accept(IEmpVisitor visitor);
}
}
4、具体节点(ConcreteElement)角色:EmpBaseElement--EmpConcreteElements
EmpBaseElement
Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyVisitor
{
//定义了具体节点(ConcreteElement)角色的基类,由它会派生出几个不同的具体节点角色
#region Employee
class Employee : EmpElement
{
#region 姓名属性
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
#endregion
#region 收入属性
private double _income;
public double Income
{
get { return _income; }
set { _income = value; }
}
#endregion
#region 假期属性
private int _vacationDays;
public int VacationDays
{
get { return _vacationDays; }
set { _vacationDays = value; }
}
#endregion
#region 构造函数
public Employee(string name, double income, int vacationDays)
{
this._name = name;
this._income = income;
this._vacationDays = vacationDays;
}
#endregion
#region Accept方法
public override void Accept(IEmpVisitor visitor)
{
visitor.Visit(this);
}
#endregion
}
#endregion
}
EmpConcreteElements
Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyVisitor
{
//定义了三个具体节点类型,它们均继承自Employee基类,则Employee基类又继承自IEmpVisitor接口
#region 一般职员
class Clerk : Employee
{ // 构造函数
public Clerk() : base("Hank", 25000.0, 14)
{
}
}
#endregion
#region 部门领导
class Director : Employee
{ // 构造函数
public Director(): base("Elly", 35000.0, 16)
{
}
}
#endregion
#region 总裁
class President : Employee
{ // 构造函数
public President() : base("Dick", 45000.0, 21)
{
}
}
#endregion
}
5、结构对象(ObiectStructure)角色:EmpStructure
Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyVisitor
{
//结构对象(ObiectStructure)角色:
class EmpStructure
{
private List<Employee> _employees = new List<Employee>();
#region 添加节点到结构中
public void Attach(Employee employee)
{
_employees.Add(employee);
}
#endregion
#region 从结构中删除节点
public void Detach(Employee employee)
{
_employees.Remove(employee);
}
#endregion
public void Accept(IEmpVisitor visitor)
{
foreach (Employee e in _employees)
{
e.Accept(visitor);
}
Console.WriteLine();
}
}
}
6、客户应用代码
Code
#region 雇员结构访问示例
Console.WriteLine("-----------雇员结构访问示例---------------");
// Setup employee collection
EmpStructure e = new EmpStructure();
e.Attach(new Clerk());
e.Attach(new Director());
e.Attach(new President());
// Employees are 'visited'
e.Accept(new IncomeVisitor());
e.Accept(new VacationVisitor());
Console.ReadKey();
#endregion
运行效果如下:
总结
1、 在什么情况下应当使用访问者模式
有意思的是,在很多情况下不使用设计模式反而会得到一个较好的设计。换言之,每一个设计模式都有其不应当使用的情况。访问者模式也有其不应当使用的情况,让我们
先看一看访问者模式不应当在什么情况下使用。
倾斜的可扩展性
访问者模式仅应当在被访问的类结构非常稳定的情况下使用。换言之,系统很少出现需要加入新节点的情况。如果出现需要加入新节点的情况,那么就必须在每一个访问对象里加入一个对应于这个新节点的访问操作,而这是对一个系统的大规模修改,因而是违背"开一闭"原则的。
访问者模式允许在节点中加入新的方法,相应的仅仅需要在一个新的访问者类中加入此方法,而不需要在每一个访问者类中都加入此方法。
显然,访问者模式提供了倾斜的可扩展性设计:方法集合的可扩展性和类集合的不可扩展性。换言之,如果系统的数据结构是频繁变化的,则不适合使用访问者模式。
"开一闭"原则和对变化的封装
面向对象的设计原则中最重要的便是所谓的"开一闭"原则。一个软件系统的设计应当尽量做到对扩展开放,对修改关闭。达到这个原则的途径就是遵循"对变化的封装"的原则。这个原则讲的是在进行软件系统的设计时,应当设法找出一个软件系统中会变化的部分,将之封装起来。
很多系统可以按照算法和数据结构分开,也就是说一些对象含有算法,而另一些对象含有数据,接受算法的操作。如果这样的系统有比较稳定的数据结构,又有易于变化的算法的话,使用访问者模式就是比较合适的,因为访问者模式使得算法操作的增加变得容易。
反过来,如果这样一个系统的数据结构对象易于变化,经常要有新的数据对象增加进来的话,就不适合使用访问者模式。因为在访问者模式中增加新的节点很困难,要涉及到在抽象访问者和所有的具体访问者中增加新的方法。
2、 使用访问者模式的优点和缺点
访问者模式有如下的优点:
访问者模式使得增加新的操作变得很容易。如果一些操作依赖于一个复杂的结构对象的话,那么一般而言,增加新的操作会很复杂。而使用访问者模式,增加新的操作就意味着增加一个新的访问者类,因此,变得很容易。
访问者模式将有关的行为集中到一个访问者对象中,而不是分散到一个个的节点类中。
访问者模式可以跨过几个类的等级结构访问属于不同的等级结构的成员类。迭代子只能访问属于同一个类型等级结构的成员对象,而不能访问属于不同等级结构的对象。访问者模式可以做到这一点。
积累状态。每一个单独的访问者对象都集中了相关的行为,从而也就可以在访问的过程中将执行操作的状态积累在自己内部,而不是分散到很多的节点对象中。这是有益于系统维护的优点。
访问者模式有如下的缺点:
增加新的节点类变得很困难。每增加一个新的节点都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作。
破坏封装。访问者模式要求访问者对象访问并调用每一个节点对象的操作,这隐含了一个对所有节点对象的要求:它们必须暴露一些自己的操作和内部状态。不然,访问者的访问就变得没有意义。由于访问者对象自己会积累访问操作所需的状态,从而使这些状态不再存储在节点对象中,这也是破坏封装的。
前往:设计模式学习笔记清单