职责链模式在开发中的应用

我在《软件设计精要与模式》第19章中介绍了职责链模式在实际项目中的应用,由于引入了该模式,使得对象在职责划分上有了更清晰的结构,然而由于项目场景的诸多限制,总有几分“为模式而模式”的生涩感觉。最近在开发WCF的相关项目时,又一次应用了职责链模式,一方面加深了自己对设计模式的进一步理解,也积累了一些心得,可以与各位分享。
 
在该项目中,我希望实现对Endpoint的合法性检验,其中对于绑定而言,则包含了许多约束,例如绑定与URI样式的约束,例如绑定与服务契约设计的约束。绑定不同,则向对应的约束也不相同。为了更好地体现Endpoint,我在项目中定义了属于自己的Endpoint类:
    public class ServiceEndpoint
    
{
        
public Uri Address
        
{
            
get;
            
set;
        }


        
public Binding Binding
        
{
            
get;
            
set;
        }


        
public Type ContractType
        
{
            
get;
            
set;
        }

    }

如果考虑最简单的实现方式,我们完全可以通过分支语句,判断不同的绑定类型,然后执行对绑定约束的检查,例如:

public class BindingConstraint

{

public bool Constraint(ServiceEndpoint endpoint)

{

bool flag = false;

switch (endpoint.Binding.Name)

{

case "BasicHttpBinding":

//check the BasicHttpBinding;

 

flag = true;

break;

case "NetTcpBinding":

//check the NetTcpBinding;

break;

case "NetPeerTcpBinding":

//check the NetPeerTcpBinding;

break;

//...Other bindings' constraint;

}

 

return flag;

}

}

这是一个合适的设计,无可厚非。然而,我们所面临的关于绑定的约束性检查,远非几行代码就可以实现。例如,对于WSDualHttpBinding绑定而言,我们就需要判断传递进来的契约类型是否具有回调契约。当然,我们可以分别将这些约束性检查放入到专门的方法,甚至是专门的类中,但不可避免的是,我们会让Constraint方法的switch语句变得越来越长。
 
这还不是关键的,最主要的是我们需要检查的绑定存在扩展的可能,因为除了WCF自身提供的绑定类型之外,我们还可以提供CustomBinding或其它自定义绑定,一旦可能增加绑定,就需要修改这里的Constraint()方法。这样的设计就难免捉襟见肘了。事实上,在.NET Framework 3.5中,微软就为WCF新增加了几个绑定,例如WebHttpBinding。
 
此时,我们就可以考虑采用职责链模式,由于我们是针对绑定类型进行约束性检查,我们可以为每个绑定类型定义一个约束对象,然后对其进行抽象,设计类图如下所示:
chainofresponsibility.gif

首先,我们需要定义一个基类BindingConstraint,如下所示:
    public abstract class BindingConstraint//:IEndpointConstraint
    {
        
protected fields

        
public methods        

        
public abstract methods

    }

这是一个抽象类,包含了两个protected字段,其中m_bindingConstraint即形成职责链的关键,通过AddConstraint()方法可以将BindingConstraint对象加入到职责链中,而字段m_hasNextConstraint则用来标识当前对象之下是否还存在BindingConstraint对象。抽象方法Constraint()则实现约束性检查的业务逻辑,同时还包括对职责链是否存在下一个职责对象的判断。例如BasicHttpBindingConstraint的定义如下:
    public class BasicHttpBindingConstraint:BindingConstraint
    
{
        
public override bool Constraint(ServiceEndpoint endpoint)
        
{
            
if (endpoint.Binding.Name == "BasicHttpBinding")
            
{
                
//check BasicHttpBinding Constraint;
                BasicHttpBinding binding = endpoint.Binding as BasicHttpBinding;
                //...

                
//check binding and address's scheme                if (endpoint.Address.Scheme.ToLower() != "http" && endpoint.Address.Scheme.ToLower() != "https")
                
{
                    
throw new BindingConstraintException("the binding is mismatch with address schema.");
                }
                

                
return true;
            }

            
else
            
{
                
if (m_hasNextConstraint)
                
{
                    
return m_bindingConstraint.Constraint(endpoint);
                }

                
else
                
{
                    
return false;
                }

            }

        }

    }

