经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » 设计模式 » 查看文章
设计模式-责任链模式
来源:cnblogs  作者:煮诗君  时间:2021/6/15 9:14:58  对本文有异议

作为一个上班族,我们可能会经常听到“管理流程混乱”,“职责边界不清晰”等这样或那样的抱怨,这是当组织或系统发展壮大后,一件事由一个人或者一个部门无法独立完成时,不得不面对的问题。就拿平时请假来说,试想如果是一个只有几个人的小公司,很可能连请假条都不用写,直接跟老板说一下就OK了,但是如果公司有一定规模,做为一个小小的螺丝钉,就未必有资格面见老板了,这时就不得不走审批流程了。我们都知道,通常情况下请假需要在OA上写请假单交由各级领导审批,不同级别的领导有权审批的天数不同,例如不超过1天的请假直接领导(TL)审批就可以了,而大于1天不超过3天就需要整个项目的负责人(PM)审批才可以,当然时间更长的,如7天,则可能需要交由CTO甚至更高级的领导审批。

这个例子就已经体现出责任的划分了,我们先用代码模拟一下这个请假场景,然后由此引出我们这次的主角---责任链模式。

示例

既然是请假,当然先得有请假单,而请假单又由申请和审批结果两部分组成,分别由请假人和审批领导填写,这不难理解,代码如下:

  1. public class LeaveContext
  2. {
  3. /// <summary>
  4. /// 申请
  5. /// </summary>
  6. public LeaveRequest Request { get; set; }
  7. /// <summary>
  8. /// 审批结果
  9. /// </summary>
  10. public LeaveResponse Response { get; set; }
  11. }

然后再来几个领导,每个领导都有在一定范围内处理请求的能力。经过前面那么多设计模式的熏陶,针对不同级别的领导抽象个管理者基类不难想到吧?

  1. public abstract class Manager
  2. {
  3. public string Name { get; set; }
  4. public Manager(string name)
  5. {
  6. Name = name;
  7. }
  8. public abstract void HandleRequest(LeaveContext context);
  9. }
  10. /// <summary>
  11. /// 团队领导者
  12. /// </summary>
  13. public class TL : Manager
  14. {
  15. public TL(string name)
  16. : base(name)
  17. {
  18. }
  19. public override void HandleRequest(LeaveContext context)
  20. {
  21. if (context.Request.LeaveDays <= 1)
  22. {
  23. context.Response = new LeaveResponse
  24. {
  25. Approver = "TL:" + Name,
  26. IsAgreed = true
  27. };
  28. }
  29. }
  30. }
  31. /// <summary>
  32. /// 项目经理
  33. /// </summary>
  34. public class PM : Manager
  35. {
  36. public PM(string name)
  37. : base(name)
  38. {
  39. }
  40. public override void HandleRequest(LeaveContext context)
  41. {
  42. if (context.Request.LeaveDays > 1
  43. && context.Request.LeaveDays <= 3)
  44. {
  45. context.Response = new LeaveResponse
  46. {
  47. Approver = "PM:" + Name,
  48. IsAgreed = true
  49. };
  50. }
  51. }
  52. }
  53. /// <summary>
  54. /// 首席技术官
  55. /// </summary>
  56. public class CTO : Manager
  57. {
  58. public CTO(string name)
  59. : base(name)
  60. {
  61. }
  62. public override void HandleRequest(LeaveContext context)
  63. {
  64. if (context.Request.LeaveDays > 3
  65. && context.Request.LeaveDays <= 7)
  66. {
  67. context.Response = new LeaveResponse
  68. {
  69. Approver = "CTO:" + Name,
  70. IsAgreed = true
  71. };
  72. }
  73. }
  74. }

每个领导都能对同一个请求进行处理,但是也各司其职,只做自己能力范围内的事,并且处理请求的角度和方式也大不相同(即代码实现上有较大的差异,这个例子过于简单,所以这点体现并不明显)。

再来看看调用的地方:

  1. static void Main(string[] args)
  2. {
  3. LeaveContext context = new LeaveContext
  4. {
  5. Request = new LeaveRequest
  6. {
  7. Applicant = "张三",
  8. Reason = "世界那么大,我想去看看",
  9. LeaveDays = new Random().Next(1, 10)
  10. }
  11. };
  12. TL tL = new TL("李四");
  13. PM pM = new PM("王五");
  14. CTO cTO = new CTO("赵六");
  15. if (context.Request.LeaveDays <= 1)
  16. {
  17. tL.HandleRequest(context);
  18. }
  19. else if (context.Request.LeaveDays <= 3)
  20. {
  21. pM.HandleRequest(context);
  22. }
  23. else if (context.Request.LeaveDays <= 7)
  24. {
  25. cTO.HandleRequest(context);
  26. }
  27. if (context.Response == null)
  28. {
  29. Console.WriteLine($"{context.Request.LeaveDays}天假期太长,没人处理请假申请,请假失败");
  30. }
  31. else
  32. {
  33. Console.WriteLine($"{context.Response.Approver}审批了{context.Request.Applicant}的{context.Request.LeaveDays}天请假申请");
  34. }
  35. }

