补充说明: 表驱动, 链表与职责链

上篇文章中, 说到了关于表驱动, 链表, 与职责链的一些问题, 不得不打自己两下的是, 写的确实很不清楚, 还存在很多问题. 下面就原文回复中一个哥们提到的问题讨论一下:

            switch (endpoint.Binding.Name)
            
{
                
case "BasicHttpBinding":

                    Constraint1();
                    Constraint2();
                    Constraint3();
                    
break;

                
case "NetTcpBinding":
                    Constraint1();
                    Constraint3();
                    
break;

                
case "NetPeerTcpBinding":
                    Constraint1();
                    Constraint2();
                    
break;

                
//Other bindings' constraint;

            }

是不是这样的形式就可以用到职责链, 或者使用链表呢?

首先, 我们看看这个结构, 是否属于职责链应用的范畴, 顺便在理清一下职责链的特征的一些要点:

1. 职责链也只有一个对象响应, 但不是类别和信息的对应关系, 是对象和所处位置的对应的关系; 同时, 这种对应关系不是一对一的, 而是多对一的: 多个对象对应同一个位置, 因为它们在位置上是重叠的. 并非位置就一定是空间位置, 只有窗口系统才能使用职责链, 因为很多模型可以映射为这种空间上的模型. 只是对于更为抽象的东西, 必须仔细辨别.

在这里我想多说一个问题. 一个类, 有一个算法, 假设这个方法不是static的, 我们把这个类new出来, 这是一个对象. 问题是, 如果该对象自身没有状态, 我们用这个算法时, 可以说是和调用一个静态方法相似的. 那我们为什么需要这个对象呢? 这个问题稍候回答.

2. 职责链的任何一个对象都可以成为入口, 而不是拿头部当作入口. 应该注意的是"接收"(即入口)和"响应", 不是一个概念. 接收者无论是自己响应, 还是传递给别人, 目的是对于接收者来说, 在它的上下文中(其实所谓上下文对应仅仅是它自己的状态和所持有的下一个对象)进行正确的响应, 这是职责链的意义所在: 无论从哪里进入, 都能找出这个入口对象正确的响应方法.

这从根本上讲, 还是因为1中描述的对应关系所要求的: 更靠近头部的对象, 未必和指定位置存在对应关系; 指定位置, 一定和入口存在对应关系; 而入口以后的多个对象, 全和该入口所指定位置存在着的对应关系.

3. 对于2, 职责链是这样做到: 响应与否是职责链上的对象基于自身状况的判断, 不是对传入信息的判断; 比如, 一个职责链里的所有对象可以是相同类型的, 但是到底谁响应, 一个是看入口, 一个是看入口接下去的候选者哪个对象是第一个待命者, 是否待命, 则看候选者自身的状态.

另外一个需要重点突出的, 就是职责链关心的是对象行为, 并且表现了对象相互间结构上的顺序. 我们来看看职责链动作时的情况和对应关系:

其它对象...->.... ->对象1(类型1) -> 对象2 (类型2) -> 对象3(类型1) -> 对象4(类型1) -> 对象5

对象1(t) ==== 类型1
--------------------------------------
对象2(f) 
        -> 对象3(t) ==== 类型1
--------------------------------------
对象2(f)
        -> 对象3(f)
                -> 对象4(t) ==== 类型1
--------------------------------------
对象2(t) ==== 类型2
....
.

说明一下: t 为可以响应的状态, f为不可以响应的状态. 我们可以看到的是: 在这个过程中, 除了对象自己, 没人关心类型; 同时, 我们关心对象间的结构顺序, 对象自身的状态; 我们从哪里入口都可以; 另外, 既然要在对象中传递, 很显然每个对象, 无论它是什么类别, 它都必须能够正确处理传递的信息.

关于职责链, 砍掉了一些细节, 我觉得这次说的比较简洁了, 感兴趣的可以看看上一篇文章, 不过事先说明, 看晕了别怪我.

接下来我们回答上面的问题:

当我们需要动态的改变算法的时候, 我们需要一个接口(无论是纯接口还是抽象类), 隔离具体的行为, 而接口的传递, 只能通过实现该接口的对象来传递, 所以我们不能把它static了. 但是通过delegate的使用, 实际上我们可以省掉这种只有一个方法的对象, 因为delegate可以使用函数签名作为接口, 将算法传递进去.

