范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

责任链模式一门甩锅的技术

  这到底是谁的锅
  我们在平常的业务开发中,经常会遇到很多复杂的逻辑判断,大概的框架可能是像下面这样子的:    public void request() {         if (conditionA()) {             // ...             return;         }          if (conditionB()) {             throw new RuntimeException("xxx");         }          if (conditionC()) {             // do other things         }     }      private boolean conditionC() {         return false;     }      private boolean conditionB() {         return false;     }      private boolean conditionA() {         return true;     } 复制代码
  如果是简单的、不多变的业务,倒也没什么大问题。但要是在比较核心的、复杂的业务,且同一个系统的代码有多人去维护,那么要在上面的这段代码中插入一段逻辑将会非常困难。在实现时,你可能会遇到这些问题:实现前:我需要考虑这个逻辑分支,应该加在什么地方才能满足要求?会不会被前置条件拦截了?实现时:逻辑分支要用到一些函数参数,这些参数会不会对后续的逻辑有影响?而且参数会不会被前面的哪些参数给修改掉了?这样我就得去了解前后所有的判断逻辑才能正确实现我要的功能,不然就是要碰碰运气,赌它没改过了...实现后:加完之后,我要怎么测试?貌似还得构造条件让前面的判断都通过才行...还是要了解前置条件的逻辑
  如果放到真实的业务场景,遇到的问题可能还不止这些。不禁感叹,我就想多加一个逻辑分支!怎么就这么难!
  有什么办法解决这些问题呢?显然,当要实现一个功能时,需要了解的细节太多了,不符合单一职责的原则。无论是新增逻辑还是修改逻辑,都是有很强的侵入性的,也不符合开闭原则。我前后的逻辑细节不是我负责的,我要把这些锅甩出去才行,要更好地甩锅,那么这时候就要用到责任链模式了。甩锅的套路
  责任链,顾名思义,是一个链条,链条中有很多个节点。映射到数据结构上,则是一个有序的队列,队列中有很多个元素,每个元素都独立处理自己的逻辑,并且在处理完之后,将流程传递到下一个节点。所以,在这个模式里,可以抽象出两个角色:链和节点。其中,链负责处理请求和组装节点,而每个节点则负责处理自己的业务逻辑,无需关心这个节点的上下游是如何处理的。
  因此,从用例的视角来看,可以得出下面的用例图:
  那么,责任链是否可以解决上述的问题呢?上述问题,其实对应着下面的几个问题:要满足需求,应该在什么地方实现需求?实现这个需求会不会对其他模块带来影响,其他模块又会不会对我实现的逻辑带来什么影响?需求实现后该如何测试?
  由责任链的角色划分可以很清楚地知道:对于第一个问题,应该是链条这个角色应该关心的,从业务的视角来安排节点在哪个位置实现即可。对于第二个问题,需求的实现由节点负责。对于责任链中的入参,只提供读方法,不提供写方法,这样可以很好地避免某个节点偷偷篡改参数的风险,对于其他节点来说,无需担心其他节点对入参进行了修改。每个节点之间的职责分明,由责任链本身的结构就决定了模块之间的影响很小。对于第三个问题,节点的逻辑实现后,只需对节点逻辑本身做测试,至于能否逻辑能否执行到这个节点上,则由链条设置的节点顺序做保证。测试时只需保证顺序正确即可,完全没有必要从请求开始的地方开始执行,构造一堆条件让代码执行到自己的逻辑上。
  上面提到的问题,都可以利用责任链模式很好地解决这些问题。那又该如何实现责任链模式呢?是时候展现真正的甩锅技术了
  按照上面用例图的定义,链条负责管理节点,是请求的入口,而节点是链条的其中一环。那么这两者的关系属于聚合关系。得到类图如下:
  甩锅秘技一
  如果我们在Spring框架的基础上进行开发,那么我们很容易就可以实现一个简单的责任链模式:@Component @RequiredArgsConstructor public class PolicyChain1 {      private final List> policies;      public void filter(ContextParams contextParams) {         policies.forEach(policy -> {             policy.filter(contextParams);         });     } } 复制代码
  只要将Policy的实现类也标记为Component,那么Spring的自动注入机制帮我们实现了addPolicy的方法,摆脱了繁琐的添加节点的过程代码。
  但是,这有一个很严重的问题,要怎么控制每个Policy之间的顺序呢?这时可能你会想到用@Order注解解决这个问题。但是假设Policy有几十个,如果你需要在第10和第11个Policy插入一个Policy,那么是不是要将从第11个开始往后的所有Policy都调整一下顺序?想想都觉得麻烦。因此,这种方式,只能用于对顺序无要求的情况,比如用来做权限校验时,各个校验条件互不相关,也无先后顺序的限制,那么就可以用这种方法实现,扩展性强,实现也简单。甩锅秘技二
  可是需求上要求一定要按顺序,那该怎么办呢?上面已经分析了指定Order的方式不可取,还有什么方法呢?
  其实上面的方法其实和插入一个数组时的操作十分相似。当要在数组中间插入一个元素时,插入位置之后的元素都要往后挪一位。对应上述的做法,其实就是对应的Policy的Order值要加1。那么类似地,数组对插入的效率低,那换个效率高的做法,不就是链表么?我们可以将每个Policy都持有下一个要处理的Policy的引用,当这个Policy处理完之后,调用下一个Policy的filter方法,然后再将上一个Policy的引用修改一下,不就可以很好地完成插入操作了么?
  先画一个类图
  这样组织之后,PolicyA需要持有PolicyB的引用,PolicyB也需要持有PolicyC的引用。当需要在B和C之间加入一个D时,那么我就需要将B中的引用指向D,然后D再指向C即可。
  但是,这样组织之后,我并不知道这个链条的全貌,这个链条有哪些节点、顺序是怎样的,我并不能一下子推断出来了。另外,这和上面推断出来的用例图不符,在用例中,链条才是负责节点的组装的,现在相当于甩给了每个节点去做了,这明显违反了单一职责原则啊!
  既然这样,那我仍然把节点组装放到链条里实现,节点只实现逻辑,只是在组装的时候,可以让使用方显式指定顺序,这样不就好了吗?
  大概的实现是这样:@Component @AllArgsConstructor public class PolicyChain2 {      private SessionJoinDeniedPolicyHandler sessionJoinDeniedPolicyHandler;      private SessionLockPolicyHandler sessionLockPolicyHandler;      private SessionPasswordPolicyHandler sessionPasswordPolicyHandler;      @PostConstruct     public void init() {         sessionLockPolicyHandler.setNextHandler(sessionJoinDeniedPolicyHandler);         sessionJoinDeniedPolicyHandler.setNextHandler(sessionPasswordPolicyHandler);     }      public void filter(ContextParams params) {         sessionLockPolicyHandler.filter(params);     } } 复制代码
  这样链条本身就需要知道各个节点都是什么,这样才能把不同的节点组装起来。// 策略抽象类 public abstract class PolicyHandler {      private PolicyHandler nextHandler;      void setNextHandler(PolicyHandler handler) {         nextHandler = handler;     }      public void filter(T context) {         doFilter(context);         if (nextHandler != null) {             nextHandler.filter(context);         }     }      protected abstract void doFilter(T context); }  // 策略实现类 @Component public class SessionPasswordPolicyHandler extends PolicyHandler {      @Override     public void doFilter(ContextParams context) {         String requestParam = context.getRequestParam();         if (Objects.equals(requestParam, "ok")) {             return;         }          throw new RuntimeException("session password throw exception");     } } 复制代码
  对于节点本身,就只需要关注自身处理的业务逻辑了,使用方只要调用一下PolicyChain的filter方法,接下来的逻辑都会自动按顺序完成了!
  看来这样的实现差不多就可以满足需求了!直到...我用这个秘技实现了一个计数器的时候...
  需求是这样的,为了减少数据库的压力,我在一个加入房间的方法上加了一个注解,并用秘技二实现了一个计数器,以用于校验加入的人数是否超出了房间限制的大小,这样可以减少对数据库的查询次数。实现代码大致如下:    @ValidatePolicy     public void filter() {         join();     }  // validate注解对应的拦截方法,此处省略了切面类的相关代码,仅展示核心内容     public void validate() {         strategyRouter.applyStrategy(ContextParams.builder()                 .isJoinDenied(false)                 .isLocked(false)                 .password("123")                 .build());     } 复制代码
  直到有一天,房间限制3人加入,此时房间里有2个人,执行join方法,validate()方法愉快地通过了校验并将自身的计数器设置为3,但是在执行join方法的时候抛出了异常,原本应该加入成功的第3个人并没有加入成功。接着第4个人加入房间,因为房间内只有2个人,那么第4个人应该是加入成功的,但是因为计数器已经被设置为3,那么第4个人直接在校验阶段就抛异常了...
  那么,在秘技2的基础上,当执行后面的方法出异常时捕获异常,然后把计数器校正就好了!可是,在这种实现方式下根本做不到,因为每个节点只专注于处理自己成功拦截时的逻辑,而忽略了自身逻辑处理完之后,后续逻辑出了异常时该怎么办的情况。由此可得,秘技二能处理有顺序的节点,能用于无状态的前置校验,但无法支持后续逻辑出现异常时,节点本身还需要处理回滚操作的情况。甩锅秘技三
  基于上面的问题,我需要找到一种能支持回滚的实现方式。这时 我参考了Spring Cloud Gateway中Filter的实现,发现有几个特点:每个节点会依赖链条本身,当要执行下一个节点的处理逻辑时,只需要调用chain.filter()方法即可。将节点顺序的定义和节点的创建分开,避免了链条对具体节点的依赖,对节点的创建,可以通过工厂模式实现,增强了扩展性。
  大致类图如下:
  首先我们看如何支持顺序。在FilterRouter中,有一个loadFilterDefinitions的方法,子类可以重写这个方法以定义责任链中存在哪些节点。链条本身变得不关心节点的顺序了,转而将节点顺序的处理委托给另一个对象。同时,除了可以支持在FilterRouter用代码显式定义之外,还可以通过重写loadFilterDefinitions的方式,从不同的来源指定节点顺序,比如配置文件、外部系统等,使得顺序的定义更灵活,扩展性更强。@RequiredArgsConstructor public abstract class FilterRouter {      private final Map> filterFactories;      public List> getFilters(T filterChainContext) {         final List filterDefinitions = new ArrayList<>();         loadFilterDefinitions(filterChainContext, filterDefinitions);         List> filters = filterDefinitions.stream().map(filterDefinition -> {             FilterFactory filterFactory = filterFactories.get(filterDefinition.getName());             return filterFactory.apply();         }).collect(Collectors.toList());         filterDefinitions.clear();         return filters;     }      protected abstract void loadFilterDefinitions(T filterChainContext, List filterDefinitions); }  @Component public class DefaultFilterRouter extends FilterRouter {       public DefaultFilterRouter(Map> filterFactories) {         super(filterFactories);     }      @Override     protected void loadFilterDefinitions(String filterChainContext, List filterDefinitions) {         filterDefinitions.add(new FilterDefinition(PasswordFilterFactory.KEY));     } } 复制代码
  接下来我们看下节点操作如何支持回滚。通过实现FilterFactory接口,可以在apply方法中执行自身的校验逻辑,并对后续的处理捕获异常,当捕获到异常时,在异常处理的代码块中处理回滚异常。另外,借助Spring框架的自动注入,将Factory声明为Component,这样FilterRouter在收集Filter实现时,也免除了繁琐的add方法。@Component public class PasswordFilterFactory implements FilterFactory {      public static final String KEY = "passwordFilterFactory";      @Override     public Filter apply() {         return (filterChainContext, filterChain) -> {             // validate             try {                 return filterChain.filter(filterChainContext);             } catch (Exception e) {                 // rollback             }              return "";         };     } } 复制代码
  至于DefaultFilterChain这个类,做的事情就是接收请求,将通过FilterRouter的FilterFactory生成Filter列表而已。代码如下:public class DefaultFilterChain implements FilterChain {      private final T filterChainContext;      private int index = 0;      private final List> filters = new ArrayList<>();      public DefaultFilterChain(FilterRouter filterRouter, T filterChainContext) {         this.filterChainContext = filterChainContext;         filters.addAll(filterRouter.getFilters(filterChainContext));     }      public R filter() throws Throwable {         return filter(filterChainContext);     }      @Override     public R filter(T filterChainContext) throws Throwable {         int size = filters.size();         if (this.index < size) {             Filter filter = filters.get(this.index);             index++;             return filter.filter(filterChainContext, this);         }          return null;     }      public void addLastFilter(Filter filter) {         filters.add(filter);     } } 复制代码
  使用时的代码:@Component @RequiredArgsConstructor public class Client {      private final DefaultFilterRouter defaultFilterRouter;      public void filter(String param) throws Throwable {         DefaultFilterChain filterChain = new DefaultFilterChain<>(defaultFilterRouter, param);         filterChain.filter(param);     } } 复制代码
  至此,最后一种实现方式,既可以满足对节点顺序性的要求,也可以支持节点对后续逻辑出错时的后置处理。同时也具备比较好的扩展性,可以实现从不同来源加载节点顺序,可以通过FilterFactory实现不同的Filter。接着将第3种秘技封装成组件,这样业务在接入的时候就可以优雅甩锅了。甩锅总结
  上述列举了3种责任链模式的实现方式,可以分别应对三种场景:对节点顺序无要求,可用秘技一,实现方式比较简单对节点顺序有要求,且所有节点的处理都是无状态的,不需要进行后置处理的,可使用秘技二对节点顺序有要求,且有其中一个节点的处理是有状态的,需要进行后置处理的,可使用秘技三
  设计模式经典书籍《设计模式:可复用面向对象软件的基础》中有一句话提到,"找到变化,封装变化"。其实这是设计模式的底层逻辑。
  回顾整个过程,我们可以看到:从流水账式的代码,到秘技一,变化的是新增一段插入逻辑,最终封装的效果,正是让这段插入逻辑变成了其中一个节点的处理逻辑。从秘技一到秘技二,变化的是需要支持节点顺序,而最终封装的效果,则是将顺序的定义内聚在了链条的内部,支持了自定义顺序。从秘技二到秘技三,变化的是节点需要支持回滚,支持后置处理,而封装的结果,就是将后续的处理的逻辑暴露给节点,但节点依赖的是链条本身,将后续的处理逻辑屏蔽起来,节点依然聚焦在自身的处理逻辑上。
  由此可见,过程式的代码,到设计模式的演进,都并不是凭空捏造的,而是由问题出发,找到其核心的变化点,并对变化点进行封装和抽象,才慢慢形成最终比较理想的结果。

LOL11。4版本改动名单沙弥拉彻底废了!剑姬盲僧将加强小维就想问下,你们见过一个英雄在一个版本内Q,W,E,R,被动全部被削弱吗?没见过吧?好的下个版本就可以看到了!沙漠玫瑰莎弥拉悍勇本色(被动)近战伤害的总AD加成由0。075降低至ADC逆天改命!10。25版本AD获史诗级加强英雄联盟S11已经上线好一段时间了,而s11改动幅度最大的无疑是装备,可以说几乎是把以前十年建立的商店系统推到重做,改动非常的大,而且推出神话传说装备的概念,把很多玩家绕晕了,但时字节跳动硬气!和美国正面刚,或宁可关停也不愿被收购美国当地时间8月24日,TikTok一纸诉状正式将美国政府告上法庭,理由是总统各种越权,禁令不符合法律程序等。同时,按之前的禁令规定,TikTok美国业务若未能在9月15日之前跟美亚索胜率一夜间飙升29名!10。25版本最强亚索攻略前两天英雄联盟10。25版本正式国服上线,此次改动非常大,虽说是有人欢喜有人忧,但对于亚索玩家来说绝对是皆大欢喜!10。25版本更新后,根据OPGG韩服显示,亚索胜率直接怒飙29名11。4版本改动剑姬男刀盲僧奶妈增强!卡莎卡米尔鳄鱼被削弱今天设计师公布了11。4版本即下个版本的英雄改动名单!大家一起跟小维看看吧!英雄增强剑姬奶妈韦鲁斯布隆金克丝小法男刀盲僧蛮王木木小维点评这次加强的英雄很多!很多英雄弱势是因为装备的为什么米国总是打压华为,而不打压小米OV?近两年华为可是吃尽了苦头,被米国疯狂打压,最近升级禁令更是让华为自研芯片没办法生产,也禁止华为i购买联发科芯片,华为面临没芯片用的艰难困境。而同为国产厂商的小米,oppo,vivo华为真的要倒了!华为通知供应商停止供货华为这次真的碰上大麻烦了。今年受美国第二波制裁升级影响,华为真的是举步维艰了。那第二轮制裁到底影响到华为什么了?简而言之,就是华为在手机芯片领域十几年的投入几乎在一瞬间付之东流了,诺手剑姬瑞雯哭了!LOL11。3版本装备更新,ADC舒服了11。3版本拳头又对装备进行一大波改动,受影响的英雄很多,那具体改动了什么来一起看下吧!剑姬至臻皮肤装备削弱亡者的板甲生命值由475降低为400小维点评血量减少,性价比更低了,让原11。5装备改动挺进黑切加强诺手起飞!破盾加强璐璐哭了近期拳头设计师分享了关于11。5版本的改动内容,其中装备改动或将对一些英雄影响非常大,那到底要改些什么呢,小维带大家一起看看吧!(PS国服现在还是11。3版本,预计将于3月4日直接在头条上获得金币的多少和什么有关?今天我就来给大家讲下昨天收入了46万多金币,也突破了前天的43万多的记录,连续两天40多万了,有点小激动,之前不会玩,错失了好多机会,想想有点可惜,现在找到了提高金币收入方法,分享出来给大家,帮助大家LOL11。13版本英雄改动厄斐琉斯站起来了!盲僧锐雯挨刀近期设计师分享了关于11。13的相关改动,有不少英雄进行了加强和削弱,具体调整了什么,有什么影响,一起看看吧!(注设计师分享内容,正式服未上线,不代表最终结果,国服以官方公告为准!
DNF为什么有玩家称剑魂传说勋章的守护珠要打双属强,不可以打负重,命中,暴击吗?DNF传说勋章是所有装备中比较重要的装备,搭配完美守护珠的情况下对于伤害能带来23的提升,一般来说守护珠的选择都是属强,暴击,移动,攻速释放,但是对于剑魂来说却是个例外。如果你去翻为什么大蛇丸忍术是无属性的?你好,这个问题就让小火来回答吧!首先在动漫火影忍者中,大蛇丸的官方查克拉属性设定是火风雷土水阴阳。而并非是无属性的。其次当我们回顾大蛇丸的历次战斗后,却惊奇的发现大蛇丸在战斗中所使热血传奇为何说30区是变态区?这里爆的装备属性你知道是多少吗?我们玩游戏时经常会听到一些特殊的服务器,什么变态服村服鬼服等等。当年盛大的热血传奇也存在这样的服务器,被玩家戏称为变态区。这些区服以极品装备泛滥而闻名,普通玩家做梦都得不到的极品,论WCG赛事的影响力,重启的WCG会让魔兽争霸3重新点燃赛事氛围吗?2019年,对于魔兽争霸爱好者来说,将是兴奋的一年具有电竞奥运会之称的WCG,自2013决赛结束后停办,暂离六年之久,今重启赛事,war3回归WCG正式比赛项目同时,魔兽争霸3重制LOL新手推荐去哪个区?先来给大家介绍一下联盟各大区英雄联盟国服总共有27个区电信艾欧尼亚祖安诺克萨斯班德尔城皮尔特沃夫战争学院巨神峰雷瑟守备钢铁烈阳裁决之地黑色玫瑰暗影岛均衡教派水晶之痕影流守望之海征服3。50棱镜更新,迈向次世代的无人深空还能再战十年近日画饼界巨人无人深空,迎来了最新的大型免费更新,该版本命名为棱镜(Prism),是游戏的3。50版本。这次更新有别于此前游戏每次更新都会添加新的玩法,或者干脆改变玩法的特点,此次梦嫂到底有多爱梦泪,看到她直播间取得一个标题,情侣羡慕极了在KPL的众多队伍中,AG超玩会有着很多的粉丝。之所以造成这种情况,主要还是因为2点原因。首先AG的成立时间比较早,在第一届KPL的时候,他们就已经存在了。其次AG超玩会的实力很强净化虽好可别乱用!遇到这3位英雄开净化,会适得其反害死你在王者荣耀中,净化是一个出场率很高的召唤师技能但是你知道吗?净化还隐藏着一些神奇作用,比如当你受到防御塔伤害的时候,使用净化可以一瞬间无敌,免疫掉防御塔伤害,不过呢净化虽好也需要慎体验服地震级更新,扁鹊米莱狄史诗级加强,登顶T0的梦奇凉凉在王者荣耀中,体验服又成为先行服,是正式服的先行版,也是正式服上线之前各项数据测试的重要依据。因此,充分了解体验服的更新信息,有助于我们更好地掌握未来版本的趋势。尤其是在S23赛季吃鸡新步枪FMR全面解析,自带8倍子弹不下坠,近战化身喷子大家好,欢迎来到由小鱼干开讲的吃鸡新鲜事速报,6月体验服终于是更新了,这个版本光子继续保持有多花哨搞多花哨的做法,弄出了重启未来的新版本。正如大家所料,重启未来玩法再次加入经典海岛热血传奇总是打不到自己用的装备,也别总怀疑是GM捣鬼那些年我们一起玩过的传奇第十二期奇怪的爆率不知道喜欢玩传奇的小伙伴们你们在玩传奇的时候,遇没遇到过这种事情,打到的装备很多时候都不是自己用的!很多玩传奇的小伙伴们相信都有类似的经历