根据传入的endpoint对象,判断绑定的名称是否为"BasicHttpBinding",如果是,则执行该约束对象的约束性检查,如果不是,则去寻找职责链中的其它对象(根据m_hasNextConstraint判断)。正是通过这样的实现方式,当客户端调用BindingConstraint的Constraint()方法时,就可以根据传入的ServiceEndpoint对象,智能地找到符合自己条件的BindingConstraint对象,并执行Constraint()方法进行约束性检查。
 
在我的《软件设计精要与模式》一书中,在应用职责链模式时,我的建议是最好提供专门创建职责链的工厂类,在本例中自然也不例外:
    public class BindingConstraintFactory
    
{
        
public static BindingConstraint CreateConstraint()
        
{
            BindingConstraint basicHttp 
= new BasicHttpBindingConstraint();
            BindingConstraint netTcp 
= new NetTcpBindingConstraint();
            BindingConstraint netPeerTcp 
= new NetPeerTcpBindingConstraint();
            BindingConstraint netNamedPipe 
= new NetNamedPipeBindingConstraint();
            BindingConstraint wsHttp 
= new WSHttpBindingConstraint();
            BindingConstraint wsFederationHttp 
= new WSFederationHttpBindingConstraint();
            BindingConstraint wsDualHttp 
= new WSDualHttpBindingConstraint();
            BindingConstraint netMsmq 
= new NetMsmqBindingConstraint();
            BindingConstraint msmqIntegration 
= new MsmqIntegrationBindingConstraint();

            netMsmq.AddConstraint(msmqIntegration);
            wsDualHttp.AddConstraint(netMsmq);
            wsFederationHttp.AddConstraint(wsDualHttp);
            wsHttp.AddConstraint(wsFederationHttp);
            netNamedPipe.AddConstraint(wsHttp);
            netPeerTcp.AddConstraint(netNamedPipe);
            netTcp.AddConstraint(netPeerTcp);
            basicHttp.AddConstraint(netTcp);

            
return basicHttp;
        }

    }

通过CreateConstraint()方法,就可以像穿珠子一般,形成一条隐藏的职责链(在创建这样的职责链时,需要注意职责对象的先后次序),然后在客户端代码中,可以非常优雅的实现对绑定的约束性检查:

private static BindingConstraint m_bindingConstraint = BindingConstraintFactory.CreateConstraint();

 

public void AddServiceEndpoint(ServiceEndpoint endpoint)

{

if (m_bindingConstraint.Constraint(endpoint))

{

m_endpointsList.Add(endpoint);

}

}


不管传入的endpoint是何种类型的绑定,只要在职责链中创建了相对应的绑定约束对象,都可以进行约束性检查,且逻辑清楚,职责明确。如果对绑定进行了扩展,我们只需要新增对应的绑定约束对象,然后修改BindingConstraintFactory工厂类的工厂方法即可。
 
使用职责链模式必须恰到好处,否则会成为模式滥用的反面教材。根据我对职责链模式的理解,可以认定只要同时符合下列三个条件,就可以引入职责链模式:
1、当一个方法的传入参数将成为分支语句的判断条件时;
2、当每一个分支的职责相对独立,且逻辑较为复杂时;
3、当分支条件存在扩展的可能时。
 
至于职责链模式的最佳实践,则包含以下内容:
1、应尽量将职责链模式的抽象定义为抽象类,而不要定义为接口。这样有利于一些公共逻辑的重用。
2、应在实现职责链模式的同时,提供创建职责链的工厂类。

本文已在IT168发表:http://tech.it168.com/msoft/2008-01-31/200801310937911.shtml
posted @ 2008-02-17 16:48 张逸 阅读(4525) 评论(20) 编辑 收藏

 回复 引用 查看   
#1楼 2008-02-17 17:48 蛙蛙池塘      
职责链设计模式和管道架构模式是不是有些联系呢,帖子内容我还没看,先问个问题。
 回复 引用 查看   
