模式使用详解 -- 手拉手就是职责链吗?

模式到今天已经20年历史了, 尤其是GoF模式, 更广泛被大家熟知, 这些模式就像武侠小说里的招式, 什么时候该使用什么招式, 师傅们只能泛泛而谈, 正确的判断必须由我自己的做出. 以后会介绍些自己的浅薄经验, 并和大家讨论, 只是不知道如何写起; 无论你是存在问题, 还是已经有了自己的答案, 都可以给我留言; 对于前者我不见得会马上回复, 但是对我如何去写有很大启发.

在这里先整理一些有疑问的使用方式, 作为未来的素材.

职责链使用情景一: 原帖

在表达我的想法之前, 我们先优化一下作者原来的代码中如下部分, 虽然这些优化中有一些对于真正的职责链模式是错误的, 而在这情况下使用链表是存在很大疑问的, 但是我们如果不拘泥于职责链称号, 也不考虑使用链表的合理性, 则这些优化可以当作一次重构的展示:

对象一:
if (变量 == "信息一") 某操作 else if (存在下一个对象) 下一对象.执行() else 默认操作
对象二:
if (变量 == "信息二") 某操作 else if (存在下一个对象) 下一对象.执行() else 默认操作
...
...

这是什么? 这就是重复, 这种重复如果混进代码里往往没有那么明显; 先把最明显的部分搞掉吧:

    public class BasicHttpBindingConstraint:BindingConstraint
    
{
        
public override bool Constraint(ServiceEndpoint endpoint)
        
{
            
if (endpoint.Binding.Name == "BasicHttpBinding")
            
{
                
//原操作
            }

           
return base.
Constraint(endpoint);
        }

    }

判断是否有下一个等等写入基类, 还是原来那些代码. 这样重构的好处除了消除了代码重复, 原文中两个protected的字段, 也不用再向实现类暴露, 做到了更好的信息隐藏. 但是真的没有重复了吗? 看看这句: if (endpoint.Binding.Name == "BasicHttpBinding"), 其中变化的只有引号内部的部分, 其余的还是重复. 同时, 每一个实现类, 虽然不用写else了但是base.Constraint(endpoint)却得Copy&Paste一遍.

接下来我们这么干一下基类:

    public abstract class BindingConstraint//:IEndpointConstraint
    {
        
#region protected fields
        
//只要是field, 最好连protected也不要.
        #endregion

        
private BindingConstraint m_bindingConstraint; //与原来相比, private做到了更好的信息隐藏
        private bool m_hasNextConstraint = false;        

        
#region public methods

        
public void AddConstraint(BindingConstraint constraint)
        {
            m_bindingConstraint 
= constraint;
            m_hasNextConstraint 
= true;
        }

        
public bool Constraint(ServiceEndpoint endpoint) //注意, 不再abstract了
        {
            
if (BindingConstraintName == endpoint.Binding.Name)
            {
                
return DoConstraint(endpoint);//注意, 这么优化是不对的, 见后面关于职责链的讨论
            }
            
else
            {
                
if (m_hasNextConstraint)
                {
                    
return m_bindingConstraint.Constraint(endpoint);
                }
                
else
                {
                    
return false;
                }
            }
        }               

        
#endregion        

        
#region public abstract methods
        
//新增了两个
        protected abstract bool DoConstraint(ServiceEndpoint endpoint); 
        
protected abstract String BindingConstraintName
        {
            
get;
        }

        
#endregion

    }

根据传说中的单一职责, 作为实现的子类, 无需再负责链表的相关工作, 只要管自己干嘛, 并提供自己负责事情的代号, 就可以了. 但是我要说明的是, GoF的代码例子, 是我和当前例子的作者的那个中间值: 第一次优化后那个结果; 为什么呢?

第一次优化后, base.Constraint(endpoint)这个, 还可以不执行, 这样我们就有了一个机会, 定义默认操作, 甚至将职责链原本的链条改变, 引导到另外一个职责链上去. 请大家注意这个事实: 使用base.Constraint(endpoint)的方法, 增加了重复, 但换到了灵活性; 也就是说, 只要是损失的, 必须换来其它的东西, 否则回家咱老婆也不干啊不是.

只是针对这个例子, 把去优化它两次没啥问题, 不过仍然可以看作吃饱了撑的没事干, 唯一的作用就是顺便就问这么一个事情:

信息隐藏完美, 没有重复, 使用优雅, 对扩展开放, 一切符合教科书上各种原则的代码. 就一定是正确的代码吗?

