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

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

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

LOL首款哈士奇特效皮肤,大招可以召唤哈士奇,直接把敌人咬死,你觉得如何?马上就要到愚人节了,英雄联盟测试服现在也出现了主题皮肤。本次愚人节的主题是猫狗大作战,这一次LOL官方脑洞可谓大到了无边无际,居然找来了一只二哈。没到正式发布那一天,小编都不敢相信花木兰的详细教学打法和铭文出装花木兰的技能介绍轻剑形态重剑形态边路铭文10个调和10个鹰眼6个红月4个异变边路出装边路对线教学1花木兰在四级前少打架,因为花木兰在没有双形态的情况下对线基本上没有优势。所以在一级王者荣耀三盾流吕布崛起,护盾可达8500点,王者局胜率接近90,应该怎么玩?大家好,这里是高进说游戏。王者荣耀中的吕布自从上次加强之后就成为了版本热门上单英雄,虽然吕布的排位赛胜率不高,但是这个英雄因为加强了二技能,混线绝对可以,能混线就代表了就会有经济,绝地求生海岛地图重制,建筑数量大增,核电站或代替机场绝地求生可谓是引领了一波吃鸡潮流,巅峰时曾创下323万玩家同时在线的记录。但任何事情都有高潮低谷,随着其他吃鸡类游戏的兴起,绝地求生也变成了普通游戏,甚至不惜以降价来笼络玩家。不过某公司招人需看王者荣耀常用位置,这些细节能够反应一个人性格,你认同这说法吗?首先我认为这种说法是具有一定的科学道理的。因为每一个位置上面的每一个英雄,他的打法不同,然后他的职业不同也决定着一个玩家的性格。那么举一个很简单的例子,就像刚开始玩的一些新手玩家,DNF这些哈林武器的提升超过60,在装备过渡期值得一用本文由Sky灬素颜游戏视频原创,禁止抄袭或转载,发现必举报,谢谢。只要有新起的角色,就会涉及装备过渡的问题。谈到装备过渡,95版本绕不开的便是哈林装备。虽然在3。7更新中,新起角色DNF17圣耀的剑魂15S打桩比不过12武器的鬼泣,这游戏职业平衡怎么做的?谢邀,我是知墨,一个爱玩游戏的云玩家!DNF真是一个神奇的游戏,同样都是鬼剑士,为什么剑神(我习惯叫剑神)和鬼泣的差别那么大呢?其实在DNF中,衡量一个职业优不优秀不能光看输出能力不负江山不负卿经典游戏吞食天地重做内容之丰富超乎你的相信吞食孔明传是一个传统的回合角色扮演游戏,讲述了诸葛孔明和刘关张从桃园结义开始东汉之初到三国统一的的故事。就像西方剑士的故事和西方游戏中的魔法精灵一样,你永远无法说完,听腻它的故事。新游简评国产硬核动作游戏精品!嗜血印试玩全面测评大家好,我是奇格屋,欢迎来到新游评测时间。今天为大家评测的游戏叫嗜血印,这款游戏是由艺龙游戏制作的一款动作类游戏,团队5人历经2年开发,目前内容完成度已有60,不日就将与大家面世。steam国产独立游戏无神之地试玩测评随着steam在国内的逐渐流行,越来越多的国产游戏上架steam,每月现在大概有十多款国产游戏上架。除了有一些爆火之外其他可能就相对的不是那么出名了,今天给大家介绍的就是一款比较小玩盗版鬼泣5,第三关删了,游戏等于屎,随后秒被打脸游戏没有圈儿PS4前几年最先推出云游戏服务PS4NOW,简单来说是你可以通过互联网来试玩好友的已购买游戏,官方的目的是帮助那些游戏卡关的玩家,可以让好友通过云来解决你的燃眉之急。云
哈利波特手游新转盘皮再次两极分化?赫敏同款很美,又不够美?哈利波特魔法觉醒最近关于赫敏同款的时装的讨论热度很高,但玩家们也各自分了阵营。有争议有好评,对于电影党的粉丝们来说,新时装真的是完美还原了,并且颜值更高。另外也有一部分的玩家表示时新赛季上单该玩啥?剑姬是时候出场了上单基本上是一个战士或坦克,它基本上是一场真人战斗!当然,剑姬锐雯刀妹青罡影四姐妹等女英雄也是必不可少的。尤其是重做之前的老版剑仙。许多人称其为低端游戏的噩梦。旧版的剑仙没有新版的献给二次元玩家盛宴魔兽争霸3我觉得你能赢华丽出击冒险升级其实现在魔兽争霸官方对战平台上面有许多的主题作品,而二次元主题算是游戏当中比较多的作品了,整体的受众还是非常广的。我觉得你能赢属于二次元的防守玩法游戏作品,从游戏的体感之下我觉得这游戏厂商混战斗罗大陆,月流水最高者破7亿在国产小说中,斗罗大陆一直是拥有固定受众群体的经典IP。而最近几年,随着动画的持续更新和真人电视剧的播出,受众群体不断扩展,IP热度也在不断攀升。于是这把火越烧越旺,2021年再一街机游戏让人绝望的BOSS,没出场玩家心态已经崩了街机游戏的BOSS设计都是让人比较满意的,基本上我们每接触一款游戏都会对其中的BOSS印象深刻。这些BOSS要么体型庞大要么技能酷炫要么速度超快,但无论难度有多大,始终都是有弱点的叠纸游戏也申请了元宇宙商标,你期待恋与元宇宙吗?叠纸游戏也申请了元宇宙商标,你期待恋与元宇宙吗?现在真的是万物皆可元宇宙,元宇宙是共享虚拟3D世界,或者是交互性沉浸式和协作性的世界。正如物理宇宙是空间上互连的一系列世界,元宇宙也SE三角战略新预告片发布!三角战略游戏演示预告片第二弹三角战略是SE八方旅人团队开发的最新游戏,预计2022年3月4日登陆Switch平台。今日官方发布了角色剧情预告片第二弹,展示了它精美的HD2D画面弗雷德里卡埃斯弗罗斯特公主(主角在城市中跑酷,揭开这座城市背后的秘密。镜之边缘催化剂游戏简介镜之边缘催化剂的主角依然是Faith,游戏的内容发生在镜之边缘之前,Faith将为我们演示她是如何成为一名优秀的跑酷信使,她向往的是自由自在不受任何约束的生活,她在城市中奔热血传奇对外挂事件的看法热血传奇刚出来的时候,整个传奇游戏都是欣欣向荣的景象,所有人都为自己提升自己的等级,提高自己的游戏技术而努力。但是过了没多久,就有很多人开始动了歪心思,选择使用外挂,刚开始的时候使KPL秋季总决赛即将来袭,斗鱼观赛领大奖,一起见证王者加冕最近王者荣耀KPL秋季总决赛即将在12月25日1800开启,对阵双方分别是武汉estar和广州TTG。这两只战队经过季后赛的激烈角逐,以傲然姿态站在了总决赛的舞台,并且将在总决赛的Steam冬季特卖来啦,是时候备一波年货了(VRPinea12月27日讯)Steam平台2021年冬季特卖活动正式开启,其中备受好评的亚利桑那阳光3。3折史低特卖,半衰期爱莉克斯无人深空ZeroCaliberVR等多款热门