65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中可重用的责任链

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (44投票s)

2014 年 3 月 14 日

CPOL

13分钟阅读

viewsIcon

116289

downloadIcon

934

本文介绍了如何借助规范模式(Specification pattern)实现可重用的责任链模式。

引言

责任链(COR)模式是一种有助于我们避免发送者和接收者之间耦合的模式。中介者/观察者模式也可以实现相同的功能,但 COR 中接收者(链)之间的链接使其脱颖而出。换句话说,请求会按顺序在不同的接收者之间传递,直到找到正确的接收者。


以下是经典责任链模式的类图

COR 在工作流等场景中是非常有用的模式。当我们需要在同一组织内或不同组织的不同域中使用可重用的工作流设计时,它总是很方便。然而,经典的 COR 模式不够灵活,无法直接重用,因为它在实现中绑定到了领域模型。此外,在所有情况下,都无法在不修改代码的情况下更改链。在本文中,我将解释如何使用规范模式来封闭 COR 代码,防止未来修改,使其灵活且可重用。

责任链

让我们通过一个例子来实现 COR。考虑一家手机店,我们有不同类型的手机(基本型、经济型和高端型)。让我们定义手机类

    public class Mobile : IProcessable
    {
        public Type Type { get; set; }
        public double Cost;
        public string GetDescription()    
        {
            return "The mobile is of type : " + this.Type;
        }
 
        public Mobile(Type type, int cost = 0)  
        {            
            this.Type = type;
            this.Cost = cost;
        }
 
        public void Process()   
        {
            this.Cost = (0.9) * this.Cost;  
            Console.Write("The new cost is: {0} and the type is {1}. ", 
                this.Cost, this.Type);
        }
    }
 
    public enum Type  
    {
        Basic,
        Budget,
        Premium
    } 

手机类包含两个主要属性:类型(Type)和成本(cost)。类型(Type)是 `enum` 类型,成本(cost)是 `double` 类型。

让我们定义以下责任链场景。该商店的库存购买政策如下:员工只能订购基本型手机。主管只能订购经济型手机。而高级经理则可以订购高端型手机。

以下是此场景的经典责任链实现

    abstract class Handler
    {
        protected Handler successor;
 
        public void SetSuccessor(Handler successor)
        {
            this.successor = successor;
        }
 
        public abstract void HandleRequest(Mobile mobile);
    }
 
    class Employee : Handler
    {
        public override void HandleRequest(Mobile mobile)
        {
            if (CanHandle(mobile))
            {
                Console.WriteLine("{0} handled request {1}",
                  this.GetType().Name, mobile);
            }
            else if (successor != null)
            {
                successor.HandleRequest(mobile);
            }
        }
 
        public bool CanHandle(Mobile mobile)
        {
            return (mobile.Type == Type.Basic);
        }
    } 

处理程序接口定义了两个方法:`SetSuccessor` 和 `HandleRequest`。`SetSuccessor` 方法用于形成不同处理程序(在本例中为商店的员工、主管和高级经理层级)之间的链。`HandleRequest` 方法会检查当前处理程序是否可以处理对象(手机)。如果不能,则通过调用 `successor.HandleRequest` 方法将其传递给链中的下一个处理程序。


员工类接受一个手机对象,并检查它是否可以由自身处理。员工的业务规则是手机类型应为基本型,这在 `CanHandle` 方法中实现。如果为真,则处理请求;否则,将其传递给员工的后继者。可以看到主管和高级经理类是员工和主管的后继者,它们的示例如下

    class Supervisor : Handler
    {
        public override void HandleRequest(Mobile mobile)
        {
            if (CanHandle(mobile))
            {
                Console.WriteLine("{0} handled request {1}",
                  this.GetType().Name, mobile);
            }
            else if (successor != null)
            {
                successor.HandleRequest(mobile);
            }
        }
        public  bool CanHandle(Mobile mobile)
        {
            return (mobile.Type == Type.Budget);
        }
    }
 
    class SeniorManager : Handler
    {
        public override void HandleRequest(Mobile mobile)
        {
            if (CanHandle(mobile))
            {
                Console.WriteLine("{0} handled request {1}",
                  this.GetType().Name, mobile);
            }
            else if (successor != null)
            {
                successor.HandleRequest(mobile);
            }
        }
 
        public  bool CanHandle(Mobile mobile)
        {
            return (mobile.Type == Type.Premium);
        }
    }
 