#2楼 2008-02-17 19:42 怪怪      
不喜职责链, 用最简单的Strategy加工厂不是照样实现吗? 开销还低. 只要一出现可以乱序switch的地方, 其实就可以说职责链不是最适合的方式了. 因为其模型实际上是:

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

这样的形式. 看你如下代码:

对象一:
if (变量 == "信息一")
对象二:
if (变量 == "信息二")
...
...

这是什么? 这就是重复. 所以虽然得到了可以扩展的好处, 但还可以说是为了模式而模式.

有话直说, LZ无怪 :).


 回复 引用 查看   
#3楼 2008-02-18 09:59 flyingchen      
支持怪怪观点。感觉有点“小题大做”了

 回复 引用 查看   
#4楼[楼主] 2008-02-18 10:04 张逸      
@怪怪
在本文的场景中,其实并不适用于策略模式,因为他缺少自我判断自我寻找合适对象的功能,通过职责链模式可以比较好地解决这一问题。你可以试试。

至于你说的重复,我也不赞成你的意见。实际上,如果仔细分析,你会发现虽然每个对象中都包含了if语句,并对变量进行了判断,但判断的值是不一样的,即使提取为参数,要进行方法重构也很困难。设计时,不可能说要将所有类似的代码都定义为重复。

至少,在这样的场景下,我不认为是为模式而模式。

 回复 引用 查看   
#5楼[楼主] 2008-02-18 10:17 张逸      
有争议就好,大家可以发表意见,但前提是应基于本文所提到的场景。
 回复 引用 查看   
#6楼 2008-02-18 10:40 Jeffrey Zhao      
@张逸
每个Constraint都有几乎完全相同的if逻辑,其实再Template Method一下会好一些。

 回复 引用 查看   
#7楼 2008-02-18 12:19 金色海洋(jyk)      
还是没看懂。
 回复 引用 查看   
#8楼 2008-02-18 13:08 jillzhang      
@Jeffrey Zhao
恩,我也觉得此处还可以用Template Method

 回复 引用   
#9楼 2008-02-19 01:42 114411[未注册用户]
怪怪,我看了原文,我觉得你前面那两个对代码的优化做的很不错,我暂时没想到这样改了以后会有哪些不足之处,你想了多久想到的?:)
另外,我对原文的出揣测,似乎还包含了这样一层意思,我直接用Switch来表达
switch (endpoint.Binding.Name)

{

case "BasicHttpBinding":

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

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


break;

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


break;

//...Other bindings' constraint;

}
如果 原文 “当分支条件存在扩展的可能时”,指的是这类情况时,那么存在一种链表或集合来存储这些不同的约束集合应该也无可厚非了。
不过除非 每个分支基本上要对所有约束都走一遍,否则以我个人的习惯,我不会只建一条链表,因为速度在当前硬件下可能不会有太大的感觉,但是空耗太多终究不太美观。我可能更倾向于选择表驱动+链表结合/或是 在binging对象中储存相应的链表对象,不过表驱动+链表可能对象重用性更好一些,不知道是否有更好一些的方法?
另外,其实Flyweight人似乎有一点点表驱动的感觉吧:)

 回复 引用 查看   
#10楼[楼主] 2008-02-19 09:35 张逸      
好吧,按照怪怪所说,我们采用策略模式和工厂来实现。假定我们为每个绑定建立一个策略对象,抽象类型为BindingConstraintStrategy。至于工厂,为了简化起见,我们直接用简单工厂实现即可,工厂方法的参数仍然考虑使用ServiceEndpoint。

我想经过这样的实现后,那么在客户端代码中,应该是这样来调用:
//注意,此处不能通过工厂来创建m_constraint的具体对象,因为一旦创建完毕,就限制了m_constraint只能为其中某个绑定的约束检查对象。
private static BindingConstraintStrategy m_constraint;


public void AddServiceEndpoint(ServiceEndpoint endpoint)
{
//只能在这里面创建具体对象
m_constraint = Factory.Create(endpoint);

if (m_Constraint.Constraint(endpoint))
{
m_endpointsList.Add(endpoint);
}
}

