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

目录:设计模式之小试牛刀
源码路径: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(依赖倒置原则),访问者依赖的是具体的元素而不是抽象元素。这样就会导致扩展访问者比较困难。

应用场景:

  • 适用于已确定访问者方法的情况,否则后续更改会需要对访问者进行更改。
  • 适用于重构时使用。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容