上面的代码只是重复了员工类,只有 `CanHandle` 方法中的业务规则不同。让我们看看它们是如何执行的

Handler employee = new Employee();
Handler supervisor = new Supervisor();
Handler seniorManager = new SeniorManager();
employee.SetSuccessor(supervisor);
supervisor.SetSuccessor(seniorManager);
 
mobiles.ForEach(o => employee.HandleRequest(o));  

在创建每个处理程序的对象后,我们使用 `SetSuccessor` 方法将它们链接起来,并通过调用链中第一个对象(员工处理程序)的 `HandleRequest` 方法开始处理。

限制

这段代码工作正常,但存在一些限制

开闭原则

此实现仅在一定程度上支持开闭原则。在经典的 COR 中,有句话说,可以通过添加新的处理程序或更改链的顺序而不修改代码来改变系统的行为。但这并不适用于所有场景。让我们举例说明。假设商店希望通过引入新的经理层级来分担主管和高级经理的工作。经理应批准所有成本超过 200 美元的经济型手机。他们还应批准所有成本低于 500 美元的高端型手机。此条件意味着我们应将经理安排在主管和高级经理之间。以下是实现此目标的方法

创建一个名为 `manager` 的新类,实现 `IHandler` 接口,并复制例如员工类的相同代码,然后如下更改 `CanHandle` 方法

public bool CanHandle(Mobile mobile)
{
      return ((mobile.Type == Type.Budget && mobile.Cost >= 200) 
           || (mobile.Type == Type.Premium && mobile.Cost < 500));
} 

我们完成了吗?答案是“否”。**我们需要更改主管和高级经理类中 `CanHandle` 方法的业务规则,以限制它们处理具有成本限制的所有经济型和高端型手机。因此,在此类场景中,代码并非对修改封闭。**

可重用性

我们能否将此实现重用到另一个业务域或项目?想象一个新领域,处理程序是 CodeProject 的作者、编辑和社区版主,而请求是 CodeProject 的文章。

COR 和规范模式之间的对话

  • COR:哦,我不知道我有这些问题?我该怎么办?
  • 规范模式:哦…你好!别担心,我来帮你!
  • COR:真的吗??怎么做?
  • 规范模式:继续阅读

规范模式

在规范模式中,业务规则根据单一职责原则(SRP)进行分离,并可以使用布尔运算符(AND、OR 或 NOT)进行链接或组合,以创建复杂的业务规则。每个分离的业务规则称为规范。我已经在该主题上写了一篇详细的文章。请点击这里在 CodeProject 上查找关于规范模式的文章。在这篇文章中,我演示了如何以经典方式使用 C# 实现规范模式。让我们简要看看它是如何工作的。规范模式的基石是 `ISpecification` 接口。

public interface ISpecification<T>
{
     bool IsSatisfiedBy(T o);
} 

每个规范都必须实现此接口。实际的业务规则在 `IsSatisfiedBy` 方法中。让我们看看如何实现规范

public class Specification<T> : ISpecification<T>
    {
        private Func<T, bool> expression;
        public Specification(Func<T, bool> expression)
        {
            if (expression == null)
                throw new ArgumentNullException();
            else
                this.expression = expression;
        }
 
        public bool IsSatisfiedBy(T o)
        {
            return this.expression(o);
        }
    }   

每个业务规则都可以定义为一个评估为布尔结果的表达式。例如,检查手机类型是否为高端型就是一种表达式。我们可以在 C# 中将这种表达式定义为 `Func`,其中 T 是评估规则(检查高端类型)的对象(Mobile),bool 是结果。上面的 `IsSatisfiedBy` 方法调用此表达式并返回表达式的布尔结果。