这里的一个关键点, 其实还是那个对应问题: 信息n->操作n的对应. 只是当我们使用类的时候, 这种对应关系就转化为: 信息n->类型n; 而new了一个对象, 如果仅仅是为了使用接口, 我们可以把这种方式看作一种不得已而为之的惯用手段, 其实并没有真正体现对象的作用; 对对象的使用, 往往更多关心的不是对象的类型, 因为对象的存在更多是为了承载状态.

对于出现了switch(某信息) case 某方法的情况, 我们如果用类型+接口+对象使用这样的方法去重构, 实际上强调的是类型;  这就在一定程度上说明在这一需求中, 我们不关心对象们的状态; 实际上, 本例中负责Constraint的对象, 基本是没有自己的状态的; 这就说明, 对请求作出响应的候选者中, 不存在上下文. 他们是平行的.

信息1 ==== 方法1 ==== 对象1(类型1)
信息2 ==== 方法2 ==== 对象2(类型2)
信息3 ==== 方法3 ==== 对象3(类型3)
......
..

在上图中, 对象间没有任何顺序和结构; 例子经过本文开头的修改以后, 虽然看似有了结构和顺序 (假设用类和对象而不是delegate承载方法):

信息1 ==== 方法1, 方法2, 方法3 ==== 对象1(类型1), 对象2(类型2), 对象3(类型3)
信息2 ==== 方法1, 方法3 ==== 对象1(类型1), 对象3(类型3)
信息3 ==== 方法1, 方法2 ==== 对象1(类型1), 对象2(类型2)
...
.

但这个结构和顺序, 从根本上来说并非是对象本身之间就带有的*结构*上的顺序, 这是职责链为什么使用链表的另一个要点: 它是针对待响应者(即职责链上的对象)*结构*存在顺序的情况. GoF的例子比较直观就在于, 对于窗口, 其结构是空间结构, 而且其本质就是一条顺序的链, 很容易找出来; 所以我们说,顺序的单向链表还算很接近事情的实质, 适合于职责链模式, 而职责链模式, 则很适合那个要解决的问题.

那么象本文开头代码所表现的需求, 其结构和顺序是什么呢? 个人认为是*操作*或*时间*上的顺序, 对于这些, 我们往往选择把这样那样的结构转化为对象间的结构, 以实现更合理的设计. 但正是因为多了一步转化, 就不可避免的存在选择, 选择就不可避免的意味着交换. 另外一点就是, 多一件事, 就多一个出错的可能性: 在人为转化成对象结构顺序的过程中, 选择了非最佳的办法, 这就是为什么在一些框架里也会存在这种对链表或者其它结构的误用.

我个人的想法, 如果是本文开头代码的这种情况下(如果我没理解错的话), 跟职责链的差别在于:

1. 职责链由对象自身决定是否响应; 而现在这个例子, 通过固定关系决定是否响应; 同时在本例中, 我们关心的仍旧是类型(多个), 而不是对象状态. 这样使得对象发挥的作用不同.

2. 这个例子是信息与类型(所定义的操作)的一对多关系, 而不是位置与对象的一对多关系. 前者是不可变的, 后者是可变的; 这决定了前者比后者在设计结构时有更多的选择范围.

3. 这个例子中不同信息所对应的响应者集合, 相互之间存在交集; 而职责链中位置对应的候选者集合, 是子集与父集的关系. 所以不能像职责链一样随便进入, 而是必须从头端进入, 这就不能发挥链表的全部优势.

也就是说, 该例子使用链表, 没有职责链使用链表那么自然. 但完全贴合需求并不代表在实践上一定更优; 比如简单的成链交换来的是设计上的结构简化, 而原作者的设计并不比一般的表驱动更加容易, 也就是说只有付出, 所以才认为那种做法比较邪恶.

在这个例子中, 使用链表最大的好处是结构简单, 配置也就简单, 但是层次少了, 结构表达不明确, 也就仍旧付出了多余的开销. 这是一个交换; 比起原文作者的例子, 本文的例子这个用法更合理在于, 改变了链表上操作的形式:  不是只有一个链表上的对象响应, 而是每一个对象都可能响应.