上述代码有点多,但基本思路就是实例化请假单和若干领导对象,然后根据请假天数判断交给哪个领导处理,最后再将处理结果打印输出。有兴趣的话,不妨下载源码,多运行几次看看结果,逻辑还是相当严谨的。

状态模式改造

不过,逻辑虽然严谨,但作为一名优雅的程序员,我们不难挑出一些毛病,一方面,if...else太多,扩展性不好;另一方面,请假难度太大了,还容易出错,实际上,请假者只是想请个假而已,他也不知道谁有权处理,请个假总感觉领导在相互甩锅,管理可不就混乱了吗?
不过对于这两个问题,我们略微思索就会发现,前面遇到过,没错,就是状态模式,只不过状态模式是调用者不想关注系统内部状态的变化,而这里是不想关注内部审批流程的变化。状态模式的解决思路是将状态的设置转移到系统内部,即在一个具体状态类中处理完成对应的业务逻辑之后,设置下一个状态,这里不妨效仿一下。

  1. public class TL : Manager
  2. {
  3. public TL(string name)
  4. : base(name)
  5. {
  6. }
  7. public override void HandleRequest(LeaveContext context)
  8. {
  9. if (context.Request.LeaveDays <= 1)
  10. {
  11. context.Response = new LeaveResponse
  12. {
  13. Approver = "TL:" + Name,
  14. IsAgreed = true
  15. };
  16. return;
  17. }
  18. PM pM = new PM("王五");
  19. pM.HandleRequest(context);
  20. }
  21. }

其他几个管理者对象类似处理,这样一来,调用者就简单了,代码如下:

  1. static void Main(string[] args)
  2. {
  3. ...
  4. TL tL = new TL("李四");
  5. tL.HandleRequest(context);
  6. ...
  7. }

不过,调用者虽然简单了,但是把锅甩给了管理者,不妨再看看上面的TL类,不难看出面向实现编程了,违背了迪米特原则,进而也就违背了开闭原则,记得在状态模式中也同样有这个问题,我们当时是通过享元模式解决的,原因是状态是可以共享的,并且状态是系统内部的,外部不应该知道的。而这里情况有所不同,管理者对象是不可以共享的,外部也是可以访问的,因此,处理手段也就不同了。我们在Manager基类中添加NextManager属性,这也是一种依赖注入的手段,之前的设计模式我们用过了方法注入,构造函数注入,这是第三种注入方式---属性注入。

  1. public abstract class Manager
  2. {
  3. public Manager NextManager { get; set; }
  4. ...
  5. }

然后具体的实现类中,通过NextManager指向下一个管理者。下面以TL类为例:

  1. public class TL : Manager
  2. {
  3. ...
  4. public override void HandleRequest(LeaveContext context)
  5. {
  6. if (context.Request.LeaveDays <= 1)
  7. {
  8. context.Response = new LeaveResponse
  9. {
  10. Approver = "TL:" + Name,
  11. IsAgreed = true
  12. };
  13. return;
  14. }
  15. NextManager?.HandleRequest(context);
  16. }
  17. }

这样所有管理者类就又面向抽象编程,可以轻松扩展了。我们再来看看如何调用:

  1. static void Main(string[] args)
  2. {
  3. ...
  4. TL tL = new TL("李四");
  5. PM pM = new PM("王五");
  6. CTO cTO = new CTO("赵六");
  7. tL.NextManager = pM;
  8. pM.NextManager = cTO;
  9. tL.HandleRequest(context);
  10. ...
  11. }

乍一看,心里拔凉拔凉的,又变复杂了,好不容易甩出去的锅又被甩回来了。不过呢,虽然有瑕疵,但相对于最开始的实现,还是好了很多,至少,请假时只需要找直接领导就可以了,审批细节也不用再关注了,这就是责任链模式,下面看一下类图。

示例类图

定义

多个对象都有机会处理某个请求,将这些对象连成一条链,并沿着这条链传递该请求,直到处理完成为止。责任链模式的核心就是“链”,是由多个处理者串起来组成。