正如我之前提到的,规范可以使用布尔运算符进行链接/组合。在我之前的文章中,我通过为每个运算符定义专用规范来实现这一点。使用 C# 的扩展方法也可以实现相同的功能,正如 Yury Goltsman 在评论部分所演示的那样。下面的代码展示了如何使用布尔 AND、OR 和 NOT 运算符链接规范。

public static class SpecificationExtensions
    {
        public static Specification<T> And<T>(this ISpecification<T> left, ISpecification<T> right)
        {
            return new Specification<T>(o => left.IsSatisfiedBy(o) && right.IsSatisfiedBy(o));
        }
        public static Specification<T> Or<T>(this ISpecification<T> left, ISpecification<T> right)
        {
            return new Specification<T>(o => left.IsSatisfiedBy(o) || right.IsSatisfiedBy(o));
        }
        public static Specification<T> Not<T>(this ISpecification<T> left)
        {
            return new Specification<T>(o => !left.IsSatisfiedBy(o));
        }
    } 

正如您所见,每个方法(AND、OR 和 NOT)接受一个或多个规范并返回一个规范。如前所述,任何接受表达式并返回布尔结果的内容都可以定义为规范。这里使用了相同的概念。让我们看 AND 方法的例子。它接受两个不同的规范,并通过使用表达式“left.IsSatisfiedBy(t) && right.IsSatisfiedBy(t)”来构建一个新规范。我们已经知道 `IsSatisfiedBy` 方法的返回类型是布尔值,因此我们可以轻松推断此表达式本质上是对两个布尔值应用的 `&&` 操作。OR 和 NOT 方法的情况也是如此。


让我们使用此规范模式实现上面讨论的商店政策(过滤掉基本型、经济型和高端型手机)。

ISpecification<Mobile> basicSpec = new Specification<Mobile>(o => o.Type == Type.Basic);
ISpecification<Mobile> budgetSpec = new Specification<Mobile>(o => o.Type == Type.Budget);
ISpecification<Mobile> premiumSpec = new Specification<Mobile>(o => (o.Type == Type.Premium));
var mobilesHandledByEmployee = mobiles.FindAll(o => basicSpec.IsSatisfiedBy(o));
mobilesHandledByEmployee.ForEach(o => Console.WriteLine(o.GetDescription()));  

现在让我们看看如何实现前面讨论的策略更改。新策略的简要摘要如下:

  1. 员工规范:所有基本型手机
  2. 主管规范:成本低于 200 美元的经济型手机。
  3. 经理规范:成本超过 200 美元的经济型手机,以及成本低于 500 美元的高端型手机。
  4. 高级经理规范:成本超过 500 美元的高端型手机。

为了实现这一点,我们需要创建四个新的规范。`BudgetLowCostSpec` 过滤掉所有成本低于 200 美元的手机(注意:不只是经济型手机),而 `BudgetHighCostSpec` 定义所有成本大于或等于 200 美元的手机。类似地,`premiumLowcostSpec` 和 `premiumHighCostSpec` 定义所有成本低于 500 美元和大于或等于 500 美元(含)的手机。

//To extract all mobile phones that costs less than 200
ISpecification<Mobile> budgetLowCostSpec = new Specification<Mobile>(o => (o.Cost < 200));
//To extract all mobile phones that costs greater than or equal to 200
ISpecification<Mobile> budgetHighCostSpec = new Specification<Mobile>(o => (o.Cost >= 200));
 
//To extract all mobile phones that costs less than 500
ISpecification<Mobile> premiumLowCostSpec = new Specification<Mobile>(o => (o.Cost < 500));
//To extract all mobile phones that costs greater than or equal to 500    
ISpecification<Mobile> premiumHighCostSpec = new Specification<Mobile>(o => (o.Cost >= 500));

