专栏电商日志财经减肥爱情
投稿投诉
爱情常识
搭配分娩
减肥两性
孕期塑形
财经教案
论文美文
日志体育
养生学堂
电商科学
头戴业界
专栏星座
用品音乐

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

  这到底是谁的锅
  我们在平常的业务开发中,经常会遇到很多复杂的逻辑判断,大概的框架可能是像下面这样子的:    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种责任链模式的实现方式,可以分别应对三种场景:对节点顺序无要求,可用秘技一,实现方式比较简单对节点顺序有要求,且所有节点的处理都是无状态的,不需要进行后置处理的,可使用秘技二对节点顺序有要求,且有其中一个节点的处理是有状态的,需要进行后置处理的,可使用秘技三
  设计模式经典书籍《设计模式:可复用面向对象软件的基础》中有一句话提到,"找到变化,封装变化"。其实这是设计模式的底层逻辑。
  回顾整个过程,我们可以看到:从流水账式的代码,到秘技一,变化的是新增一段插入逻辑,最终封装的效果,正是让这段插入逻辑变成了其中一个节点的处理逻辑。从秘技一到秘技二,变化的是需要支持节点顺序,而最终封装的效果,则是将顺序的定义内聚在了链条的内部,支持了自定义顺序。从秘技二到秘技三,变化的是节点需要支持回滚,支持后置处理,而封装的结果,就是将后续的处理的逻辑暴露给节点,但节点依赖的是链条本身,将后续的处理逻辑屏蔽起来,节点依然聚焦在自身的处理逻辑上。
  由此可见,过程式的代码,到设计模式的演进,都并不是凭空捏造的,而是由问题出发,找到其核心的变化点,并对变化点进行封装和抽象,才慢慢形成最终比较理想的结果。