注意这段代码与我的文章中客户端代码的区别。在AddServiceEndpoint()方法中出现了对象的创建,这种设计是否合理呢?如果不仅仅是该方法需要检查绑定的约束,那么就会在多个地方出现调用工厂的逻辑。这种实现方式与职责链的区别,是因为它无法智能的识别和判断,以找到准确的对象。

实际上,这两种实现方式并无太多的所谓轻量级与重量级的区别,不过坦白说,在性能上,职责链的消耗还是要稍微大一些了。

如Jeffery Zhao所说,如果约束对象确实存在一些公有的逻辑,那么引入Template Method,确实不错。

 回复 引用   
#11楼 2008-02-19 10:10 m j[未注册用户]
@张逸
楼主。。怪怪说的很有道理。。你客户端只需要用到BindingConstraint里的Constraint方法的话又何须用工厂来创建对象呢。。用一个Executor使用字典来选择具体调用方法不是更好吗。。这样就可以智能识别和判断了撒。。^^

 回复 引用 查看   
#12楼[楼主] 2008-02-19 10:23 张逸      
@m j
部分同意你的意见。这里实际上是一个选择的问题。如你所说,采用字典来选择就可以实现智能识别和判断,那么实际上这一过程仍然需要封装成一个单独的类来实现,也就近似于工厂了。

关键不在于这点,而是因为采用这种方法的话,那么在客户端调用时,并不能事先创建BindingConstraint对象,而是在需要约束对象时,根据参数的值从字典中去寻找。虽然寻找的过程在职责链中也存在,但职责链将这一寻找过程自身抽象到了BiindingConstraint对象中,因而代码上会简略一些。我们只需要在一处创建整条职责链即可。如果采用策略模式,就需要每次去调用对应的工厂,来智能生成或寻找准确的对象。这两者之间的区别,从我前面的评论中列出的代码可以得到比较。

采用策略模式,是不可能将在字典中查找的实现逻辑抽象到策略基对象中。

不过除了获得这一个优点,策略模式结合工厂的方式是要比职责链模式来得直观与简洁一些,我必须得承认这一点。

 回复 引用   
#13楼 2008-02-19 10:57 m j[未注册用户]
@张逸
客户端的代码还是一样简洁的撒。。因为字典又并不是由客户端来实现对吧。。

我觉得从解决具体问题来看你的方法完全可以接受没有任何问题。。要在这里讨论性能和可读性带来的影响几乎就是扯淡。。可怪怪说的关键的一点是你用的这种方法或许可以叫做什么什么链。。但确实不能简单的叫做“责任链模式”。。这个概念用在这里不够准确就会误导初学者哈。。

你是让我认识设计模式的启蒙老师之一。。当初舍书本而学习你们的博客就是因为我买的那本模式的书完全没解释清楚各种模式之间的区别和关键点。。让我越看越混淆。。而你现在也是能影响不少人的大师了。。不知道知识掌握得越多是否越觉得自己不足。。总之不妨就技术的问题多多讨论。。理是越辩越清。。期待你更多的好文章。。

 回复 引用 查看   
#14楼[楼主] 2008-02-19 13:28 张逸      
@m j
很喜欢我们这种讨论,不过别给我戴高帽子哦,这样我可受不起,因为这样我的责任可就大了。

首先我要申明一点,本文的实现绝对是职责链模式的实现,这是毋庸置疑的,而且我也不怕误导初学者,因为我的这个实现是正确的。所谓“职责”,其实就是为每个绑定进行约束性检查的职责而已。

实际上我和怪怪有分歧的是,在本文描述的场景下,是使用职责链模式好,还是使用其他方式,例如策略模式。

另,看来你没有仔细看清楚我在评论中所说,我并不否认关于字典的操作一定不会由客户端来完成,我也表达了这个意见,而是认为为了便于客户端调用,如果采用策略模式,那么关于字典的操作应该封装在专门的工厂类中,但绝对不应该放到策略对象本身中,作为它的一个职责。

