当我们在开发C#程序时,或多或少遇到过对象结构和操作之间互相依赖的情况,比如需要对某一组对象进行相同的操作。而当我们需要添加一个新的操作时,又不希望去修改原本的对象结构,因为这样做很容易引入新的错误,势必会导致系统不稳定。这个时候,我们可以考虑使用访问者模式(Visitor Pattern)来解决这个问题。
什么是Visitor模式
在C#中,访问者模式是一种行为型设计模式,它允许你对不同类型的对象结构中的元素添加新的操作,而不用改变这些元素的类。
访问者模式定义了两个类:Visitor和Element。Visitor是所有访问者的基类。它声明了一些Visit方法,每个方法对应了Element的一个派生类。
在Visitor模式中,元素被声明为抽象的或者基于接口的,该元素包含接受操作的接口,这样所有的元素共享一个accept()方法。
当一个对象结构中的元素被访问时,访问者对象通过调用适当的访问者方法,去做与元素的操作,从而实现了两个对象之间松耦合。
为什么需要Visitor模式
我们来看一个例子:假设我们有一个对象结构,这个对象结构由三个类组成:Employee, Manager和CEO。这其中,Employee是一个基类,Manager和CEO派生于Employee。该对象结构定义好了,我们需要对其中每个对象进行操作。
首先,我们可以在每个Employee的派生类中定义一个方法,来执行员工相应的操作。但是这种情况下我们很难统一维护整个对象结构,因为我们每新增一个派生类的时候,都必须修改已有类,这样会对代码的可扩展性造成负面影响。另外,我们还容易因为修改已有类而引入新的错误。
另一种方式是:我们将操作逻辑提取到一个独立的类Visitor中,每个访问员工对象的方法都被转移到了访问者中。它不会对对象结构造成任何影响,使得在不修改已有代码的情况下,通过添加一个新的操作,我们可以轻易地扩展整个中对象结构。
接下来,我们将使用Visitor模式的实例来更好地说明Visitor模式的作用。
示例1:管理动态类型(动态调用)
来看一个实例,假设我们有一个代码库的API,我们想对代码库中的变量、类型和方法进行动态分析。这里,我们可以使用Reflection.Emit类库动态加载和编译C#脚本,并且使用动态绑定来获取类型和方法,以便我们能够调用代码库中尚未知道的类型和方法。我们需要分别对Type、Method以及Variable进行处理。让我们来看一下对于代码分析的实现。
定义一个抽象的元素Element类,包括以下代码:
abstract class Element
{
public abstract void Accept(Visitor v);
}
每个Element类型都有一个Accept()方法,把自己作为参数传递给它,以便访问者能够进行相应的操作。
实现一个Type类,代码如下:
class TypeElement : Element
{
public override void Accept(Visitor v)
{
v.Visit(this);
}
public string Name { get; set; }
}
Type同样有一个Accept()方法,将自己参数传递给访问者。
为了进一步细化我们的实现,为Method和Variable添加相应的方法。代码如下:
class MethodElement : Element
{
public override void Accept(Visitor v)
{
v.Visit(this);
}
public string Name { get; set; }
}
class VarElement : Element
{
public override void Accept(Visitor v)
{
v.Visit(this);
}
public string Name { get; set; }
}
现在,我们需要编写Visitor类,它包含访问Element和派生类的方法。代码如下:
class Visitor
{
public void Visit(TypeElement t)
{
Console.WriteLine("Type: {0}", t.Name);
}
public void Visit(MethodElement m)
{
Console.WriteLine("Method: {0}", m.Name);
}
public void Visit(VarElement v)
{
Console.WriteLine("Variable: {0}", v.Name);
}
}
现在,我们就可以使用这些类对代码库进行动态分析。假设我们有一个代码库,其中包含以下内容:
class Person
{
public int Age { get; set; }
public void DoSomething()
{
}
}
class Program
{
static void Main(string[] args)
{
var myType = new TypeElement { Name = typeof(Person).FullName };
var myMethod = new MethodElement { Name = typeof(Person).GetMethod("DoSomething").Name };
var myVar = new VarElement { Name = "Age" };
var v = new Visitor();
myType.Accept(v);
myMethod.Accept(v);
myVar.Accept(v);
}
}
我们通过反射获取了Person的Type、Method和Variable,然后依次将他们作为参数传递给访问者。Visitor就会去调用相应的Visit方法,并输出结果:
Type: ConsoleApp1.Person
Method: DoSomething
Variable: Age
示例2:使用Visitor转换XML
在这个例子中,我们将使用访问者模式来将XML文件转换为HTML字符串。XML文件将表示一个库存列表,其中可能包含以下几个节点:Products, Product, Name, Type和Price。
首先,我们定义一个抽象的Element类:
abstract class XmlElement
{
public abstract void Accept(XmlVisitor v);
}
与前面不同的是,这里的Accept方法要传递一个XmlVisitor对象。然后我们为每个节点类型定义类。
class ProductsElement : XmlElement
{
List<ProductElement> products = new List<ProductElement>();
public void Add(ProductElement d)
{
products.Add(d);
}
public override void Accept(XmlVisitor v)
{
v.Visit(this);
}
public IEnumerable<ProductElement> GetProducts()
{
return products;
}
}
class ProductElement : XmlElement
{
public string Name { get; set; }
public string Type { get; set; }
public string Price { get; set; }
public override void Accept(XmlVisitor v)
{
v.Visit(this);
}
}
class NameElement : XmlElement
{
public string Name { get; set; }
public override void Accept(XmlVisitor v)
{
v.Visit(this);
}
}
class TypeElement : XmlElement
{
public string Type { get; set; }
public override void Accept(XmlVisitor v)
{
v.Visit(this);
}
}
class PriceElement : XmlElement
{
public string Price { get; set; }
public override void Accept(XmlVisitor v)
{
v.Visit(this);
}
}
然后创建Visitor:
class XmlVisitor
{
StringBuilder sb;
public string Result { get { return sb.ToString(); } }
public XmlVisitor()
{
sb = new StringBuilder();
}
public void Visit(ProductsElement p)
{
sb.AppendLine("<ul>");
foreach (var d in p.GetProducts())
{
d.Accept(this);
}
sb.AppendLine("</ul>");
}
public void Visit(ProductElement p)
{
sb.AppendLine("<li>");
sb.AppendLine($"<span>{p.Name}</span>");
sb.AppendLine($"<span>{p.Type}</span>");
sb.AppendLine($"<span>{p.Price}</span>");
sb.AppendLine("</li>");
}
public void Visit(NameElement n)
{
sb.AppendLine($"<span>{n.Name}</span>");
}
public void Visit(TypeElement t)
{
sb.AppendLine($"<span>{t.Type}</span>");
}
public void Visit(PriceElement p)
{
sb.AppendLine($"<span>{p.Price}</span>");
}
}
最后,我们创建一个XmlParser类,用于解析XML文件并生成对象结构:
class XmlParser
{
public ProductsElement Parse(string xml)
{
var products = new ProductsElement();//伪代码
return products;
}
}
现在我们就可以像下面这样,正确解析XML并生成HTML了:
string xmlString = @"
<products>
<product>
<name>Product 1</name>
<type>Type 1</type>
<price>10.00</price>
</product>
<product>
<name>Product 2</name>
<type>Type 2</type>
<price>20.00</price>
</product>
</products>";
var parser = new XmlParser();
var products = parser.Parse(xmlString);
var htmlVisitor = new XmlVisitor();
products.Accept(htmlVisitor);
Console.WriteLine(htmlVisitor.Result);
输出结果如下:
<ul>
<li>
<span>Product 1</span>
<span>Type 1</span>
<span>10.00</span>
</li>
<li>
<span>Product 2</span>
<span>Type 2</span>
<span>20.00</span>
</li>
</ul>
总结
访问者模式将操作(即Visitor)从被操作对象中抽离开来,让其自成一体,并根据Element继承结构进行分配。使用访问者模式,我们可以通过派生新的类(具体的访问者),实现对元素结构的新增操作。然而,如果我们在要求解析每个新的XML时,每次都要添加一个新的类,这可能会导致代码的膨胀。
因此,对于一些真正的大型项目,我们可能还要考虑其它的结构化数据访问和序列化技术,例如XPath、XML DOM、JSON序列化等。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:一起详细聊聊C#中的Visitor模式 - Python技术站