zl程序教程

您现在的位置是:首页 >  .Net

当前栏目

这一次数据说了算,『访问者模式』

2023-02-18 16:47:11 时间

定义:(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方法,用来分别对SaleOrderReturnOrder进行处理。

接下来我们就来看看具体的访问者的实现吧:

/// <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(依赖倒置原则),访问者依赖的是具体的元素而不是抽象元素。这样就会导致扩展访问者比较困难。

应用场景:

  • 适用于已确定访问者方法的情况,否则后续更改会需要对访问者进行更改。
  • 适用于重构时使用。

源码:

源代码C# GitHub

系列导航:

设计模式之小试牛刀