总目录
前言
在软件构建过程中,由于需求的改变,某些类层次结构中常常需要增加新的行为,如果直接在基类中做这样的更改,将会给子类带来很繁重的变更负担,甚至破坏原有设计。如何在不更改类层次结构的前提下,在运行时根据需要透明地为类层次结构上的各个类动态添加新的操作,从而避免上述问题?这就要使用到本文的访问者模式了!
1 基础介绍
- 定义: 表示一个作用于某对象结构中的各个元素的操作。它可以在不改变各元素的类的前提下定义作用于这些元素的新的操作。
- 将数据操作与数据结构分离,使得同一组操作可以作用于不同的数据结构。
- 访问者模式是用来封装一些施加于某种数据结构之上的操作。它使得可以在不改变元素本身的前提下增加作用于这些元素的新操作,访问者模式的目的是把操作从数据结构中分离出来。
- 解决在稳定数据结构和易变操作之间的耦合问题,使得操作可以独立于数据结构变化。
- 访问者模式中的角色
- 抽象访问者角色(Vistor): 声明一个包括多个访问操作,多个操作针对多个具体节点角色(可以说有多少个具体节点角色就有多少访问操作),使得所有具体访问者必须实现的接口。
- 具体访问者角色(ConcreteVistor):实现抽象访问者角色中所有声明的接口,也可以说是实现对每个具体节点角色的新的操作。
- 抽象节点角色(Element):声明一个接受操作,接受一个访问者对象作为参数,如果有其他参数,可以在这个"接受操作"里在定义相关的参数。
- 具体节点角色(ConcreteElement):实现抽象元素所规定的接受操作。
- 结构对象角色(ObjectStructure):节点的容器,可以包含多个不同类或接口的容器。
2 使用场景
当需要对一个对象结构中的对象执行多种不同的且不相关的操作时,尤其是这些操作需要避免"污染"对象类本身。
- 如果系统有比较稳定的数据结构,而又有易于变化的算法时,此时可以考虑使用访问者模式。因为访问者模式使得算法操作的添加比较容易。
- 如果一组类中,存在着相似的操作,为了避免出现大量重复的代码,可以考虑把重复的操作封装到访问者中。
- 如果一个对象存在着一些与本身对象不相干,或关系比较弱的操作时,为了避免操作污染这个对象,则可以考虑把这些操作封装到访问者对象中。
3 实现方式
1. 实现方式 - 无ObjectStructure角色
实例:为不同形状计算面积和周长。假设我们有不同的几何形状类,比如圆形和矩形。我们希望计算它们的面积和周长,而不改变这些形状类的实现。
定义抽象访问者,声明可以作用于不同形状的操作。
csharp
// 访问者接口,声明访问不同元素的方法
public interface IVisitor
{
// 访问圆形的操作
void Visit(Circle circle);
// 访问矩形的操作
void Visit(Rectangle rectangle);
}
定义具体访问者,实现两个具体的访问者,一个用于计算形状的面积,另一个用于计算形状的周长。
csharp
// 具体访问者:用于计算面积
public class AreaVisitor : IVisitor
{
public void Visit(Circle circle)
{
double area = Math.PI * circle.Radius * circle.Radius;
Console.WriteLine($"圆的面积: {area}");
}
public void Visit(Rectangle rectangle)
{
double area = rectangle.Length * rectangle.Width;
Console.WriteLine($"矩形的面积: {area}");
}
}
// 具体访问者:用于计算周长
public class PerimeterVisitor : IVisitor
{
public void Visit(Circle circle)
{
double perimeter = 2 * Math.PI * circle.Radius;
Console.WriteLine($"圆的周长: {perimeter}");
}
public void Visit(Rectangle rectangle)
{
double perimeter = 2 * (rectangle.Length + rectangle.Width);
Console.WriteLine($"矩形的周长: {perimeter}");
}
}
定义抽象元素/节点,声明接受访问者的方法。所有几何形状类都将实现这个接口。
csharp
// 元素接口,定义接受访问者的方法
public interface IShape
{
// 接受访问者
void Accept(IVisitor visitor);
}
定义具体元素/节点,实现具体的形状类,比如圆形和矩形。这些类都将实现Accept方法,允许访问者访问自己。
csharp
// 圆形类,表示一个具体的形状
public class Circle : IShape
{
public double Radius { get; }
public Circle(double radius)
{
Radius = radius;
}
// 接受访问者,调用访问者的Visit方法
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
// 矩形类,表示一个具体的形状
public class Rectangle : IShape
{
public double Length { get; }
public double Width { get; }
public Rectangle(double length, double width)
{
Length = length;
Width = width;
}
// 接受访问者,调用访问者的Visit方法
public void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
客户端使用
csharp
class Program
{
static void Main(string[] args)
{
// 创建形状对象
IShape circle = new Circle(5);
IShape rectangle = new Rectangle(4, 6);
// 创建访问者对象
IVisitor areaVisitor = new AreaVisitor();
IVisitor perimeterVisitor = new PerimeterVisitor();
// 计算圆形的面积和周长
circle.Accept(areaVisitor);
circle.Accept(perimeterVisitor);
// 计算矩形的面积和周长
rectangle.Accept(areaVisitor);
rectangle.Accept(perimeterVisitor);
}
}
2. 实现方式 - 有ObjectStructure角色
案例:医院看病后,医生开药单,拿着药单先去缴费,然后去取药的过程
csharp
/// <summary>
/// 抽象访问者
/// </summary>
public abstract class Visitor
{
protected string name { get; set; }
public Visitor(string name)
{
this.name = name;
}
public abstract void Visit(MedicineA a);
public abstract void Visit(MedicineB b);
}
csharp
/// <summary>
/// 具体访问者:划价员
/// </summary>
public class Charger :Visitor
{
public Charger(string name) : base(name) { }
public override void Visit(MedicineA a)
{
Console.WriteLine("划价员:"+this.name+"给药"+a.GetName()+"价格:"+a.GetPrice());
}
public override void Visit(MedicineB b)
{
Console.WriteLine("划价员:" + this.name + "给药" + b.GetName() + "价格:" + b.GetPrice());
}
}
csharp
/// <summary>
/// 具体访问者:药房工作者
/// </summary>
public class WorkerOfPharmacy:Visitor
{
public WorkerOfPharmacy(string name) : base(name) { }
public override void Visit(MedicineA a)
{
Console.WriteLine("药房工作者:"+this.name+",拿药:"+a.GetName());
}
public override void Visit(MedicineB b)
{
Console.WriteLine("药房工作者:" + this.name + ",拿药:" + b.GetName());
}
}
csharp
/// <summary>
/// 抽象元素:药
/// </summary>
public abstract class Medicine
{
protected string name { get; set; }
protected double price { get; set; }
public Medicine(string name, double price)
{
this.name = name;
this.price = price;
}
public string GetName()
{
return name;
}
public double GetPrice()
{
return price;
}
public void SetPrice(double price)
{
this.price = price;
}
public abstract void accept(Visitor visitor);
}
csharp
/// <summary>
/// 具体元素:A名称药
/// </summary>
public class MedicineA:Medicine
{
public MedicineA(string name, double price) : base(name, price) { }
public override void accept(Visitor visitor)
{
visitor.visitor(this);
}
}
csharp
/// <summary>
/// 具体元素:B名称药
/// </summary>
public class MedicineB:Medicine
{
public MedicineB(string name, double price) : base(name, price) { }
public override void accept(Visitor visitor)
{
visitor.visitor(this);
}
}
csharp
/// <summary>
/// 具体元素:药单
/// </summary>
public class Presciption
{
private List<Medicine> listmedicine = new List<Medicine>();
public void accpet(Visitor visitor)
{
foreach (var item in listmedicine)
{
item.accept(visitor);
}
}
public void add(Medicine med)
{
listmedicine.Add(med);
}
public void remove(Medicine med)
{
listmedicine.Remove(med);
}
}
csharp
/// <summary>
/// C#设计模式-访问者模式
/// </summary>
class Program
{
static void Main(string[] args)
{
//药类型
Medicine a = new MedicineA("药A", 10);
MedicineB b = new MedicineB("药B", 20);
//药单
Presciption presciption = new Presciption();
presciption.add(a);
presciption.add(b);
Visitor charger = new Charger("张三"); //划价员
Visitor workerOfPharmacy = new WorkerOfPharmacy("李四"); //抓药员
presciption.accpet(charger); //划价
Console.WriteLine();
presciption.accpet(workerOfPharmacy); //抓药
}
}
4 优缺点分析
设计模式本质就是针对编码过程中常见问题的一种解决方案,通俗讲就是一种堵漏洞的方式,但是没有一种设计模式能够堵完所有的漏洞,即使是组合各种设计模式也是一样。每个设计模式都有漏洞,都有它们解决不了的情况或者变化。每一种设计模式都假定了某种变化,也假定了某种不变化。Visitor模式假定的就是操作变化,而Element类层次结构稳定。
优点:
- 访问者模式使得添加新的操作变得容易。如果一些操作依赖于一个复杂的结构对象的话,那么一般而言,添加新的操作会变得很复杂。而使用访问者模式,增加新的操作就意味着添加一个新的访问者类。因此,使得添加新的操作变得容易。
- 访问者模式使得有关的行为操作集中到一个访问者对象中,而不是分散到一个个的元素类中。这点类似与"中介者模式"。
- 访问者模式可以访问属于不同的等级结构的成员对象,而迭代只能访问属于同一个等级结构的成员对象。
缺点:
- 增加新的元素类变得困难。每增加一个新的元素意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中添加相应的具体操作。具体来说,Visitor模式的最大缺点在于扩展类层次结构(增添新的Element子类),会导致Visitor类的改变。因此Visitor模式适用于"Element类层次结构稳定,而其中的操作却经常面临频繁改动"。
结语
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
【C#设计模式-访问者模式】
深入探索 C# 中的访问者模式:让对象结构变得更加灵活
使用 C# 实现23种常见的设计模式
C#设计模式之二十一访问者模式(Visitor Pattern)【行为型】
C#设计模式(22)------访问者模式(Vistor Pattern)
C#设计模式系列:访问者模式(Visitor)