现在让我们重新定义我们新策略的规范

employee.SetSpecification(basicSpec);
            
// For supervisor spec we combine the budget spec with budget low cost spec to achieve the 
// constraint all budget mobiles that costs less than 200
supervisor.SetSpecification(budgetSpec.And<Mobile>(budgetLowCostSpec));

// For manager spec we combine the budget spec with budget high cost spec to achieve the 
// constraint all budget mobiles that costs more than or equal to 200.
// For manager spec we combine the premium spec with premium low cost spec to achieve the 
// constraint all premium mobiles that costs less than 500. 
manager.SetSpecification(budgetSpec.And<Mobile>(budgetHighCostSpec).Or<Mobile>(premiumSpec.And<Mobile>(premiumLowCostSpec)));
            
// For senior manager spec we combine the premium spec with premium high cost spec to 
// achieve the constraint all premium mobiles that costs more than or equal to 500.   
seniorManager.SetSpecification(premiumSpec.And<Mobile>(premiumHighCostSpec));

员工规范未更改,但其他规范通过使用布尔 AND 和 OR 将现有规范与新规范组合来更改,以达到我们的解决方案。这就是规范模式的强大之处,不必修改现有规范。我们可以创建新的规范并根据需要添加到现有规范中。是不是很酷?

救援行动

开闭原则

正如我们在 COR 中看到的,不同处理程序之间的唯一区别在于业务规则(`CanHandle` 方法实现)。如果我们能将其通用化,就能封闭代码以进行未来的修改。因此,让我们看看如何通过使用规范模式使业务规则通用化。让我们首先向 `IHandler` 接口添加一个新的方法定义,称为 `SetSpecification`;

public interface IHandler<T>
{
    void SetSuccessor(IHandler<T> handler);
    void HandleRequest(T o);
    void SetSpecification(ISpecification<T> specification);
} 

SetSpecification 方法接受一个特定于处理程序的规范对象。现在,处理程序类仅限于处理实现 `IProcessable` 的对象。稍后我们将讨论这一点。


让我们看一下处理程序类的实现。请记住,我们将实现这个通用实现,这意味着我们将为所有处理程序只实现一次。

public class Approver<T> : IHandler<T> where T: IProcessable
    {
        private IHandler<T> successor;
        private string name;
        private ISpecification<T> specification;
        public Approver(string name)      {
            this.name = name;
        }
        
        public bool CanHandle(T o)     {
            return this.specification.IsSatisfiedBy(o);
        }
 
        public void SetSuccessor(IHandler<T> handler)     {
            this.successor = handler;
        }
 
        public void HandleRequest(T o)    {
            if (CanHandle(o))  {
                o.Process();
                Console.WriteLine("{0}: Request handled by {1}.  ", o,  this.name);
            }
            else  {
                this.successor.HandleRequest(o);
            }
        }
 
        public void SetSpecification(ISpecification<T> specification)     {
            this.specification = specification;
        }        
    } 
 

您可以看到 `SetSpecification` 方法用于分配与处理程序相关的规范。其余方法与 `CanHandle` 相同,只是 `CanHandle` 除外。在 `CanHandle` 中,我们不指定具体的业务规则,而是调用规范对象的 `IsSatisfiedBy` 方法。现在我们的代码可以在任何处理程序和任何域中重用。

可重用性

我们没有指定 COR 作用的域对象(Mobile),这清楚地表明了其可重用性。还请注意,我们的规范模式本身是通用的,可以轻松地与其他项目一起用于 COR。

最终解决方案

让我们首先使用 COR 和规范的组合来实现我们之前的策略。我们的三个业务规范分别是基本型、经济型和高端型手机。我们使用规范模式来定义这些规则,如下所示

//Create the different handlers
IHandler<Mobile> seniorManager = new Approver<Mobile>("SeniorManager");
IHandler<Mobile> supervisor = new Approver<Mobile>("Supervisor");
IHandler<Mobile> employee = new Approver<Mobile>("Employee");
 