该文作者使用链表作设计, 无可厚非, 毕竟, 他达到了他想要的目的; 但是其实这种用法, 存在着一个的问题: 链表上每一个对象, 都要保存着上一个对象的引用, 从接到请求, 到有一个对象响应, 中间有多少个对象, 就有多少个废开销. 有开销没有问题, 问题是开销换来的是什么? 该文作者总结到:

引用(错误!见后)使用职责链模式必须恰到好处,否则会成为模式滥用的反面教材。根据我对职责链模式的理解,可以认定只要同时符合下列三个条件,就可以引入职责链模式:
1、当一个方法的传入参数将成为分支语句的判断条件时;
2、当每一个分支的职责相对独立,且逻辑较为复杂时;
3、当分支条件存在扩展的可能时。

我的看法是, 基本上打到靶子外去了. 那些可能产生的开销, 其实是白白牺牲了还被扣了个"废"的帽子; 勤奋工作的CPU, 被我一个初学者硬生生的钉上了毛主席"贪污和浪费, 是极大的犯罪"的历史的耻辱柱. 我们这次付出, 不像上面GoF的老大们选择的不优化, 反而像谈恋爱, 大把的白花花的银子花出去, 最后只等到一张好人卡~

比如用Strategy模式实现, 照样可以得到这些好处. 比如该文作者的例子:

1. 创建一个类作为执行者, 执行者中持有一个保寸字符串和IBindingConstraint的字典(比如Dictionary, HashTable);
2. 这个字典的初始化, 既可以用原来Add的方式, 也可以通过一个配置文件初始化;
3. 实现一个Execute, 根据endpoint.Binding.Name从字典取出IBindingConstraint, 并执行Constraint;
4. 使用时调用: 推荐者.Execute(endpoint).

很显然, 这样的做法, 不但有更小的开销, 也有更少的代码, 同时拥有更合理的结构, 只是看起来不那么炫. 在这里我要隆重的提醒大家注意使用字典, 这种方式虽然不入GoF 23种模式大雅之堂, 却是n多大牛/大嘴推荐的万灵丹. 对字典的使用也有一个广泛学名, 叫做"表模式"(还是表驱动来着? 记性不好...). 当然就针对当前的讨论所针对的问题来说, 我们也可以把它理解为策略模式的一个应用.

为什么说这种方式更合理呢? 让我们来看一下原文作者所说的三条, 其模型到底是什么样的:

信息1 -> 过程或对象一
信息2 -> 过程或对象二
....

SELECT 对象 FROM 某处 WHERE 信息 = @变量, 查表, 赤裸裸的查表! 职责链呢? 作为一个设计模式, 仅仅是针对查表然后执行算法, 那策略模式早就对这个抢饭碗的提反对意见了. 事实上职责链到底是干嘛地的呢? 搞清楚这个问题, 就可以判断真正的职责链在这里是不是合适, 或者原文作者使用链表的方式, 是否是职责链所覆盖的范畴了.

DP955.1 CHAIN OF RESPONSIBILITY
1. 意图
使多个对象都有机会处理请求......
2. 动机
考虑一个图形界面中上下文有关的帮助机制.....如果对于那一部分界面没有特定的帮助信息, 那么显示一个关于当前上下文的较一般的帮助信息, 比如说.....
因此很自然地, 应根据普遍性即从最特殊的到最普通的顺序来组织..
3. 适用性
  • 有多个对象可以处理一个请求, 哪个对象处理一个请求运行时刻自动确定.
  • ...
  • ...

找啊找啊找朋友, 找到一个女朋友~ 没错, 职责链说的就是这个事: 你认识一个特别出色的女人, 你跟她说, 做我马子吧, 如果她同意了, 自然好; 如果她说, 你就像我哥哥, 于是你说了, 既然我是你哥哥, 你得帮哥哥介绍一个吧? 她为了尽快摆脱你, 把一个比她差点的朋友介绍给了你, 然后你说, 做我马子吧... 如此下去, 直到有一个人接受你为止.

有一个问题我们要想到: 假设那个特别出色的女人是A, 她那天正好想到的朋友是B, 你就去给B花钱了; 如果想到的是C, 那你就是给C花钱; 同时, B如果不同意, 也许会给你C的电话, 也许会给你D或者E的电话. 而BCDE都有可能接受你. 也就是说, 谁把你接收了是不一定的, 但总有一个能接收你, 当然这里假设你是个女人就行, 换个说法, 你是无要求的, 响应者是不固定的. 将Gof的例子形象化, 想像所有的东西都是卡片, 叠在一起 如下:

           --  
        ------
   --------------