类图

  • Handler:抽象处理者角色,是一个处理请求的接口或抽象类;
  • ConcreteHandler:具体的处理者角色,具体的处理者接收到请求后可以选择将请求处理掉,或者将请求传递给下一个处理者。

责任链模式+模板方法模式

通过前面的代码和定义,我们可以看到,责任链模式其实并不完善,首先管理者子类实现中有逻辑和代码上的重复,例如都需要判断是否有权力处理请求,处理完之后都需要交给下一个处理者处理,并且这个流程是固定的。因此,我们可以进行如下改造,把公共固定的部分提取到基类中:

  1. public abstract class Manager
  2. {
  3. public Manager NextManager { get; set; }
  4. public string Name { get; set; }
  5. public Manager(string name)
  6. {
  7. Name = name;
  8. }
  9. public void HandleRequest(LeaveContext context)
  10. {
  11. if (CanHandle(context))
  12. {
  13. Handle(context);
  14. return;
  15. }
  16. NextManager?.HandleRequest(context);
  17. }
  18. protected abstract bool CanHandle(LeaveContext context);
  19. protected abstract void Handle(LeaveContext context);
  20. }

上述代码将算法骨架封装到了HandleRequest(LeaveContext context)方法中,然后将算法子步骤CanHandle(LeaveContext context)Handle(LeaveContext context延时到子类中实现,当然,由于子步骤不应该被外部直接调用,因此访问修饰符为protected,看到了吗?这是标准的模板方法模式。
再来看看具体子类如何实现,还是以TL为例:

  1. public class TL : Manager
  2. {
  3. public TL(string name)
  4. : base(name)
  5. {
  6. }
  7. protected override bool CanHandle(LeaveContext context)
  8. {
  9. return context.Request.LeaveDays <= 1;
  10. }
  11. protected override void Handle(LeaveContext context)
  12. {
  13. context.Response = new LeaveResponse
  14. {
  15. Approver = "TL:" + Name,
  16. IsAgreed = true
  17. };
  18. }
  19. }

子类更加干净清爽,职责也更加单一了,只需关心自己的处理逻辑,甚至都不用关心做完之后该交给谁,扩展起来更加容易了。

责任链模式+建造者模式

除此之外,另外一个问题其实更加明显,就是前面说的锅甩来甩去还是回到了调用者身上,虽说调用者不再需要知道每个领导的审批权限范围,但是除了指定自己的领导,还得指定领导的领导,领导的领导的领导,这其实也不合理,出现这个问题的原因是什么呢?原因是不符合常理,我们忽略的一个很重要的部门---人力行政部(HR),请假流程应该是他们提前制定好的,而不是每次请假时临时制定的。

因此,要解决这个问题,首先得加入一个HR类,用于管理请假审批流程:

  1. public class HR
  2. {
  3. public Manager GetManager()
  4. {
  5. TL tL = new TL("李四");
  6. PM pM = new PM("王五");
  7. CTO cTO = new CTO("赵六");
  8. tL.NextManager = pM;
  9. pM.NextManager = cTO;
  10. return tL;
  11. }
  12. }

然后,再看看调用的地方:

  1. static void Main(string[] args)
  2. {
  3. ...
  4. HR hR = new HR();
  5. Manager manager = hR.GetManager();
  6. manager.HandleRequest(context);
  7. ...
  8. }

又变得简单了,并且也更合理了。不过整体上还是有点自欺欺人,因为新的HR类又面向实现编程,变得难以维护了,因此,还得改进,改进方法还是老套路,面向抽象编程,然后通过集合管理多个实例,具体代码如下:

  1. public class HR
  2. {
  3. private List<Manager> _managers = new List<Manager>();
  4. public void AddManager(Manager manager)
  5. {
  6. _managers.Add(manager);
  7. }
  8. public Manager GetManager()
  9. {
  10. Manager currentManager = null;
  11. for (int i = _managers.Count - 1; i >= 0; i--)
  12. {
  13. if (currentManager != null)
  14. {
  15. _managers[i].NextManager = currentManager;
  16. }
  17. currentManager = _managers[i];
  18. }
  19. return currentManager;
  20. }
  21. }

这里直接一步到位了,但是应该能看懂,不过,大家有没有看出这是建造者模式呢?没看出也没关系,我们后面会再改进一次,毕竟HR没面向抽象编程,光秃秃的看着也不舒服。但在此之前,我们先看看调用的地方:

  1. static void Main(string[] args)
  2. {
  3. ...
  4. HR hR = new HR();
  5. hR.AddManager(new TL("李四"));
  6. hR.AddManager(new PM("王五"));
  7. hR.AddManager(new CTO("赵六"));
  8. Manager manager = hR.GetManager();
  9. manager.HandleRequest(context);
  10. ...
  11. }

看到这里的朋友怕是要开骂了,这到底是想干啥,封装来封装去,来来回回好几次,最后还是回到了原点。但事实上,已经有了很大的不同,第一次,调用者对业务逻辑有了较深的耦合,例如调用者必须知道每个领导的审批权力;第二次,耦合度降低了,但还是需要知道调用链的关系,而第三次,也就是这一次,其它的都不用知道,只需要创建对象就可以了,而创建对象是无论如何都绕不开的。并且,我们经过一次次的改进,看似还是回到了原点,但实际上已经将变化一步步从程序内部抛到了程序的最外层,可以通过依赖注入进一步解耦了,我们不妨换成ASP.Net Core应用程序看看,同时我们再进行最后一次改造:

  1. public interface IManagerBuilder
  2. {
  3. void AddManager(Manager manager);
  4. Manager Build();
  5. }
  6. public class ManagerBuilder: IManagerBuilder
  7. {
  8. private readonly List<Manager> _managers = new List<Manager>();
  9. public void AddManager(Manager manager)
  10. {
  11. _managers.Add(manager);
  12. }
  13. public Manager Build()
  14. {
  15. ...
  16. }
  17. }

其实这次改造并没有实质性的变化,仅仅是换了个名字并且加了个抽象的接口而已,目的是为了方便看出它确实是建造者模式。重点是如何使用它,先添加如下针对IServiceCollection的扩展方法。

  1. public static class ManagerServiceCollectionExtensions
  2. {
  3. public static IManagerBuilder AddManagers(this IServiceCollection services)
  4. {
  5. IManagerBuilder managerBuilder = new ManagerBuilder();
  6. managerBuilder.AddManager(new TL("李四"));
  7. managerBuilder.AddManager(new PM("王五"));
  8. managerBuilder.AddManager(new CTO("赵六"));
  9. services.AddSingleton(managerBuilder);
  10. return managerBuilder;
  11. }
  12. }

然后在Startup中调用,代码如下所示:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddControllers();
  4. services.AddManagers();
  5. }