//Set the specifications for the handlers
employee.SetSpecification(basicSpec);
supervisor.SetSpecification(budgetSpec);
seniorManager.SetSpecification(premiumSpec);
 
//Set the successors
employee.SetSuccessor(supervisor);
supervisor.SetSuccessor(seniorManager);
 
//Execute
mobiles.ForEach(o => employee.HandleRequest(o));

这比经典方式更整洁,不是吗?首先我们定义审批者,然后相应地将规范分配给每个审批者。定义后继者。还需要什么?只需调用 `HandleRequest` 方法。

需求变更

现在让我们看看如何在代码中实现策略更改。我们需要引入一个新的审批者,称为经理,他可以批准所有成本超过 200 美元的经济型手机以及所有成本低于 500 美元的高端型手机。通过简单地重新创建对象,我们可以实现这一点,如下所示

IHandler<Mobile> seniorManager = new Approver<Mobile>("SeniorManager");
IHandler<Mobile> manager = new Approver<Mobile>("Manager");
IHandler<Mobile> supervisor = new Approver<Mobile>("Supervisor");
IHandler<Mobile> employee = new Approver<Mobile>("Employee");
 
employee.SetSpecification(basicSpec);
supervisor.SetSpecification(budgetSpec.And<Mobile>(budgetLowCostSpec));
manager.SetSpecification(budgetSpec.And<Mobile>(budgetHighCostSpec).Or<Mobile>(premiumSpec.And<Mobile>(premiumLowCostSpec)));
seniorManager.SetSpecification(premiumSpec.And<Mobile>(premiumHighCostSpec));
 
employee.SetSuccessor(supervisor);
supervisor.SetSuccessor(manager);
manager.SetSuccessor(seniorManager);
 
mobiles.ForEach(o => employee.HandleRequest(o));  

宾果!同样,我们可以用不同的处理对象更改处理程序,如下所示

//The sepcification and successors are assigned through constructor for simplicity
IHandler<Article> commModerator = new Approver<Article>("CommunityModerator", commModeratorSpec, null);
IHandler<Article> editor = new Approver<Article>("Editor", editorSpec, commModerator);
IHandler<Article> author = new Approver<Article>("Author", authorSpec, editor);
 
author.HandleRequest(o); 

您是否注意到这里使用了相同的 COR/Spec 组合,但用于 CodeProject 文章提交流程。在这里,处理程序是作者、编辑和社区版主(好吧,流程在这里进行了调整)。请求是一篇文章。为简单起见,规范和后继者通过构造函数接受,这意味着 `Approver` 接口必须相应更改。

处理

我们有了解决方案,但处理仍然没有在手机上进行。在本例中,是进行库存采购订单。让我们让我们的手机实现 `IProcessable` 接口,该接口有一个名为 `process` 的方法。让我们看看如何在 `HandleRequest` 方法中进行处理

public void HandleRequest(T o)    
{
    if (CanHandle(o))  
    {
         o.Process(); //Processing will be done inside the process method of Mobile object
         Console.WriteLine("{0}: Request handled by {1}.  ", o,  this.name);
     }
     else  {
         this.successor.HandleRequest(o);
     }
} 

这部分可以通过不同的方式处理,以满足不同的需求。例如,如果处理必须在手机对象的上下文之外进行,我们可以使用 `<Action>` 类型并传入手机对象作为方法参数来调用它。

public Approver(string name, Action<T> action)      {
            this.name = name;
            this.action = action;
        } 
public void HandleRequest(T o)    
{
    if (CanHandle(o))  {
       //o.Process();
       this.action.Invoke(o);                
       Console.WriteLine("{0}: Request handled by {1}.  ", o.ToString(),  this.name);
    }
    else  {
       this.successor.HandleRequest(o);
    }
}

用于放置库存订单的方法可能如下所示

public class InventoryProcess<T>
    {
        public void placeOrder(T o)
        {
            Console.WriteLine("Action is invoked");
            //Place the order.
        }
 
    }  