--------------------

从正上方看,  是一个窗口平面,  最顶层的那个区域是某种Widget1, 下面一种是 Widget2, 再下面一种是Widget3, 最底下是4, 记住, 可以是相同或不同的class, 继承自一个Handler. 当我们用小问号点击Widget1所在区域的时候, 如果Widget1含有帮助信息, 就显示Widget1的, 如果没有, 就显示Widget2的以此类推. 当然, 真实的例子不会这么摆, 只会比这个更复杂, 从最底层的对象开始, 往上基本是一个树状的样子. 我们可以认为上面的图, 是这棵树某一个分支, 正好就这么简单.

我们换一个放法: Widget1下面是Widget7, Widget3, Widget9(按顺序), 也就是说, 队伍根本不同了; 寻找的过程仍然相似, 只是最终显示的帮助信息有可能不一样.  为什么呢? 我们考虑一个控件: Label. Label本身一般没有帮助, 我们把它放入Widget2里, 它如果被点击了, 其效果和点击Widget2应该是一样的; 换成Widget7如果Widget7有帮助, 就应该显示Widget7的帮助. 但如果正好Widget7也没有帮助呢? 就显示Widget3的呗, 因为Label既然是Widget7的一部分, 显然也是Widget3的一部分; Widget7自己被点击, 显示的也是Widget3的帮助.

我们需要看到的是: Widget7和Widget3, 显示帮助的方式可能完全不同, 这原因并非因为Widget7对应了一个既定的传入信息而Widget3对应了另外一个, 而是因为Widget7的自己的情况(在GoF的例子里是Widget自身的帮助)和画图方式可能和Widget3不同; 即无论是不同的选择, 还是不同的行为, 不是对应于外部对象, 而是职责链上对象本身的特性所确定的. 这就决定了, 在运行时, 对于所有可能传入的任意信息全体, 我们通过职责链决定谁是接收者从而改变了处理它们中每一个方式, 实际上这就是使用职责链的一个需求. 对于GoF的例子, 没有传入任何信息; 对于本文的例子, 其实没有这样的需求.

接下来要说明的是, GoF的例子中, 一个对象的类别是否能够响应职责链, 也不是固定的, 这话看着和上面重复, 其实这是另一个不固定: 任何一个从基类继承下来的对象, 比如上面这些Widget, 如果它被设置了帮助, 当它被点击就会响应, 如果没有设定帮助(默认值), 就应该由它传递给更下层的一个. 这个情况下Widget7响应这个请求, 另一个情况也许就不响应了, 这只和是否给Widget7设置了帮助文本有关. 即: 判断是不是响应的条件, 并非传入对象持有的信息(事实上GoF的例子根本没有传入对象), 而是由职责链上的对象自己持有的信息来确定; 在GoF的说法里, 非常关键的一个词, 就是上下文相关, 这才是职责链的精髓所在, 而传递这一形式, 仅仅是具体做法罢了.

最后, 考虑这个窗口平面的例子, 我们用小问号点击的Widget, 不一定是处于链表头的对象, 我们的点击可以落在链表中任何一层上任何一个对象所对应的范围内, 所以链表中每一个对象都可能而且可以成为入口. 按照原文例子的用法, 所有的请求都只能从链表头也就是最上层进去, 这和DP95上所描述的意图和结构完全不符, 在GoF描述的职责链功能和用法中, 每一个具体子类都可能响应请求, 第一个接收者是谁是由输入动态决定的, 如果只从头部进去, 只不准哪个不该负责的对象就进行了不正确响应: 比如点击的Widget3, 请求却让Widget1给截获了.

另外既然说的这么细了, 顺便说一下, 没人处理的方式(这不是因为链条上的对象没有能力处理, 而是因为没人愿意处理)一般也不是返回false, 而是交给最终的一个DefaultHandler, 做默认操作; 这表面看跟直接返回没啥区别, 每次还必须将DefaultHandler接到职责链尾端, 但实质上保证了处理方式的统一.

