Spiga

餐馆的故事-浅析职责链模式

2008-02-28 17:09 by Anders Cui, 2080 visits, 网摘, 收藏, 编辑

我们在餐馆吃饭的时候,一般都是在拿到菜单后,选择喜欢的菜,然后通知服务员。服务员会将我们的定单交给大厨,大厨可能会亲自去做这道菜,也可能安排给小厨来做,总之,我们不用担心他们没有人做菜,即使有时候等的时间长点。

下面我们来分析一下。首先,对于我们这些点菜的人来说,我们一般不了解这些厨师,我们没法找到某个具体的厨师让他去做,所以只好把请求交给服务员;然后,对于餐馆的服务员、大厨、小厨来说,他们都可以接受并处理这个请求,但很明显,他们有分工,不会一人去做所有的菜。

简单地说,顾客发送请求(点菜),餐馆的人接受请求(拿到定单),有多个人可以处理该请求(做菜),或者说履行职责,但最后只有一人处理该请求。如下图所示:

CoR

这当中就包含了职责链模式(Chain Of Responsibility,简称CoR)。

我们来看看Gof中CoR的描述:

意图

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。

从第一个对象开始,链中收到请求的对象要么亲自处理它,要么转发给链中的下一个候选者,请求发送者不确定到底哪个对象会处理它,——我们称该请求有一个隐式的接受者(implicit receiver)。

在餐馆的例子中,顾客是请求的发送者,接受者则是服务员、大厨、小厨,他们构成了一条职责链,他们当中会一个人来处理请求。

好了,菜终于上来了,先把它吃掉吧...

现在走出餐馆,我们来看看在一般场景下,CoR的类图是:

CoR2

参与者

Handler(如Employee)

——定义一个处理请求的接口;

ConcreteHandler(如Server和Chef)

——处理它负责的请求;

——可访问它的后继者(Successor);

——如果可处理该请求,处理之;否则转发给后继者;

Client

——向职责链提交请求。

适用性

1、有多个的对象可以处理一个请求,而具体的处理者在运行时自动确定;

2、希望在对接受者不了解的情况下,向多个对象的一个提交请求;

3、处理请求的对象集合需要动态指定。

示例代码

using System;
 
namespace ChainOfPatterns
{
    class Program
    {
        static void Main(string[] args)
        {
            // 餐馆工作人员
            Server server = new Server("anders");
            Chef chef = new Chef("dudu");
            AssistantChef ac = new AssistantChef("bill");
            server.SetSuccessor(chef);
            chef.SetSuccessor(ac);
 
            Customer customer = new Customer();
            // 点第一道菜
            customer.OrderName = "酸辣土豆丝";
            server.HandleRequest(customer);
 
            // 点第二道菜
            customer.OrderName = "农家小炒肉";
            server.HandleRequest(customer);
 
            Console.ReadLine();
        }
    }
 
    public class Customer
    {
        private string orderName;
 
        public string OrderName
        {
            get { return orderName; }
            set { orderName = value; }
        }
    }
 
    public abstract class Employee
    {
        protected string name;
        protected Employee successor;
 
        public Employee(string name)
        {
            this.name = name;
        }
 
        public void SetSuccessor(Employee successor)
        {
            this.successor = successor;
        }
 
        public virtual void HandleRequest(Customer customer)
        {
            if (successor != null)
            {
                successor.HandleRequest(customer);
            }
        }
    }
 
    /// <summary>
    /// 服务员不用炒菜,所以直接转发给后继者。
    /// </summary>
    public class Server : Employee
    {
        public Server(string name) : base(name)
        {
        }
    }
 
    public class Chef : Employee
    {
        public Chef(string name) : base(name)
        {
        }
 
        public override void HandleRequest(Customer customer)
        {
            if (customer.OrderName == "农家小炒肉")
            {
                Console.WriteLine("{0}做的{1}", name, customer.OrderName);
            }
            else
            {
                base.HandleRequest(customer);
            }
        }
    }
 
    public class AssistantChef : Employee
    {
        public AssistantChef(string name) : base(name)
        {
        }
 
        public override void HandleRequest(Customer customer)
        {
            if (customer.OrderName == "酸辣土豆丝")
            {
                Console.WriteLine("{0}做的{1}", name, customer.OrderName);
            }
            else
            {
                base.HandleRequest(customer);
            }
        }
    }
}

