美文网首页设计模式 C#程序员.NET
这一次数据说了算,『访问者模式』

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

作者: 圣杰 | 来源:发表于2017-04-17 18:50 被阅读201次

    目录:设计模式之小试牛刀
    源码路径:Github-Design Pattern


    定义:(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(具体访问者)
      用来定义访问者访问到具体类的行为。
      例子中就是我们的PickerDistributor。我们在捡货员和发货员分别定义了处理销售订单和退货订单的行为。
    • Element(抽象元素)
      接口或者抽象类,一般通过定义抽象Accept方法,由子类指定接受哪一种访问者访问。
      例子中就是我们的Order类。
    • ConcreteElement(具体元素)
      通过调用visitor.Visit(this)实现父类定义的抽象Accept方法。
      例子中,SaleOrderReturnOrder就是这样做的。
    • ObjectStruture(结构对象)
      抽象元素的容器。
      例子中对应的是订单中心OrderCenter维护的一个Order集合。

    优缺点:

    • 符合SRP(单一职责原则),具体元素负责数据的存储,访问者负责数据的操作。
    • 扩展性好灵活性高,假如我们现在有财务要根据订单来核查财务了。我们只需要实现一个财务的访问者就好了。
    • 不符合LKP(迪米特原则),访问者访问的具体元素内容全部暴露给了访问者。比如本例中,捡货员和发货员是没必要知道商品的价格信息的。
    • 不符合OCP(开放封闭原则),如果要更改具体的某个元素,可能就需要修改到涉及到的所有访问者。
    • 不符合DIP(依赖倒置原则),访问者依赖的是具体的元素而不是抽象元素。这样就会导致扩展访问者比较困难。

    应用场景:

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

    相关文章

      网友评论

      • 圣杰:经典案例:去医院看病,医生开的药单,对收费员来说,他们只关心价格和数量。
        对捡药员来说,他们只关心药物及数量。

      本文标题:这一次数据说了算,『访问者模式』

      本文链接:https://www.haomeiwen.com/subject/shanattx.html