不同的子类, 只是行为不同, 而不代表该子类从根本上只可以处理它对口的信息, 不能处理其它信息, 即不存在(信息n --> 行为n)这样的对应关系; 每一个行为都应该可以处理任意一种信息, 子类改变的就只是处理方式, 而到底哪一个响应用什么方式处理, 原则上说与传进来的信息是什么无关, 而是动态的根据输入决定入口, 接下来入口后面的候选者根据自身状态决定的. 正是因为我们不知道某一对象, 是否会响应这一操作及处理它附带的信息, 所以我们就无法应用表模式, 这样就需要职责链模式来解决.这才是23种而不是22种设计模式的原因.

比如这样的形式:

XxxBindingConstraint x = new XxxBindingConstraint();
x.SetParent(previousBindingConstraint);
x.AddNames(
{"Basic""WS""xxx".});


很显然, 如果这样修改原文的例子, 虽然每个对象都能响应请求了, 却不能正确执行: 但因为其设计目的缘故, 比如Basic对应的实现只并不负责处理WS, 只能等着抛出异常, 根本不符合这个意义:

DP95第五章开头
Chain of Responsibility(5.1)....,根据运行时刻情况任一候选者都可以响应相应的请求.

关于从链条中间开始接收请求倒是不需要改, 只是这一行为根本无法做出: 因为这个设计从需求即根本上讲只能够从头部遍历到那个属于他的真命天女, 而并非由运行时输入所指定的那个开始找一个最合适的对象. 后者作为职责链的一个需求, 本身就最小化了传递的代价: 从接收者到头部之间的对象, 是不参与判断的.

所以我们可以判断, 这个例子只是徒具链表的形式, 却根本没有考虑链表更适合哪些情况, 在那些情况下如何发挥正面作用的; 无论这一设计实现的多么完美, 它相比其它选择, 更多的是担负了不该有的负面效果. 抛开是不是明白或误解了什么设计模式这样毫无重要性的事情, 这在我看来就像在过去经常提到的, 是缺乏对事物的正确分析的缘故. 这一前提, 才是做出良好设计的唯一前提, 哪怕你都不知道设计模式是啥, 甚至连面向对象几个字怎么写都不知道.

多说一句, 对于GoF的例子, 其实也存在着很多粗陋之处(过于简单就不算了); 比如是不是HasHelp, 由基类提供一个方法判断, 但是如果HasHelp()传回false, 由咱们实现的子类来转发. 这个模型产生了一个不一致性: 如果基类的HasHelp()认为没有Help, 但其实你根本没有用基类这个逻辑, 按照子类的逻辑是有Help的所以你就直接响应了呢? 于是为了消除这个不一致性, 我们实现子类时就不得不给基类提供信息, 这就可能会增加不必要的繁文缛节. 只是这些不完美, 可以理解为示例的不严谨, 总体来说是没有任何大的漏洞的.

再回顾一下原帖作者的例子为什么不符合对职责链产生的需求与职责链自身特征: 每一个传入的对象, 有一个明确的接收者; 链上除这个明确接收者之外的其它对象, 绝不会处理请求; 而这个明确的接收者的类别, 是否处理某请求这一要素是固定的. 相反该模式要求链上的所有接收者们在运行时刻根据具体情况判断是否响应(而不是单纯的接收和传递)这个请求, 从而改变响应请求(无论带有什么信息)所执行的行为. 这样的用法, 半点职责链的独有好处也得不到, 只能说形似职责链, 实则表模式, 而且由于是链表, 就变成了负担职责链开销的策略模式.

怪怪设计模式第一反律: 可以找出对应关系的, 不要使用链表, 如果硬以传递的方式成链, 也不能叫做职责链, 只能叫做对象链表. 职责链是一根链上每一环节皆可响应, 但不一定响应; 不是只有一个固定响应, 其它负责传递; 归根结底, 是整条链有机的负责一个响应一个请求, 而不是一个链表中某一对象一个人在战斗, 职责是链的职责而不是某一对象的职责. 否则这条链除了因为传统链表的传递而增加了负担又有何意义?

什么时候该用职责链, 请自己思考, 但我们完全可以做出什么时候不要使用职责链模式的判断, 即和原文作者正好相反:

1、当一个方法的传入参数将成为分支语句的判断条件时;
2、当每一个分支的职责相对独立,且逻辑较为复杂时;
3、当分支条件存在扩展的可能时。

总而言之, 只要你看见"分支"二字, 请不要使用职责链模式. 职责链上的所有相同类别不同类别的对象, 都具有相似的职责, 而不是A类别的对象负责处理A信息, B类别的对象负责处理B信息. 作为一个行为模式, 该模式处理的是对象间, 而不是类别间的职责分配. 这两者的区别是, 前者是根据对象状态而确定是否中彩, 而每一个对象从原则上来说都会花钱, 无论彩票兑换的是美元英镑还是人民币.