那么还能怎么做呢? 考虑到这个例子中, 我们的对应关系仍然是固定的对应关系, 只是一个信息对应的操作, 从一个, 变为了一组,下面是我能想到的一个稍微复杂些的结构(它同时也耗费了更多的内存, 应该是减少了CPU占用):

1. 增加结构, 比如用Composite. 如下:
ConstraintComposite cc1 = new ConstraintComposite("名字1"); //注意, 最早的例子是一个类别对应一个名字
cc.AddChild(constraint1);
cc.AddChild(constraint2);
cc.AddChild(constraint3);

ConstraintComposite cc2 
= new ConstraintComposite("名字2");
cc.AddChild(constraint1);
cc.AddChild(constraint3);


2. 使用表驱动(原来叫这个 :P). 如下:
//虽然是树状结构, 但是对于字典来说, 只当它是表状的~ 
ConstraintExecutor ce = new ConstraintExecutor();
ce.AddConstraint(cc1);
ce.AddConstraint(constraint1);
ce.AddConstraint(cc2);
ce.AddConstraint(constraint3);


如果使用Composite, 我们图就变为:

信息1 ==== 对象1 ==== 方法4(方法1, 方法2, 方法3) ==== 类型Composite
信息2 ==== 对象2 ==== 方法2  ==== 类型2
信息3 ==== 对象3 ==== 方法4(方法1, 方法3) ==== 类型Composite
........
.

其实这样做, 比起链表实现的好处还不只这些. 考虑本文开头的代码: 如果Constraint1和Constraint2之间的操作是无序的, 或者有一个固定顺序, 链表最后一个操作被调用的情况, 随着链的增长, 仍然会变得很长, 如果有100种操作, 只调用了第一个和最后一个, 即使是本例, 开销比起最早的例子, 仍没有太大改进.

但是如果Constraint1和ConstraintN之间的操作排列顺序变得更加复杂, 有时候需要先执行Constraint1, 再执行Constraint7, 最后执行Constraint3, 又有时候, 我们需要先执行Constraint3, 再执行Constraint7呢? 可链表的顺序只能有一个! 我们唯一的方法就是根据传入对象不同, 先建立合适的链表了! 那样即使能实现, 结构恐怕反而要复杂于使用表驱动+策略+Composite吧?

在本例中也许不会碰到这个麻烦, 但不见得在任何设计中都不可能存在; 因为在类似的需求中有一个非常隐含的东西, 可能会被忽略的: 即我们到底如何处理, 是由外部传进来的对象所持有的信息所决定的. 如果外部对象和处理逻辑足够复杂, 使得我们处理对象的操作, 涉及改变对象的状态, 或某个我们系统中自身持有的全局状态, 同时另一些操作又依赖于这些状态, 怎么办? 我们不能要求外部对象或系统内的其它对象来符合这个处理链条的规范, 因为这么做实际上是变相耦合: 不但你知道它们怎么回事, 还要它们来就和你不成?

当然我们目前并没有碰见这个风险, 但为什么不让这个风险永远不会出现呢? 使用Composite, 你可以改变局部的执行顺序, 总而言之, 你再也不需要担心这个问题了. 同时, 这样的结构也同样很容易转为元数据或者配置文件. 其实如果使用反射和delegate, 我们连对象都不必要有, 一堆待配置的静态方法就好了. 只是delegate的Composite稍微需要一点技巧而已.

顺便说一句, 正宗的职责链可不会存在上面说的这种那种的问题(就像它也不会真正背负太多链表的废开销一样): 因为职责链从根本上来说, 只有一个对象响应. 前提只有一个, 你没把它用到不该用的地方. 写到这, 我就只能表达我对GoF面向对象功力的敬仰之情了: 只有四个字, 恰到好处!

本文要点在于:
1. 重新强化和清晰化一下职责链的重点.
2. 在直线结构和树状结构两种都还算合理的设计间, 进行一定分析.
3. 混乱的介绍一下设计时需要考虑的一些问题.

其实个人认为, 最重要的还是你怎么看待目标事物的, 只要眼光练的够毒够辣, 说实话, 爱怎么设计就怎么设计; 剩下的就看你是否了解语言和工具, 还有它们如何支持你的设计了.

posted on 2008-02-19 05:15  怪怪  阅读(2553)  评论(9编辑  收藏  举报

导航