注意

如果我们运气很差,点了一个没有存货的菜,那么不管是服务员还是大厨、小厨都没法处理了。也就是说,对于一个请求,可能没有任何接受者会处理它。

问题

我还想到一个问题,如果一道菜很复杂,需要大厨和小厨一起做,该怎么办呢?望各路高手指点迷津 :)

其它的例子

浏览器事件模型

假设我们的HTML页面上有一个<div />,它又包含了一个<input />按钮,对于按钮的click事件来说,在IE中它的触发顺序为从最特定的事件目标(button)道最不特定的事件目标(document对象)。input,div,body到document,它们也构成了一条职责链。

击鼓传花

击鼓传花是一种热闹而又紧张的饮酒游戏。在酒宴上宾客依次坐定位置,由一人击鼓,击鼓的地方与传花的地方是分开的,以示公正。开始击鼓时,花束就开始依次传递,鼓声一落,如果花束在某人手中,则该人就得饮酒。

看起来,酒宴上的宾客构成了一条链。但我认为这是CoR的一个反面例子。对于每一轮具体的游戏来说,这些宾客唯一能做的事情就是将花束传递(转发给后继者),没有谁能够主动停下来,而能够处理请求的人(击鼓者)到底是谁的后继者呢?这个并不确定,职责链如何构建呢?

另外,建议看一看下面两篇文章中高手的论述:职责链模式在开发中的应用手拉手就是职责链吗?

 

参考:

《设计模式-可复用面向对象软件的基础》 Gof

《UML基础、案例与应用》Joseph Schmuller

http://www.dofactory.com/Patterns/PatternChain.aspx

作者:Anders Cui
出处:http://anderslly.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
0
0
(请您对文章做出评价)
« 上一篇:微软代码共享网站MSDN Code Gallery上线(转)
» 下一篇:C#中的预处理器指令
Add your comment