我们深入一下这个例子的实质的话, 就会发现, 因为谁对谁响应这个对应关系, 是在运行之前就确定的, 所以最适合的方式是使用表驱动. 由此我们可以得出:

怪怪设计模式第一推广: 可以找出对应关系的, 优先考虑表驱动; 看见"分支"二字, 优先考虑策略模式.

最后我们再看一下原作者的最佳实践:

引用(错误! 见后)1、应尽量将职责链模式的抽象定义为抽象类,而不要定义为接口。这样有利于一些公共逻辑的重用。
2、应在实现职责链模式的同时,提供创建职责链的工厂类。

关于1, GoF当初是抽象类, 但是其实接口并不是不可以. 如果你想细究这个问题, 可以看上面关于GoF例子瑕疵的讨论, 来思考父类集中操作有多大意义. 但记住请先翻DP95, 了解清楚那个例子. 有没有抽象类, 关键还在于抽象类合不合适:

怪怪第一哲学定律: 一切决定都是交换.

如果你公共操作很多, 那你就付出抽象类站位的代价(提示:DP95的代码是C++, 可以多继承); 如果你的其它设计需要基类, 就要付出打字的代价. 这两者可以兼得吗? 答案见文章末尾注释一.

关于2, 完全不靠谱. DP95上说的非常清楚, 职责链要解决的一个问题就是, 链条上单元的动态改变; 同时在DP95的模式关系图上, 职责链和工厂完全没有联系. 拿上面的Widget的例子来说, 难道我们应该为每种不同排列顺序, 都搞个工厂方法? 不同类别的Widget们只要能互相嵌套, 其排列组合有多少是可以预期的. 另外, 真正的职责链, 接收请求的对象可以是链条中的任何一个具体子类, 而用了Factory, 我们只能从Factory返回的头部传入信息, 职责链原本具有的行为就不正常了, 讨论见上面Widget那部分.

在文章末尾, 我想严肃的说两句多余的话: 我一直在呼吁的是, 如果作者们不确定自己的理解, 请考虑对博文读者的影响有多大可能性是坏的; 写东西时, 也不要太过自信, 如金科玉律; 不如以提问的方法说, 比如: "我是这样想的, 大家看对不对? "; 更不要着急在得到验证前, 随便将自己的文章放传播到更广泛的地方上去, 把炸弹变成原子弹.

不过我仍然觉得, 今天这块板砖有点太大了, 因为我拿一本设计模式书籍的作者当作了例子. 我想这篇文章是dudu都不愿看到的(小人之心度君子之腹), 毕竟该作者的大作属于博客园系列. 我其实非常犹豫发不发该文, 因为说实在的, 别人对设计模式会不会误解, 和我一样的初学者会不会受误导, 跟我有屁关系? 我又不是以技术宣传为生存手段的, 况且如果我打算写书, 恐怕这篇文章连出版商都不愿意看到; 恐怕他们更愿意的是别人自己写自己的, 别涉及他们的作者的文章; 哪个出版商会对没事得罪人的人感兴趣? 我之所以意识到这些问题, 却没有去换个例子, 也有考虑: 我一直提倡的是, 无论谁写的是黑纸白字几十块一本, 还是随便讨论瞎说几句, 大家最好都自己思考后再说; 从这个意义上讲, 原文作者的情况作为例子倒是最合适的.

且不说招一大片人反感的问题, 作为一个软件从业人员和潜在竞争对手, 对我来说, 别人走越多弯路, 是不是对我越有益呢? 我偶尔会思考这个问题, 也希望得到大家的答案.

----------------------------------------
注释一: 我的答案是可以; 这需要额外的学习和外部条件, 和引入一个新模式, Gasket, 即垫片模式; 这个模式也要交换从而付出很大的代价:该模式有扩散的特性, 导致你写程序方方面面有彻底的变化, 甚至是没有接触过的方式; 所以也要下功夫学很多新东西.

P.S. 不用Baidu和Google了, 该模式是我自娱自乐的一个野路子, 在经过我自己严格检验前, 不打算发表(而且对于很多固执的人而言肯定不愿意付出某些代价去交换), 先卖个关子吧.

posted on 2008-02-18 08:03  怪怪  阅读(4021)  评论(36编辑  收藏  举报

导航