定义:(Visitor Pattern)
封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
换句话说:
访问者模式赋予了【数据】的选择权。
一般而言,我们都是直接通过【数据操作类】操作【数据】。
而通过访问者模式,【数据】可以选择某个【数据操作类】来访问它。
类图:
启示:
现在的互联网时代真是给我们提供了极大的便利。出门不用带现金了,买票不用本人到火车站了,水电费手机上就缴了,网上购物直邮到家了,吃饭也不用下楼了。
慢着,似乎我要跑题了。
这节可是要讲访问者模式,跟互联网有半毛钱关系。
别急关系是硬扯的。
正如六度空间理论,又名六度分隔理论。
你至多只要通过六个人就能认识全世界的任意一个人。
这咋一听是不很玄乎。
举个例子,就像你跟隔壁村的老王扯关系一样,最终还是能扯上点亲戚关系的。
下面我们就开始正二八经的扯吧。
我们就以淘宝购物为例来进行访问者模式的思考。
想一想我们在淘宝下单支付之后,淘宝做了什么?
是不是需要捡货发货?
对于拣货员来说,需要根据订单进行拣货。
对于发货员来说,需要根据订单的收货信息,进行快递发货。
....
就从以上场景来说,针对一张订单,已经有两个不同访问者。
每个访问者访问订单的不同数据,做成不同的操作。
好了,废话不多说,咱们代码见。
代码:
假设淘宝后台有一个订单中心,负责订单相关业务的流转。订单一般上而言主要包括两种,销售订单、退货订单。
根据以上购物场景,我们简单抽象出以下几个对象:
- Product:商品类
- Customer:客户类
- Order:订单类(SaleOrder:销售订单、ReturnOrder:退货订单)
- OrderLine:订单分录类
- Picker:拣货员
- Distributor:发货员
- OrderCenter:订单中心
客户类主要包含简单的个人信息和收货信息:
/// <summary>
/// 客户类
/// </summary>
public class Customer
{
public int Id { get; set; }
public string NickName { get; set; }
public string RealName { get; set; }
public string Phone { get; set; }
public string Address { get; set; }
public string Zip { get; set; }
}
产品类简单包含产品名称、价格信息:
/// <summary>
/// 产品类
/// </summary>
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public virtual decimal Price { get; set; }
}
下面来看看订单相关类:
/// <summary>
/// 订单抽象类
/// </summary>
public abstract class Order
{
public int Id { get; set; }
public Customer Customer { get; set; }
public DateTime CreatorDate { get; set; }
/// <summary>
/// 单据品项
/// </summary>
public List<OrderLine> OrderItems { get; set; }
public abstract void Accept(Visitor visitor);
}
/// <summary>
/// 销售订单
/// </summary>
public class SaleOrder : Order
{
public override void Accept(Visitor visitor)
{
visitor.Visit(this);
}
}
/// <summary>
/// 退货单
/// </summary>
public class ReturnOrder : Order
{
public override void Accept(Visitor visitor)
{
visitor.Visit(this);
}
}
public class OrderLine
{
public int Id { get; set; }
public Product Product { get; set; }
public int Qty { get; set; }
}
其中Order
类定义了一个抽象方法Accept(Visitor visitor);
,子类通过visitor.Visit(this)
直接简单重载。
下面我们来看下访问者角色的定义:
/// <summary>
/// 访问者
/// </summary>
public abstract class Visitor
{
public abstract void Visit(SaleOrder saleOrder);
public abstract void Visit(ReturnOrder returnOrder);
}
其中主要定义了两个抽象Visit
方法,用来分别对SaleOrder
和ReturnOrder
进行处理。
接下来我们就来看看具体的访问者的实现吧:
/// <summary>
/// 捡货员
/// 对销售订单,从仓库捡货。
/// 对退货订单,将收到的货品归放回仓库。
/// </summary>
public class Picker : Visitor
{
public int Id { get; set; }
public string Name { get; set; }
public override void Visit(SaleOrder saleOrder)
{
Console.WriteLine($"开始为销售订单【{saleOrder.Id}】进行销售捡货处理:");
foreach (var item in saleOrder.OrderItems)
{
Console.WriteLine($"【{item.Product.Name}】商品* {item.Qty}");
}
Console.WriteLine($"订单【{saleOrder.Id}】捡货完毕!");
Console.WriteLine("==========================");
}
public override void Visit(ReturnOrder returnOrder)
{
Console.WriteLine($"开始为退货订单【{returnOrder.Id}】进行退货捡货处理:");
foreach (var item in returnOrder.OrderItems)
{
Console.WriteLine($"【{item.Product.Name}】商品* {item.Qty}");
}
Console.WriteLine($"退货订单【{returnOrder.Id}】退货捡货完毕!", returnOrder.Id);
Console.WriteLine("==========================");
}
}
/// <summary>
/// 收发货员
/// 对销售订单,进行发货处理
/// 对退货订单,进行收货处理
/// </summary>
public class Distributor : Visitor
{
public int Id { get; set; }
public string Name { get; set; }
public override void Visit(SaleOrder saleOrder)
{
Console.WriteLine($"开始为销售订单【{saleOrder.Id}】进行发货处理:", saleOrder.Id);
Console.WriteLine($"一共打包{saleOrder.OrderItems.Sum(line => line.Qty)}件商品。");
Console.WriteLine($"收货人:{saleOrder.Customer.RealName}");
Console.WriteLine($"联系电话:{saleOrder.Customer.Phone}");
Console.WriteLine($"收货地址:{saleOrder.Customer.Address}");
Console.WriteLine($"邮政编码:{saleOrder.Customer.Zip}");
Console.WriteLine($"订单【{saleOrder.Id}】发货完毕!" );
Console.WriteLine("==========================");
}
public override void Visit(ReturnOrder returnOrder)
{
Console.WriteLine($"收到来自【{returnOrder.Customer.NickName}】的退货订单【{returnOrder.Id}】,进行退货收货处理:");
foreach (var item in returnOrder.OrderItems)
{
Console.WriteLine($"【{item.Product.Name}】商品* {item.Qty}" );
}
Console.WriteLine($"退货订单【{returnOrder.Id}】收货处理完毕!" );
Console.WriteLine("==========================");
}
}
代码中已经写的够清楚了,我就不多说了。
最后上下我们的订单中心的代码:
/// <summary>
/// 订单中心
/// </summary>
public class OrderCenter : List<Order>
{
public void Accept(Visitor visitor)
{
var iterator = this.GetEnumerator();
while (iterator.MoveNext())
{
iterator.Current.Accept(visitor);
}
}
}
OrderCenter
就是简单的集合类,提供了一个Accept(Visitor visitor)
方法来指定接受哪一种访问者访问。
看看场景类:
static void Main(string[] args)
{
Customer customer = new Customer
{
Id = 1,
NickName = "圣杰",
RealName = "圣杰",
Address = "深圳市南山区",
Phone = "135****9358",
Zip = "518000"
};
Product productA = new Product { Id = 1, Name = "小米5", Price = 1899 };
Product productB = new Product { Id = 2, Name = "小米5手机防爆膜", Price = 29 };
Product productC = new Product { Id = 3, Name = "小米5手机保护套", Price = 69 };
OrderLine line1 = new OrderLine { Id = 1, Product = productA, Qty = 1 };
OrderLine line2 = new OrderLine { Id = 1, Product = productB, Qty = 2 };
OrderLine line3 = new OrderLine { Id = 1, Product = productC, Qty = 3 };
//先买了个小米5和防爆膜
SaleOrder order1 = new SaleOrder { Id = 1, Customer = customer, CreatorDate = DateTime.Now, OrderItems = new List<OrderLine> { line1, line2 } };
//又买了个保护套
SaleOrder order2 = new SaleOrder { Id = 2, Customer = customer, CreatorDate = DateTime.Now, OrderItems = new List<OrderLine> { line3 } };
//把保护套都退了
ReturnOrder returnOrder = new ReturnOrder { Id = 3, Customer = customer, CreatorDate = DateTime.Now, OrderItems = new List<OrderLine> { line3 } };
OrderCenter orderCenter = new OrderCenter { order1, order2, returnOrder };
Picker picker = new Picker { Id = 110, Name = "捡货员110" };
Distributor distributor = new Distributor { Id = 111, Name = "发货货员111" };
//捡货员访问订单中心
orderCenter.Accept(picker);
//发货员访问订单中心
orderCenter.Accept(distributor);
Console.ReadLine();
}
总结:
从上例我们结合访问者模式的通用类图,来理一理主要的几个角色:
- Visitor(抽象访问者)
抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是Visit方法的参数定义哪些对象是可以被访问的。
- ConcreteVisitor(具体访问者)
用来定义访问者访问到具体类的行为。
例子中就是我们的Picker
和Distributor
。我们在捡货员和发货员分别定义了处理销售订单和退货订单的行为。 - Element(抽象元素)
接口或者抽象类,一般通过定义抽象Accept
方法,由子类指定接受哪一种访问者访问。
例子中就是我们的Order
类。 - ConcreteElement(具体元素)
通过调用visitor.Visit(this)
实现父类定义的抽象Accept
方法。
例子中,SaleOrder
和ReturnOrder
就是这样做的。 - ObjectStruture(结构对象)
抽象元素的容器。
例子中对应的是订单中心OrderCenter
维护的一个Order
集合。
优缺点:
- 符合SRP(单一职责原则),具体元素负责数据的存储,访问者负责数据的操作。
- 扩展性好灵活性高,假如我们现在有财务要根据订单来核查财务了。我们只需要实现一个财务的访问者就好了。
- 不符合LKP(迪米特原则),访问者访问的具体元素内容全部暴露给了访问者。比如本例中,捡货员和发货员是没必要知道商品的价格信息的。
- 不符合OCP(开放封闭原则),如果要更改具体的某个元素,可能就需要修改到涉及到的所有访问者。
- 不符合DIP(依赖倒置原则),访问者依赖的是具体的元素而不是抽象元素。这样就会导致扩展访问者比较困难。
应用场景:
- 适用于已确定访问者方法的情况,否则后续更改会需要对访问者进行更改。
- 适用于重构时使用。