WE大腿正式回归,全员冲击洲际赛!粉丝国产第一上单回来了!还有两天时间就要迎来紧张刺激的季后赛,首场比赛将在WE和JDG战队中展开。对此微笑在直播中表示,希望WE能够好好表现进入四强。这个春季赛的WE可以说是跌宕起伏,从赛季初的连败到赛季王者荣耀碎片商店更新,哪些英雄皮肤值得换?文电竞资讯日常番原创,盗载必究。王者荣耀30号更新,碎片商店也总算是换新了,这次上线了40多款皮肤,其中还包括貂蝉吕布的圣诞系列,但除了这两款就没别的好换了吗?当然不!下面4款换了熊孩子买下全套虎年限定皮肤,父母上线询问账号价格,网友六块文陌瑾引言陌瑾出品,争做精品!先赞后看,一键三连,养成习惯!新年新气象,祝大家虎年行大运!在新的一年,陌瑾身边又出现了一个熊孩子,大家应该都知道,王者荣耀这款游戏在每个新年的节假日王者荣耀小虎集市2月4号最新代码必得史诗皮肤不用愁大家好我是游戏分享家今天就是大年初一给大家带来2月4号小虎集市最新代码想要的小伙伴直接复制就可以了,喜欢的可以分享给身边的朋友们欢迎点赞评论收藏哟关注不迷路每天持续更新大量小虎集市春节忍住了没充钱,看到元宵节上架皮肤,才知道终究躲不过要破财都说正月里来是新年,但是我们都知道,随着社会的快速发展,现在大家基本上在家里待到初三,初四又该奔波在外。所以现在的新年差不多也就是春节当天和初一,初二。在春节期间大家看到了官方上架买的第一个皮肤第一次玩王者荣耀你买的第一个皮肤是哪个英雄的皮肤?是什么皮肤呢?小编第一次玩这个王者荣耀的时候是想着不买皮肤的。因为不太知道自己能坚持玩多久,总觉得花钱就是亏了,到时候不玩了都是浪打王者荣耀你有没有那个最不想面对的英雄?对面最不想面对的狗链三雄张良一技能封走位,二技能封走位,大招拴住一套输出东皇前期反野强无敌,一打五如儿戏,后期r闪毫无脾气老夫子前两个老哥还是强控,昏昏沉沉的就挂了,这个老哥是直接奇拉比的技能自带霸体而且扫地,墙角大招接技能,为什么却被玩家称为弟弟忍者?奇拉比刚上架的时候,很多人都认为这个忍者非常无解,因为他的尾兽化技能自带霸体效果,并且还提升了移动速度,基本上对方如果没有抓取技能或者控制类秘卷,就只能等着挨揍。但是,这样的场景维王者荣耀百里守约影响游戏平衡应该去掉这个英雄吗?感谢诚邀!本栏目由忆涵出品原创撰写大家好,我是忆涵,说真的,忆涵也挺讨厌百里守约这个英雄的,忆涵一般比较喜欢玩中单法师的位置,遇到对面有百里守约的时候,经常被狙击两下,又得回家,超刺激战场这个游戏,开陀螺仪的玩家是大神还是异类?现在的刺激战场玩家,如果以陀螺仪的使用来划分,可以分为两类。一个就是使用陀螺仪的玩家,另一个就是非陀螺仪玩家。我先说我自己,我在玩刺激战场的时候,是属于陀螺仪玩家一派。没有陀螺仪,王者荣耀程咬金出完鞋子后,第二件装备出极寒风暴还是红莲斗篷好?为什么?专注游戏二十年。三岁玩积木,四岁霸王大陆扔爆炎,五岁战纪走雷道,六岁吞食借东风,七岁带黄金斯拉姆最近专治王者各种疑难杂症。程咬金不死鸟先出必出,我玩振兴之铠时程咬金就一直这样理解。
消息队列之事务消息,RocketMQ和Kafka是如何做的?今天我们来谈一谈消息队列的事务消息,一说起事务相信大家都不陌生,脑海里蹦出来的就是ACID。通常我们理解的事务就是为了一些更新操作要么都成功,要么都失败,不会有中间状态的产生,而AMySQL不会丢失数据的秘密,就藏在它的7种日志里进入正题前先简单看看MySQL的逻辑架构,相信我用的着。MySQL逻辑架构MySQL的逻辑架构大致可以分为三层第一层处理客户端连接授权认证,安全校验等。第二层服务器server层,这年头苹果还在卖情怀,行得通吗?网友不靠情怀靠实力一段白话丨iPhoneSE经问世以来,大多评测机构评价其为清库存的电子垃圾。用户们也对其褒贬不一。尽管如此,iPhoneSE已经突破百万销量,用户还是愿意为这款电子垃圾买账。iPh用GO语言写一个数据库连接池池(Pool)是指某类资源的容器,它是一种用于提高程序效率和降低系统开销的技术,比如线程池连接池内存池对象池。但它们的核心理念一致资源复用。本文主要探究数据库连接池的相关问题,并实一段白话丨IQOONeo3干翻友商,搅局高端市场,游戏发烧友的福音4月23日,VIVO旗下独立品牌IQOO发布了一个重磅炸弹,直接炸翻友商,那就是IQOONeo3。发布会刚刚结束,3000可以买到的骁龙865旗舰,就成为了机友们心中的性价比之王。一段白话丨5G命运多舛,普及前反转再反转,网友还会再反转吗?13日,中国电信发布了中国电信5GSA安全增强SIM卡白皮书,意在明确用户卡的发展方向。但该白皮书内容被部分网友错误解读为用5G需要换SIM卡,原因是白皮书指出,现有4G不能满足5一段白话丨在乌江漂流过的iPhone,防水功能究竟是刚需还是鸡肋?近日,一则手机掉乌江8个月后回来了的新闻,吸引网友的关注。据报道,2019年9月,贵阳的周先生在遵义乌江乘独木舟的时候,装在防水袋里的手机掉进江里。今年5月,一个陌生电话打来,对方一段白话丨一加8Pro隐藏透视黑科技?官方收起你们的大胆想法近日,一个关于一加8Pro的消息在国外火了,那就是国外网友们用一加8Pro拍摄时意外发现一加8Pro相机可以透视。是的并没有看错,其后置4摄模组经过包括MaxWeinbach及an明日之后玩家与游戏策划直面硬刚,末日大片只用手机看怎能过瘾?明日之后持续两周的种种异样,预兆着危机将至。就在5月28日,明日之后进行了一次重大更新。在新版本中,官方引进火山大爆发自然灾害,需要玩家共同应对。在船新版本中,策划团队为了增强玩家从一块披萨开始,被AI复刻,这款游戏究竟是复古还是潮流?20世纪80年代,一名游戏设计师看着吃了一块的披萨陷入沉思,不久后,一款游戏横空出世,一度霸占世界各地的游戏机屏幕,成为史上最早的游戏IP。这名游戏设计师叫岩谷彻,这款在80年代风写了三百篇算法题解,关于如何刷题有些话我想对你说这篇文章憋了我挺久的,感觉都快憋出内伤,一次次的打开Typora写几十个字,一次次的修改删除最后关闭Typora,如此反复。为什么会如此纠结?或许是太狂妄了,我真的想让那些看了这篇
友情链接:快好知快生活快百科快传网中准网文好找聚热点快软件