21 条回复

  1. #1楼 pk的眼泪      2008-02-28 17:36
    有点牵强.
      回复  引用  查看    
  2. #2楼 Autumoon      2008-02-28 19:51
    我觉得楼主举的例子用Command模式更合适。
      回复  引用  查看    
  3. #3楼 梁逸晨      2008-02-28 20:34
    你的世界地图的访问量好大啊,连非洲的都有那么多
      回复  引用  查看    
  4. #4楼[楼主] Anders Cui      2008-02-28 20:40
    @pk的眼泪
    可否细解?
      回复  引用  查看    
  5. #5楼[楼主] Anders Cui      2008-02-28 20:42
    @梁逸晨
    呵呵,其实很少啊
    看看国外的大牛们的就知道了
      回复  引用  查看    
  6. #6楼[楼主] Anders Cui      2008-02-28 21:18
    @Autumoon
    两者适用的场景不同
    职责链表示一条对象的链,链上的每个对象都有机会处理一个请求。
    而命令模式允许系统使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
      回复  引用  查看    
  7. #7楼 未登录的怪怪[未注册用户]2008-02-29 01:51
    ...你这个例子比较接近职责链所要解决的模型了。不过估计你没有看我补充那篇文章。 你这个例子里,和张兄的问题,有一个同样的特征, 即:

    if (customer.OrderName == "酸辣土豆丝")
    {
    Console.WriteLine("{0}做的{1}", name, Customer.OrderName);

    }
    else
    {
    base.HandleRequest(customer);
    }

    这就存在着信息->处理者一一对应的关系。如果非要成链, 当然可以成链,然后说这是一个链模型。

    这句话:链上的每个对象都有机会处理一个请求。 DP95的翻译版确实这么说的, 原版没看过。 但是这话存在着严重的误理解。我们应该这样看待问题的实质:

    AssistantChef既然只会做“酸辣土豆丝", 那么他根本就没有机会响应“鱼香肉丝”, "宫保鸡丁"等请求。所以对一个客户的请求(这时不应该分辨请求详细内容), 说他是有机会, 是不完备的。所以职责连就是不恰当的。

    但是你这个例子, 实际上非常容易变成一个真正的职责链:

    无论是谁, 都可以有如下逻辑:

    if (!this.IamBusy)
    {
    if(材料.齐全) {
    Console.WriteLine("{0}做的{1}", name, Customer.OrderName);
    }
    else {
    base.告诉服务员向顾客道歉。
    }
    }
    else
    {
    base.HandleRequest(customer);
    }

    当然,这个例子可以扩展的更复杂,以至于厨师确实有会做, 不会做的特征。 但是, 我们要注意的是, 同一级别的厨师, 不见得全都会做同一种菜。 比如A会做123, B会做246,但都是同一级别的(类相同); 在初始化时, 可以人为的设定状态, 比如: Set会做的菜(InfoMap),然后对象自身CanICook(Info)?

    对于不同级别的厨师, 或者不同职责的人物, 甚至可以不会做菜, 但他要能响应顾客任何一个需求: 比如服务员对顾客任何一个需求, 都会说“抱歉”, 而保安则会给顾客饱以老拳。

    但是, 保安是否给顾客老拳, 是由是否传递到它身上而定的, 而是否传递到它身上, 是由*对象*而不是类型或传入信息定的。 职责链, 其实更正确的翻译我觉得是响应链, 由这个设计模式来帮助建立一个快速找到哪个对象响应的结构。而不是传递一个信息直到有一个类型能够响应为止。我在那两篇文章里提到一个极端情况:职责链里的全部对象,甚至可以是一个类型的。

    既然你也翻了设计模式, 你会注意到, 它的例子是在构造函数中, 通过设定Topic, 决定自身状态, 从而决定是否响应的。这和我后面这个Set的例子相似。 而我第一个例子, 则可能涉及到一些与Busy相关的逻辑。

    这都是很灵活的, 但核心原则是一致的。 对特殊到类型->信息一一对应的情况, 真的是不该使用职责链的。一个待建立模型的对象, 不是它*可以*成链, 就能够发挥职责链的作用‘ 职责连真正的作用就那么几句话, 但特别难把握,牵扯到一些比设计模式,甚至比面向对象更基本的理解; 这些还有其它一些可能的疑问,我在那两篇文章中有过详解, 就不赘述了。

    老哥你和我臭味相投, 但是我这人比较爱较真, 误怪。我不敢说我一定是对的, 但是如果你打算与我讨论这个问题, 最好把我那两篇文章看到确认了解了我到底JJWW了些什么为止。 确实我写的太差, 不太容易理解, 但是如果你把我那些生涩的句子都忽略了, 我和你讨论时也只能再重复, 效率就太低了。

    最后还是回到职责链上,还是我上次说的, 职责链等几个模式, 有着强烈的针对性; 绝不是成链就是职责链的, 也不是能按链的方式去理解建模, 按链的方式就是最好的。

    另外, 我不太赞成为了解释这些手法, 人为的制造例子, 因为咱们制造例子的过程, 会严重受到想往上靠的诱惑, 自己还不觉得, 我是天天的犯这种错, 所以对这个问题相当敏感 ^^

    另外B4一下GoF, 这章写的真差, 差到一定地步了。
      回复  引用    
  8. #8楼 未登录的怪怪[未注册用户]2008-02-29 02:06
    职责链这个翻译也很恶, 我靠。

    响应链, 响应链, 响应链, 挂满了对象的响应链。

    每一个对象必须可以响应任何情况的传入请求。

    对象类型可以相同, 也可以不同。

    是否响应, 由对象自己的状态, 而不是类别和传入信息或它们的对应关系决定。

    记住这几点, 响应链的形象就会更清晰了。我怎么这么大舌头,就是说不清呢?急死我了。
      回复  引用    
  9. #9楼 怪怪      2008-02-29 02:51
    第一个回复中:

    "无论是谁, 都可以有如下逻辑"

    "谁", 指的是同一个类型的不同对象, 比如"小厨A", "小厨B", "小厨C", 比如这些小厨全IsBusy, 就交给大厨, 如果大厨也IsBusy, 就让服务员去安抚客人.

    所以你看到了, 如果是这样的规则, 服务员反而不在响应链头端, 而在尾端.

    如果这个餐厅换了规则, 比如大厨先做, 大厨忙, 才一个一个轮到小厨, 规则就变了. 还可以: 大厨根本不会做菜, 但是会指挥, 会见客户, 有国家一级厨师证等等特殊技能, 但是照样也可以挂在职责链上. 以他自身的特殊响应方式回复任何一种请求, 只要确实其它*对象*因为*自身逻辑*(比如忙, 比如不会做, 比如尿尿去了), 没有截获该请求.

    但是无论哪个规则, 如果想像一个多线程环境, 就如真正的饭馆, 你就可以理解: 每个*对象*都有机会(见上面如何理解"有机会"一段)响应了.

    直接在某一个厨师类别里, 写死代码判断是不是能做某种菜, 其实并不是真正的"对象"的响应, 仅仅是为了利用接口, 有关这个我在<<补充说明>>一文中也有交代. 另外, 请注意GoF所谓的对象行为模式中对*对象*的强调 -- 我指的不是响应链(我以后绝对不说职责链这俩字), 而是所有对象行为模式. 这对理解面向对象的一些用法, 有非常大的好处.

    我打住了, 又越说越多了, 唉唉, 我什么时候能跟各位一样, 两句话把事说清楚啊.
      回复  引用  查看    
  10. #10楼[楼主] Anders Cui      2008-02-29 09:43
    --引用--------------------------------------------------
    未登录的怪怪: 职责链这个翻译也很恶, 我靠。
    响应链, 响应链, 响应链, 挂满了对象的响应链。
    每一个对象必须可以响应任何情况的传入请求。
    对象类型可以相同, 也可以不同。
    是否响应, 由对象自己的状态, 而不是类别和传入信息或它们的对应关系决定。
    记住这几点, 响应链的形象就会更清晰了。我怎么这么大舌头,就是说不清呢?急死我了。
    --------------------------------------------------------
    这段话说得很清楚了 :)
    你的文章我看了三遍了,感觉能理解个大概了。

    我昨天下午在写的时候,也想到过不该使用if/else这样的结构来对接受者进行选择,这样看起来就跟张兄的有点类似了,但一时想不到好的做法,所以希望有人指点迷津,老兄你给出了 :)
      回复  引用  查看    
  11. #11楼[楼主] Anders Cui      2008-02-29 09:51
    --引用--------------------------------------------------
    怪怪: 第一个回复中:
    我打住了, 又越说越多了, 唉唉, 我什么时候能跟各位一样, 两句话把事说清楚啊.
    --------------------------------------------------------
    我想这主要是你的思路很开阔吧,一个餐馆可以引申出那么多故事。
      回复  引用  查看    
  12. #12楼 金色海洋(jyk)      2008-02-29 12:52
    1、关于厨师做菜的问题
    一般饭店有厨师和水案,简单说呢,水案是切菜切肉的,厨师只管炒菜,不会负责切菜的。当然这一点可以忽略不记,我们要讨论的是“响应链”,而不是厨房到底怎么做菜。

    2、顾客点的是菜,一般不会顾忌是那个厨师做的,那么是不是可以用菜做为主线呢?

    我想了一下没想出来,也许根本就不行吧。


    瞎说两句,说错勿怪。
      回复  引用  查看    
  13. #13楼 bmrxntfj      2008-02-29 13:06
    厨师的责任就是炒菜。但不是菜单来了我就一定得炒。我也可以不炒,因为我有情绪(工资不涨啊)。
    怪怪的响应链 名字不错。
      回复  引用  查看    
  14. #14楼[楼主] Anders Cui      2008-02-29 20:53
    @金色海洋(jyk)
    同意你的说法 :)
      回复  引用  查看    
  15. #15楼[楼主] Anders Cui      2008-02-29 20:54
    @bmrxntfj
    是否响应, 由对象自己的状态, 而不是类别和传入信息或它们的对应关系决定。这句话还要好好体会
      回复  引用  查看    
  16. #16楼 金色海洋(jyk)      2008-03-01 13:08
    如果说到如何做菜呢,想来想去,发现自己想到了ERP上去了。

    比如,做一道菜需要哪些原材料,需要多少人来加工,需要什么工具(锅碗瓢盆),需要什么炉具,厨房有哪些炉具,有哪些人员(厨师、水案等),都有什么能力(能作什么及效率),做一道菜需要哪些工序。这不就是ERP了吗?

    跑题了,所以上次回复就没有说。

    我学习面向对象最郁闷的地方就是:

    if (customer.OrderName == "农家小炒肉") { Console.WriteLine("{0}做的{1}", name, customer.OrderName); }

    像这样的代码写起来容易,实现起来难。写这样的一句才救出来了吗?不会这么简单的。

    (注意我不是在抬杠,再说我的理解过程)。

    后来才发现,不能这么想,这么想就永远理解不了面向对象。


    虽然知道了,但是在实际工作中怎么应用呢?和自己写过的代码对照了一下,没有发现类似的呀,怎么理解?怎么应用呢?

    真的希望谁能够举一个实际工作中能遇得到的例子。

    像这样的例子不是说不好,而是说不好理解,不好应用到实际的编码中(尤其对于初学者),更不容易扩展,一扩展那范围就广了,什么都能够带得出来。





      回复  引用  查看    
  17. #17楼[楼主] Anders Cui      2008-03-02 08:49
    @金色海洋(jyk)
    你说的是如何做菜
    而本文说的是由谁做菜
    这应当属于我们编程中“抽象”的不同程度和层次吧

    就像盖房子一样,
    我们最开始想的应该是房子、庭院等的整体关系
    而不是上来就想厨房的灶台如何实现吧?

    你后面说得有道理
    我以后要在工作中多注意一下
    把其中用到的模式抽取出来
    这样的价值要比餐馆的例子好很多
      回复  引用  查看    
  18. #18楼 没登录的怪怪[未注册用户]2008-03-03 04:22
    @金色海洋(jyk)
    其实关键是两点:

    1. 如何正确认识事物,这个,需要在生活和编程中广泛的锻炼自己的思维方式,比如对同一件事左看右看上看下看。

    2. 如何让实现又轻巧又简单, 想做到朴实无华,就要时时刻刻关注这个问题;但是不要过度追求,而是把握平衡点。

    多视角检视和不要过度的问题并不是传统意义上理解的找寻一个合适的中庸之道,而是比如关于2这个问题,过了某一个度以后,你越追求简单,在一些层面它反而就越复杂。
      回复  引用    
  19. #19楼 没登录的怪怪[未注册用户]2008-03-03 04:36
    上面指的关键点是指作出良好设计的关键点~, 不是面向对象的关键点。我最近在找寻不用实现一个新的语言,就可以面向上下文的编程和设计方式。

    我有一种感觉,一切最终会产生不合理的根源在于:Object->人->厨师, 这种看法很可能在根本上是错误的。

    厨师就是厨师,无论它一台智能化烤面包机,还是一个活人,或者象《料理鼠王》里一样的一群老鼠。

    我理想的编程范式, 应该是“管中窥豹”形式的;这样能隔绝掉大多数复杂度,也要比面向对象容易学习和理解。对于一个存在,你只有给了它正确的环境,比如厨房,比如女朋友的床上,它才会显现出它的能力。没有这个环境,这些能力对于这个场景就从绝对意义上是根本没有的(而不是隐藏起来,通过接口暴露)。

    现在的动态脚本语言也不是这样的:比如js的prototype,你扩展了,就是扩展了;况且动态语言没有我想要的严格检查:越是灵活,其实就越需要这样的拐棍。

    Extension Method倒是有点像,可惜它不是真正的上下文式的内涵,而只是个看起来类似的语法糖豆;也不能通过技巧转化为真正意义上的面向场景(既上下文)的编程方式。

    其实CLR和DLR应该给我提供了足够的空间去创造一门语言,只是我水平还不够~ 而且没有什么亚洲研究院这样的环境,吃饭成问题也支撑不下去。

    嗯又说多了,只当随便聊聊吧,正经写说不准还被很多人当SB了... 反正对我来说,破除迷信是很重要的一件事。对现代编程语言和实践方式知道的越多,就越感觉不是那么回事了。
      回复  引用    
  20. #20楼[楼主] Anders Cui      2008-03-04 12:14
    @没登录的怪怪
    期待你能够研究出更为理想的开发语言
    毕竟大多数人都只是在使用语言
      回复  引用  查看    
  21. #21楼 飞林沙      2008-10-05 06:37
    我个人认为这个例子举的有些牵强,而且在职责链模式的最后一般要加上一个兜底的条款。也就是说某条件,之前所有的人都不能处理时,那最后一个继任者应该怎么做。
    而且,我想请教一下楼主,您觉得职责链模式和状态模式的差别在哪呢?一直都没想明白?
    虽然说两者之间的用途不大一样,但是我认为他们的结构是差不多的,都是来设置她的继任者,然后在处理当前状态(或职责),希望指教。
      回复  引用  查看