源代码

完整的源代码已附在本文顶部供您下载。上述解决方案的输出是

影响链的其他因素

在这里,我根据不同的需求汇总了影响责任链模式的几个因素。

循环链

如果我们想要一个循环引用,其中链的最后一个成员(`Approver`)的后继者是第一个成员,该怎么办?

解决方案

通过这种方法,这很简单。我们只需要在为 `SeniorManager` 设置后继者时添加以下一行

employee.SetSuccessor(supervisor);
supervisor.SetSuccessor(manager);
manager.SetSuccessor(seniorManager);
seniorManager.SetSuccessor(employee); //Employee is set as the successor here.     

不是很直观吗?这种方法的主要好处是我们无需更改代码即可使其成为循环或线性,这完全取决于您的配置(对象创建)。

但是,我们必须非常小心规范。跨越所有处理程序的规范应涵盖所有可能的请求。例如,我们有一个定义成本为 `< = 200` 的规范,这意味着它涵盖了所有负值。类似地,我们有一个定义为 `> = 500` 的规范,它涵盖了从 500 到无穷大的所有可能值。假设我们有一个规范,而不是 **成本 `< = 200`**,而是 **成本 `> = 2 && 成本 `< = 200`**,那么我们就有一个问题。系统将进入无限循环并抛出 `System.StackOverflowException`。

传递列表作为请求

传递一组请求并通过链处理它们。

解决方案

简单就是最好的!因此,我们通过循环处理所有请求(对每个请求调用链),如演示源代码(上面已附件)中所实现的,而不是传递列表并在 `approver` 类中处理维护列表项的复杂性。但是,如果需要,并非不可能做到。我们需要创建一个新的审批者类,例如 `ListApprover`,它接受请求列表并相应地处理它们。

动态添加审批者

即时动态添加新的审批者。

解决方案

我在需求变更部分已经演示了这一点。

重叠规范

想象一下存在重叠规范的情况,例如:两个员工 `Employee1` 和 `Employee2` 都必须在将其传递给主管之前处理一次所有请求。因此,两个员工都定义了相同的规范。在当前方法中,只有其中一个能够处理请求。

解决方案

为了实现这一点,我们需要如下调整我们的 `Approver` 类。

public void HandleRequest(T o)
        {
            if (CanHandle(o))
            {
                //o.Process();
                Console.WriteLine("{0}: Request handled by {1}.  ", o.ToString(), this.name);
                this.action.Invoke(o);
                Console.WriteLine("\n****************************************");
            }

            if (this.successor != null)
            {
                this.successor.HandleRequest(o);
            }
        }  

这里删除了 else 部分,以强制后继者处理请求,而不是在请求处理后停止链。

默认规范/处理程序

与重叠规范因素相反,存在没有为某些条件定义规范的情况。想象一下,我们为成本从 **$**10 到无穷大的手机定义了规范,并决定不处理成本低于 **$**10 的手机。

解决方案

我们需要定义一个默认的 `approver`,它将处理此异常情况(**`< $10`**),如下所示:

IHandler<Mobile> defaultApprover = new Approver<Mobile>("Default", invProcess.defaultOrder);
ISpecification<Mobile> defaultSpec = new Specification<Mobile>(o => true);
defaultApprover.SetSpecification(defaultSpec);
seniorManager.SetSuccessor(defaultApprover); 

在这里,我们定义了一个名为 `defaultApprover` 的新审批者和一个名为 `defaultSpec` 的新规范,它总是返回 true。在将默认规范应用于默认处理程序后,我们将此处理程序链接到链中的最后一个成员,在本例中是 `SeniorManager`。请注意,第一行有一个名为 `defaultOrder` 的方法,我在上面“处理”部分解释的 `InventoryProcess` 类中定义了它。此方法为所有默认条件执行必要的处理。

输出示例将是

参考

历史

更新了包含异常处理的源代码。

© . All rights reserved.