设计模式(01)责任链模式和中介者模式
从我最熟悉的设计模式写起。英文是chain of responsbility,常常被译为责任链模式。
在1980年代,报纸上常见的一个批评政府的词汇叫"踢皮球"。当下看来,从社会主义社会转型到资本主义社会,这当中有诸多的未定义案例出现,导致了政府部门互相推诿的现象。在软件设计中,有时候也会有类似的案例,就是不知道一个事件/数据该由哪个对象来处理,或者是需要多个对象来依次处理,就出现了责任链模式。
责任链与踢皮球的不同之出在于,责任链不可以把球回传。责任链应该是一个单向可走通的链,允许中间退出,而不允许往回走导致死循环。
前面说过,设计模式的思想不限于面向对象设计。。。那么先举一个PC机上PCI总线的中断共享的例子。我还记忆犹新,当年上大学的时候,用着导师的一款386机器,很不错的,装了ISA网卡,在DOS下上网。后来想在那台机器上玩大富翁3, 找了个ISA声卡,费了九牛二虎之力才装上啊,记得是把网卡声卡的中断全改了,还改了DOS的config文件。。。
为什么装个声卡会这么痛苦?原因么,当然是因为ISA总线是不支持PnP的,共享内存地址和IO口地址都要手工指定,中断也不可以冲突。。。其它不深入了,总之后来的PCI总线就支持中断共享了。按照一般人的想法,一个PCI系统,当中断发生时,系统应该可以通过中断号、中断发生的PCI槽号来找到对应的驱动程序(别忘了多块PCI卡可以共享一个中断号),调用中断回调函数。--可是,令人发指的是,由于历史原因,主要是中断控制器的设计的一些兼容性原因(PCI和ISA是要共存的),当中断发生时系统没有办法知道是哪个槽中的PCI卡发出了中断,系统只能得到一个中断号。。。
那么我们来看一下WindowsNT下的驱动程序是怎么处理的(Linux下其实也类似)。首先,计算机开机。上电的时候系统能够识别那个插槽插了什么卡,得到它的厂商ID和板卡型号ID,并写入注册表。系统也会得到它所期望的资源类型和大小,并分配一块物理资源。Windows会根据厂商ID和板卡ID, 去寻找已经在注册表里注册的驱动程序,并使用驱动里的AddDevice功能增加一个实例。这个AddDevice中间包括了往PCI卡的配置空间写资源和向Windows注册资源的,包括注册中断号和中断回调函数的过程。这一点Windows做得很绕,先是分一块资源,把这些资源告诉PCI驱动,然后驱动再自己去注册这些资源。到了这一步,Windows内存里就有了以下模块(假设有三个板卡使用中断号17, 顺便说,很多主板都不完整支持PCI规范,也不支持中断号16~31),结构图是这样的
当中断发生时,通信图是这样的。这里画了一个最糟糕情况,当a收到中断请求包(IRP)时,它去查自己的板卡的寄存器或者共享空间,判断是否自己发出了中断,如果是自己的,立即擦除板卡上的标志,并且改变IRP的状态。题外话,因为中断是不可重入的,在这里做太多事情会影响多线程系统的可协作性,那些响应中断的活应该调用驱动里的DPC(推迟过程调用)来实现。中断服务程序应该在改变IRP状态字后立即返回。Windows核心会判断IRP的状态字,决定是否需要把这个IRP传递给下一个中断服务函数。所以责任链并不一定要调到到链上的所有元素,它是很有可能中间就完成任务。
如果觉得上面的例子太过晦涩,请看第二个例子。最近我需要做一些文件的文字处理。我有一个C++写的处理程序,叫Process。可耻的是,Process的功能不够强,有一些特殊的情况处理不了,所以我用一个perl脚本pre-processor.pl去预处理一下,把输入文件规整成可以被Proess的格式。再可耻的是,Process的处理功能还是不够强,出来的文件某些地方不符合我的需求,于是我再写了一个脚本叫post-processor.pl的来后处理一下。最后,我不愿意手工做这些事情:去调用、判断中间结果是否生成,另外,我还需要输入一个目录下的所有文件逐个处理,所以我写了一个shell脚本来做这个事情。
怎么样,我把文件画成了圆圈,很生动的踢球过程吧。上图只画了最佳结果,没考虑处理失败的情况,程序需要处理的。
最后,作为面向对象的责任链模式,有什么特征呢?很容易想到,所有的责任处理模块都有相同的输入和输出,所以这些模块作为类,应该有共同的基类。这是四人帮的书里举的例子,里面有一个Handler基类和两个派生类
这个例子,让责任处理类互相知道彼此的存在,一个责任处理类处理过输入之后,再传递给successor里所存的下一个责任处理类实例。这样做的一个坏处是,责任处理类必须知道谁是它的下一个负责者(successor),而且也很难在运行期改变这个链的结构。
我个人更喜欢有一个额外的中介类来存储责任链链表,这个类里面有一个链表存储所有的handler, 有一个接口接受client输入,有方法响应外部从链表里增加或删除Handler。这样具有更多的灵活性,比如可以方便地修改责任链;甚至是取消一个已经进入责任链的请求。更复杂一点的来说,当handler的处理速度有可能瞬时慢于client的请求速度时,可能还需要实现一个buffer来存放请求(下图没有画出这种情况,而是假设client自己处理了这种情况)。
有没有觉得上图我的做法(不考虑责任链的功能)也许可以归纳出另外一种模式?没错,看四人帮对Mediator( 中介者)设计模式的定义:"用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。"--所以上图是中介者模式和责任链模式的一个混合。
总结:(1)当有一个请求需要处理,但发送方不知道应该由谁处理的情况,可以使用责任链模式。责任链降低了请求发送类和多个处理类之间的耦合度。
(2) 当你有很多类来协同处理一个事情时,考虑用一个中介者来掩盖各对象之间的耦合。
分类
Software引用通告(0)
被引用的日记: 设计模式(01)责任链模式和中介者模式。
TrackBack URL for this entry: http://www.debagua.net/cgi-bin/mt4/mt-tb.cgi/196
如果您想引用这篇日记到您的Blog,
请复制上面的链接,放置到您发表文章时的相应界面中。






发表评论