我只觉得如果采用这种方式,客户端的代码虽然不复杂,但如果多处涉及到对绑定约束的检查,就可能出现调用工厂方法创建策略对象的重复代码。因为,采用策略模式,即使使用字典来查找相应的策略对象,该策略对象的创建仍然是要根据客户端调用时的ServiceEndpoint来决定的。如果使用职责链模式,这一判断自身就放在职责链对象中了,因而客户端可以免去这一个操作。

 回复 引用   
#15楼 2008-02-19 14:45 m j[未注册用户]
@张逸
Sorry。。楼主是不是还没看过怪怪的这篇文章(http://www.cnblogs.com/guaiguai/archive/2008/02/18/1071753.html)。。看你写的“好吧,按照怪怪所说”以为是看到这篇文章后写的。。

 回复 引用 查看   
#16楼[楼主] 2008-02-19 17:15 张逸      
@m j
呵呵,幸得你的提醒,我才知道怪怪另外写了一篇文章。我在那篇文章之后留了言。谢谢!

 回复 引用   
#17楼 2008-03-15 19:20 beniao[未注册用户]
初学者 路过 `学习上有问题我会提出来喜欢各位前辈门多多指点.
 回复 引用 查看   
#18楼 2010-09-25 23:23 肖敏      
我认为在本文的场景中,只要使用一个代理就可以了。
public delegate void Constraint(ServiceEndpoint endpoint);

public class ConstraintClass
{
public static void WSFederationHttpBindingConstraint(ServiceEndpoint endpoint)
{
...//如果不符合条件抛出异常
}
public static void BasicHttpBindingConstraint(ServiceEndpoint endpoint)
{
...//如果不符合条件抛出异常
}
}

客户端:
static void Constraint Cons;
Cons+=BasicHttpBindingConstraint;
Cons+=WSFederationHttpBindingConstraint;

try
{
Cons(endpoint)
}
cath(Exception e)
{
//检验失败
}
扩展只需要在ConstraintClass增加一个方法就可以。

 回复 引用 查看   
#19楼 2010-09-26 09:05 肖敏      
而且楼主所说的理由
1.switch语句会越变越长。我觉得变长并不可怕,只要不是指数级别的变成。更何况,switch语句变长可怕,CreateConstraint方法变成就不可怕吗。
2.扩展问题,如果说扩展需要修改Constraint方法是不可接受的,那扩展需要修改CreateConstraint方法也应该是不可接受的。
引用使用职责链模式必须恰到好处,否则会成为模式滥用的反面教材。根据我对职责链模式的理解,可以认定只要同时符合下列三个条件,就可以引入职责链模式:
1、当一个方法的传入参数将成为分支语句的判断条件时;
2、当每一个分支的职责相对独立,且逻辑较为复杂时;
3、当分支条件存在扩展的可能时。


第二点根本不是问题,逻辑复杂就写成单独的处理函数。剩下两个问题其实就是if else的经典问题。解决if else的扩展问题需要职责链吗?
首先,如果该判断语句的源码在我们手上,我觉得修改原有判断语句和把原有代码重构成职责链模式相比来说,我倾向于前者。而且正如我上面提到的,使用了职责链模式,在需要扩展的情况下,依然需要修改工厂的代码。
其次,如果我们需要写一个动态扩展的机制,留给第三发公司做二次开发。那么可以考虑以下解决方案:
interface IResponse
{
bool IsMyDuty(object request);
void Process(object request);
}

public void ProcessRequest(IList<IResponse> responseList,object request)
{
foreach(IResponse response in responseList)
{
if(response.IsMyDuty(request))
{
response.Process(request);
return;
}
}
}
在客户端,我们可以使用ObjectBuilder根据配置文件来动态生成IList<IResponse>对象。这样的话,对于扩展,只需要把扩展的dll部署到程序中,修改配置文件即可。

 回复 引用 查看   
#20楼 2011-04-15 15:40 鱼有胖的头      
好文先收了,待细看,感觉责任链模式是来自现实世界的交通运输行业中 Chain of Responsibility law,不是是否有此说。