好了,就是这么简单,接下来就可以在项目的任何地方通过依赖注入的方式使用了,当然,AddManagers(this IServiceCollection services)还有瑕疵,但是,这可以通过配置文件或者读取数据库的方式解决,这里就不再继续深入下去了。

优缺点

优点

责任链模式最大的优点就是将请求和处理分离,请求者可以不用知道是谁处理的,处理者也可以不用知道请求的全貌,两者解耦,提高系统的灵活性。

缺点

性能不高

既然责任链模式的核心是“链”,就说明每次请求都需要遍历整个链条,这必然会带来较大的性能损耗,不过事实上,责任链模式并非必须使用链条,我们知道数据结构中有数组和链表两种结构,而我们前面刚学过的观察模式就和责任链模式就有类似的关系,观察者模式中通过集合保存所有的观察者,然后遍历集合,责任链模式也可以采用相同的手段,不过责任链模式采用集合保存所有处理者之后,或许就变成观察者模式了,但是这重要吗?

调试不方便

责任链模式采用了类似递归的方式,调试的时候逻辑可能比较复杂。

总结

责任链模式通常可以用在含有流程的业务中,如工作流,流水线,请求流等,当然也可以将一个大的功能块切分成若干小块,然后通过责任链模式串联起来,责任链模式常见于各种框架中,是代码重构的利器,不过由于其性能不高,逻辑相对复杂,并且如果责任划分不清,很容易产生误用,带来的可能是灾难,因此也需要慎重使用。况且,能通过责任链模式实现的场景往往也可以通过其它模式代替,如策略模式,状态模式,观察者模式等。另外,责任链模式的每个处理者也可以只处理请求的一部分,ASP.Net Core中的中间件就是典型例子,还有前面请假的例子,在有些公司,不管请多少天假,可能都需要所有领导逐级审批,所有领导都同意才算通过,只要有一个不同意就不通过,这依然是责任链模式。

责任链模式使用起来可以非常灵活,实现方式也不止一种,但很少单独使用,更多时候还需要搭配其他模式一起使用,因此,要用好责任链模式也别忘了复习其它设计模式哦!

源码链接

原文链接:http://www.cnblogs.com/FindTheWay/p/14881519.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号