目录
- 下载 ClassicVisitor 的源代码 – 26.2 KB
- 下载DynamicVisitor的源代码 – 18.5KB
- 下载ReflectiveVisitor的源代码 – 19.5 KB
- 下载ReflectiveExtensionVisitor 的源代码 – 20 KB
- 下载GenericsVisitor的源代码的源代码 – 19KB
介绍
这是一篇描述C#访问者模式的教程文章。目标受众是中级C#程序员及以上。
访客模式是23种GoF模式中最复杂的模式之一。在C#中,它有多个版本。在这里,我们将用五个版本来描述它:
- C#中的访问者模式——第 1 版——经典访问者
- C#中的访问者模式——第 2 版——动态访问者
- C#中的访问者模式——第 3 版——反射访问者
- C#中的访问者模式——第 4 版——反射扩展访问者
- C#中的访问者模式——第 5 版——泛型访问者
我们试图解决的问题
首先,让我们尝试了解我们试图用这种模式解决什么问题,以及经典OO设计的局限性是什么。让我们看一下图1-1和代码1-1中的经典OO设计。
public abstract class Element
{
public int Attribute1 = 0;
public int Attribute2 = 0;
abstract public void V1();
abstract public void V2();
abstract public void V3();
}
public class ElementA : Element
{
public ElementA()
{
}
public int Attribute3 = 0;
public override void V1()
{
}
public override void V2()
{
}
public override void V3()
{
}
}
public class ElementB : Element
{
public ElementB()
{
}
public int Attribute3 = 0;
public override void V1()
{
}
public override void V2()
{
}
public override void V3()
{
}
}
我们在这个解决方案中看到的问题,或者更好地说,限制是:
- 在这种方法中,数据和算法(方法V1、V2等)是耦合的。有时尝试将它们分开可能很有用
- 在不改变现有类结构的情况下,添加新操作(例如V4)并不容易。这与开/关原则相反。希望能够在不改变类结构的情况下添加新的操作(方法)。
- 在同一个地方有不同的方法(例如,V1和V2),它们可以解决完全不同和不相关的功能/问题。例如,V1可以关注生成.pdf,而V2可以关注生成html。这与关注点分离的原则背道而驰。
访问者模式
访问者模式通过将数据和操作划分为单独的类来解决上述问题/限制。数据保存在Element/Elements类中,而操作保存在Visitor/Visitors类中,每个特定的Visitor都可以解决不同的问题。通过创建新Visitor类可以很容易地实现对, Elements的操作的扩展。
此模式的关键部分是设计解决方案,使Visitor对象能够在Element上执行操。 我们说“Visitor访问Element”是对Element进行操作。
如果我们查看类图Picture1-1,我们会看到对于对象 ElementA,我们有方法V1,所以操作调用看起来像:
ElementA elementa=new ElementA();
elementa.V1();
在访问者模式中,使用V1()方法执行的操作将封装在对象Visitor1中,使用V2()方法执行的操作将封装在object Visitor2中,等等。相同的操作调用现在看起来像:
ElementA elementa=new ElementA();
Visitor1 visitor1=new Visitor1();
visitor1.visit(elementa);
情况并没有到此结束。问题是我们将有几个Element和Visitor对象,我们经常通过基类/接口来处理这些对象。然后,出现了调度适当方法的问题。“Dispatch”是找出调用哪个具体方法的问题。
C#与大多数OO语言一样,以虚函数调用的形式支持“单次调度”。也就是所谓的“动态绑定”。根据所讨论对象的类型,在运行时动态地,C#将从虚拟方法表中调用适当的虚拟函数。
但有时,这还不够,需要“多次调度”。多重分派是根据多个对象的运行时类型找到调用哪个具体方法的问题。
C#中的访问者模式——第 1 版——经典访问者
访问者模式的经典访问者版本最常见于文学作品中。在经典版本的访问者模式中,模式基于C#的“双重调度”机制。此解决方案中使用的双重调度机制基于C#的两个特性:
- 基于对象类型动态绑定具体虚方法的能力
- 根据参数类型将重载方法解析为具体方法的能力
以下是示例代码的类图的样子:
这是这个的代码:
public abstract class Element
{
public abstract void Accept(IVisitor visitor);
}
public class ElementA : Element
{
public int Id = 0;
public ElementA(int Id)
{
this.Id = Id;
}
public override void Accept(IVisitor visitor) //(2)
{
visitor.Visit(this);
}
}
public class ElementB : Element
{
public int Id = 0;
public ElementB(int Id)
{
this.Id = Id;
}
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
public interface IVisitor
{
void Visit(ElementA ElemA);
void Visit(ElementB ElemB);
}
public class Visitor1 : IVisitor //(3)
{
public virtual void Visit(ElementA ElemA) //(4)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
}
public virtual void Visit(ElementB ElemB)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
}
}
public class Visitor2 : IVisitor
{
public virtual void Visit(ElementA ElemA)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
}
public virtual void Visit(ElementB ElemB)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
}
}
class Client
{
static void Main(string[] args)
{
//--single element, explicit call-------------------------
ElementA element0 = new ElementA(0);
Visitor1 vis0 = new Visitor1();
vis0.Visit(element0); //(0) works
//--single element, base class call-----------------------
Element element = new ElementA(1);
IVisitor visitor = new Visitor1();
//visitor.Visit(element); //(5) will not compile
element.Accept(visitor); //(1)
//--collection of elements-----------------
List<IVisitor> listVis = new List<IVisitor>();
listVis.Add(new Visitor1());
listVis.Add(new Visitor2());
List<Element> list = new List<Element>();
list.Add(new ElementA(2));
list.Add(new ElementB(3));
list.Add(new ElementA(4));
list.Add(new ElementB(5));
foreach (IVisitor vis in listVis)
foreach (Element elem in list)
{
elem.Accept(vis);
}
Console.ReadLine();
}
}
以下是示例执行的结果:
请注意,在(0)中,当Visitor通过显式类调用时,一切正常。我们说“Visitor访问Element”是对Element进行操作。
但是,在(5)中,当我们尝试通过基类/接口调用visitor时,我们无法编译。编译器无法解析调用哪个方法。这就是为什么我们需要使用“双重调度”来正确解决调用哪个具体方法的所有这些魔法。
在(1)中,我们有适当的调用。正在发生的事情是:
- 在(1)中,我们动态绑定到(2)
- 在(2)中,我们动态绑定到(3)
- 在(2)中,我们对(4)有重载决议
因为在(2)中,我们有双重分辨率,这就是它被称为“双重调度”的原因。
此解决方案的局限性。作为任何解决方案,这将有一些限制/不需要的副作用:
- 类层次结构Elements和Visitor之间有很强的循环依赖性。 如果需要经常更新层次结构,这可能是一个问题。
- 请注意,在(4)中,Visitor要访问Element的数据属性Id,该属性必须是public。这有点打破了封装原则。例如,在我们的第一个解决方案类图Picture1-1 中,方法V1()可以对类Element的private成员进行操作。在C++中,这可以通过使用“friend类”范式来解决,但在C#中并非如此。
C#中的访问者模式——第 2 版——动态访问者
访问者模式的动态访问者版本基于对动态调度的C#支持。这就是语言动态调度的能力,即在运行时做出具体的调用决策。我们将变量转换为“dynamic”,这样,将调度决策推迟到运行时。我们再次有“双重分派”,因为我们是根据两个对象的类型分派到具体方法,只是使用的语言机制不同。
以下是示例代码的类图的样子:
这是这个的代码:
public abstract class AElement
{
}
public class ElementA : AElement
{
public int Id = 0;
public ElementA(int Id)
{
this.Id = Id;
}
}
public class ElementB : AElement
{
public int Id = 0;
public ElementB(int Id)
{
this.Id = Id;
}
}
public interface IVisitor
{
void Visit(ElementA ElemA);
void Visit(ElementB ElemB);
}
public class Visitor1 : IVisitor //(2)
{
public virtual void Visit(ElementA ElemA) //(3)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
}
public virtual void Visit(ElementB ElemB)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
}
}
public class Visitor2 : IVisitor
{
public virtual void Visit(ElementA ElemA)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
}
public virtual void Visit(ElementB ElemB)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
}
}
class Client
{
static void Main(string[] args)
{
//--single element-------------------------
AElement element = new ElementA(1);
IVisitor visitor = new Visitor1();
visitor.Visit((dynamic)element); //(1)
//--collection of elements-----------------
List<IVisitor> listVis = new List<IVisitor>();
listVis.Add(new Visitor1());
listVis.Add(new Visitor2());
List<AElement> list = new List<AElement>();
list.Add(new ElementA(2));
list.Add(new ElementB(3));
list.Add(new ElementA(4));
list.Add(new ElementB(5));
foreach (IVisitor vis in listVis)
foreach (AElement elem in list)
{
vis.Visit((dynamic)elem);
}
Console.ReadLine();
}
}
以下是示例执行的结果:
在(1)中,我们有新的调用。由于动态对象如何工作的性质,解决方案被推迟到运行时。然后,我们首先基于Visitor到(2)的类型进行动态绑定,然后根据在运行时动态发现的Element类型动态解析到(3) 。
此解决方案的局限性
作为任何解决方案,这有一些限制/不需要的副作用:
- 类层次结构Elements和Visitor之间有很强的循环依赖性。如果需要经常更新层次结构,这可能是一个问题。
- 请注意,Visitor要访问Element的数据属性ID ,该属性必须是public。这有点打破了封装原则。例如,在我们的第一个解决方案类图Picture1-1中,方法V1()可以对Element类的private成员进行操作。
- “dynamic”对象的使用给我们带来了性能影响。
C#中的访问者模式——第 3 版——反射访问者
访问者模式的反射访问者版本基于使用C#反射技术在运行时发现对象类型并执行基于发现的类型的显式方法分派。我们再次有“双重分派”,因为我们是根据两个对象的类型分派到具体方法,只是使用的语言机制不同。
以下是示例代码的类图的样子:
这是这个的代码:
public abstract class AElement
{
}
public class ElementA : AElement
{
public int Id = 0;
public ElementA(int Id)
{
this.Id = Id;
}
}
public class ElementB : AElement
{
public int Id = 0;
public ElementB(int Id)
{
this.Id = Id;
}
}
public abstract class AVisitor
{
public void Visit(AElement Elem) //(2)
{
if (Elem is ElementA)
{
Visit((ElementA)Elem);
};
if (Elem is ElementB)
{
Visit((ElementB)Elem);
};
}
public abstract void Visit(ElementA ElemA);
public abstract void Visit(ElementB ElemB);
}
public class Visitor1 : AVisitor
{
public override void Visit(ElementA ElemA) //(3)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
}
public override void Visit(ElementB ElemB)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
}
}
public class Visitor2 : AVisitor
{
public override void Visit(ElementA ElemA)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
}
public override void Visit(ElementB ElemB)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
}
}
class Client
{
static void Main(string[] args)
{
//--single element-------------------------
AElement element = new ElementA(1);
AVisitor visitor = new Visitor1();
visitor.Visit(element); //(1)
//--collection of elements-----------------
List<AVisitor> listVis = new List<AVisitor>();
listVis.Add(new Visitor1());
listVis.Add(new Visitor2());
List<AElement> list = new List<AElement>();
list.Add(new ElementA(2));
list.Add(new ElementB(3));
list.Add(new ElementA(4));
list.Add(new ElementB(5));
foreach (AVisitor vis in listVis)
foreach (AElement elem in list)
{
vis.Visit(elem);
}
Console.ReadLine();
}
}
以下是示例执行的结果:
在(1)中,我们有新的调用。即使在编译时,它也被解析为方法(2)。然后在运行时,使用反射,解析参数类型并将调用传递给(3)。
此解决方案的局限性
作为任何解决方案,这有一些限制/不需要的副作用:
- 类层次结构Elements和Visitor之间有很强的循环依赖性。如果需要经常更新层次结构,这可能是一个问题。
- 请注意,Visitor要访问Element的数据属性Id,该属性必须是public。这有点打破了封装原则。例如,在我们的第一个解决方案类图Picture1-1中,方法V1()可以对Element类的private成员进行操作。
- 请注意,在(2)中,每个继承自AElement的类都被明确提及并检查类型。缺少某些类型可能是实现的问题。一种可能的解决方案是通过使用反射来发现程序集中的所有类型并自动调度到所有继承自AElement。但是,我们不会在这里这样做。
C#中的访问者模式——第 4 版——反射扩展访问者
访问者模式的Reflective-Extension访问者版本基于:1)使用C#反射技术在运行时发现对象类型,并根据发现的类型执行显式方法分派;2)扩展方法的使用。此版本与“Reflective Visitor”版本非常相似,但由于在其他文献中提到过,我们在此也将其列为单独的变体。我们再次有“双重分派”,因为我们是根据两个对象的类型分派到具体方法,只是使用的语言机制不同。
以下是示例代码的类图的样子:
这是此代码:
public abstract class AElement
{
}
public class ElementA : AElement
{
public int Id = 0;
public ElementA(int Id)
{
this.Id = Id;
}
}
public class ElementB : AElement
{
public int Id = 0;
public ElementB(int Id)
{
this.Id = Id;
}
}
public abstract class AVisitor
{
public abstract void Visit(ElementA ElemA);
public abstract void Visit(ElementB ElemB);
}
public static class AVisitorExtensions
{
public static void Visit<T>(this T vis, AElement Elem)
where T : AVisitor //(2)
{
if (Elem is ElementA)
{
vis.Visit((ElementA)Elem); //(3)
};
if (Elem is ElementB)
{
vis.Visit((ElementB)Elem);
};
}
}
public class Visitor1 : AVisitor
{
public override void Visit(ElementA ElemA) //(4)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
}
public override void Visit(ElementB ElemB)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
}
}
public class Visitor2 : AVisitor
{
public override void Visit(ElementA ElemA)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
}
public override void Visit(ElementB ElemB)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
}
}
class Client
{
static void Main(string[] args)
{
//--single element-------------------------
AElement element = new ElementA(1);
AVisitor visitor = new Visitor1();
visitor.Visit(element); //(1)
//--collection of elements-----------------
List<AVisitor> listVis = new List<AVisitor>();
listVis.Add(new Visitor1());
listVis.Add(new Visitor2());
List<AElement> list = new List<AElement>();
list.Add(new ElementA(2));
list.Add(new ElementB(3));
list.Add(new ElementA(4));
list.Add(new ElementB(5));
foreach (AVisitor vis in listVis)
foreach (AElement elem in list)
{
vis.Visit(elem);
}
Console.ReadLine();
}
}
以下是示例执行的结果:
在(1)中,我们有新的调用。即使在编译时,它也被解析为方法(2)。然后在运行时,使用反射,参数类型在(3)中解析,调用传递给(4)。
此解决方案的局限性
作为任何解决方案,这有一些限制/不需要的副作用:
- 类层次结构Elements和Visitor之间有很强的循环依赖性。如果需要经常更新层次结构,这可能是一个问题。
- 请注意,Visitor要访问Element的数据属性Id,该属性必须是public。这有点打破了封装原则。例如,在我们的第一个解决方案类图Picture1-1中,方法V1()可以对Element类的private成员进行操作。
- 请注意,在(2)中,每个继承自AElement的类都被明确提及并检查类型。缺少某些类型可能是实现的问题。一种可能的解决方案是使用反射发现程序集中的所有类型,并自动分派给所有继承自AElement。但是,我们不会在这里这样做。
C# 中的访问者模式——第 5 版——泛型访问者
访问者模式的泛型访问者版本类似于反射访问者模式,因为它依赖于1)反射在运行时动态发现类型;2) C#泛型来指定接口Visitor实现。我们再次有“双重分派”,因为我们是根据两个对象的类型分派到具体方法,只是使用的语言机制不同。
以下是示例代码的类图的样子:
这是此的代码:
public abstract class Element
{
public abstract void Accept(IVisitor visitor);
}
public class ElementA : Element
{
public int Id = 0;
public ElementA(int Id)
{
this.Id = Id;
}
public override void Accept(IVisitor visitor) //(2)
{
if (visitor is IVisitor<ElementA>)
{
((IVisitor<ElementA>)visitor).Visit(this);
}
}
}
public class ElementB : Element
{
public int Id = 0;
public ElementB(int Id)
{
this.Id = Id;
}
public override void Accept(IVisitor visitor)
{
if (visitor is IVisitor<ElementB>)
{
((IVisitor<ElementB>)visitor).Visit(this);
}
}
}
public interface IVisitor { }; // marker interface
public interface IVisitor<TVisitable>
{
void Visit(TVisitable obj);
}
public class Visitor1 : IVisitor,
IVisitor<ElementA>, IVisitor<ElementB>
{
public void Visit(ElementA ElemA) //(3)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
}
public void Visit(ElementB ElemB)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
}
}
public class Visitor2 : IVisitor,
IVisitor<ElementA>, IVisitor<ElementB>
{
public void Visit(ElementA ElemA)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
}
public void Visit(ElementB ElemB)
{
Console.WriteLine("{0} with Id={1} visited by {2}",
ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
}
}
class Client
{
static void Main(string[] args)
{
//--single element, base class call-----------------------
Element element = new ElementA(1);
IVisitor visitor = new Visitor1();
element.Accept(visitor); //(1)
//--collection of elements-----------------
List<IVisitor> listVis = new List<IVisitor>();
listVis.Add(new Visitor1());
listVis.Add(new Visitor2());
List<Element> list = new List<Element>();
list.Add(new ElementA(2));
list.Add(new ElementB(3));
list.Add(new ElementA(4));
list.Add(new ElementB(5));
foreach (IVisitor vis in listVis)
foreach (Element elem in list)
{
elem.Accept(vis);
}
Console.ReadLine();
}
}
以下是示例执行的结果:
在(1)中,我们有一个新的调用。在运行时,它动态绑定到(2)。然后在(2)中,我们使用反射将其显式解析为(3)。
此解决方案的局限性
作为任何解决方案,这有一些限制/不需要的副作用:
- 请注意,Visitor要访问Element的数据属性Id,该属性必须是public。这有点打破了封装原则。例如,在我们的第一个解决方案类图Picture1-1中,方法V1()可以对Element类的private成员进行操作。
结论
首先,我们讨论了我们的动机和我们试图解决的问题。我们正在努力实现的目标很重要,因为解决问题的方法可能不止一种。
然后我们展示了“经典访客”,这是由GoF提出并在文献中经常提到的版本。我认为由于创建它时语言(C++。Smalltalk)的限制,这被提议为唯一和最终的解决方案。
现代OO语言,如C#,具有“动态对象”和“反射”等新功能,可以通过不同的方式实现相同的目标。这在访问者模式的其他四个版本中得到了展示。如果您愿意,可以将它们视为“启用现代C#”的访问者模式的替代版本。
https://www.codeproject.com/Articles/5326263/Visitor-Pattern-in-Csharp-5-Versions