当前位置: 首页 > 科技观察

代码越写越乱?那是因为你没有使用责任链

时间:2023-03-22 16:44:57 科技观察

目的在开始学习责任链之前,我们先来看看开发中常见的问题。下面是前端用来处理API错误码的代码:consthttpErrorHandler=(error)=>{consterrorStatus=error.response.status;if(errorStatus===400){console.log('Didyousubmitsomething奇怪?有什么事吗?');}if(errorStatus===401){console.log('需要先登录!');}if(errorStatus===403){console.log('你想登录吗?偷偷摸摸地做些什么?');}if(errorStatus===404){console.log('这里什么都没有...');}};当然实际项目中不可能只有一行console,这里是为了说明原理做的简化版。代码中的httpErrorHandler会收到API的响应错误,并对错误状态码进行不同的处理,所以代码中需要大量的if(或switch)来判断当前需要执行什么,什么时候要添加处理代码针对新的错误,需要修改httpErrorHandler中的代码。虽然频繁修改代码是不可避免的,但这样做可能会导致几个问题。下面是基于SOLID的单一职责(Singleresponsibility)和开/关(open/close)这两个原则:单一职责(Singleresponsibility)simple也就是说,单一职责就是只做一件事。从使用的角度来说,之前的httpErrorHandler方法就是把错误对象交给它,让它根据错误码来处理。看似是在做“错误处理”这件单一的事情,但是从实现的角度来说,它把不同错误的处理逻辑都写在了httpErrorHandler中,这可能会导致只修改400逻辑的错误码的可能性,但是必须阅读一大堆不相关的代码。开闭原则(open/close)开闭原则是指已经写好的核心逻辑不应该被改变,但同时由于增加了需求,即开启扩展功能,同时关闭修改原有的正确逻辑。回过头来看httpErrorHandler,如果需要对错误码405增加处理逻辑(扩展新功能),需要修改httpErrorHandler中的代码(修改原来正确的逻辑),这样很容易导致原来正确的执行错误代码。既然httpErrorHandler有这么多的缺陷,那我们该怎么办呢?解决问题分离逻辑,让httpErrorHandler符合单一原则。首先将每个错误的处理逻辑分离到方法中:constresponse400=()=>{console.log('你提交了奇怪的东西吗?');};constresponse401=()=>{console.log('你需要先登录!');};constresponse403=()=>{console.log('你是想偷偷摸摸做坏事吗?');};constresponse404=()=>{console.log('有这里什么都没有...');};consthttpErrorHandler=(error)=>{conterrorStatus=error.response.status;if(errorStatus===400){response400();}if(errorStatus===401){response401();}if(errorStatus===403){response403();}if(errorStatus===404){response404();}};虽然每个块的逻辑只是拆分成了方法,但是这已经可以让我们在修改某个状态码的错误处理时,不需要再去阅读httpErrorHandler中的大量代码。只是分离逻辑的操作也让httpErrorHandler符合了开放封闭的原则,因为当错误处理的逻辑被拆分成方法时,就相当于封装了完成的代码。这时候需要在为httpErrorHandler添加405的错误处理逻辑时,不会影响其他错误处理逻辑方法(关闭修改),而是新建一个response405方法,为httpErrorHandler添加新的条件判断(打开扩展了新功能)。现在的httpErrorHandler其实就是一种策略模式。httpErrorHandler使用统一的接口(方法)来处理各种错误状态。文末再解释一下策略模式和责任链的区别。责任链模式(ChainofResponsibilityPattern)责任链的实现原理很简单,就是把所有的方法串起来,一个一个执行,每个方法只需要做它需要做的事情。比如response400只有遇到状态码为400时才执行,response401只处理401错误,其他方法只有在该处理的时候才执行。每个人都各司其职,这就是责任链。然后开始实施。增加判断根据责任链的定义,每个方法都必须知道当前的事情是否应该由自己来处理,所以原本在httpErrorHandler中实现的if判断应该分发给每个方法,在内部控制责任:constresponse400=(error)=>{if(error.response.status!==400)return;console.log('你提交了奇怪的东西吗?');};constresponse401=(error)=>{if(error.response.status!==401)return;console.log('需要先登录!');};constresponse403=(error)=>{if(error.response.status!==403)return;console.log('你是不是想偷偷摸摸地做些什么?');};constresponse404=(error)=>{if(error.response.status!==404)return;console.log('这里什么都没有...');};consthttpErrorHandler=(错误)=>{response400(错误);response401(错误);response403(错误);response404(错误);};将判断逻辑放在各自的方法中后,httpErrorHandler的代码简化了很多,去掉了httpErrorHandler中的所有逻辑。现在httpErrorHandler只需要依次执行response400到response404即可。反正该执行,不该执行的直接return就好了。实现真正的责任链虽然只要你重构到上一步,所有的split错误处理方法此刻都会判断自己是否应该去做,但是如果你的代码是这样的,那么以后其他人看到httpErrorHandlerwillonly会说:这是什么魔码?API一遇到错误就执行所有的错误处理?因为他们不知道每个处理方法中还有判断,也许过一段时间你就会忘记它,因为现在httpErrorHandler看起来只是从response400到response404,即使我们知道它运行正常,它看起来没有使用责任链。那么它怎么看起来像链条呢?其实可以直接用一个数字记录所有要执行的错误处理方法,通过命名告诉以后看到这段代码的人,这是责任链:consthttpErrorHandler=(error)=>{consterrorHandlerChain=[response400,response401,response403,response404];errorHandlerChain.forEach((errorHandler)=>{errorHandler(error);});};这样一来,责任链的目的就达到了,如果在上面的代码中使用forEach的话,当遇到400错误的时候,其实并不需要执行后面的response401到response404。所以需要在每个错误处理方法中加入一些逻辑,让每个方法都可以判断,如果遇到了自己无法处理的事情,就抛出一个指定的字符串或者布尔值,接收到之后再返回。然后执行下一个方法,但是如果这个方法可以处理,处理完就直接结束,不需要继续跑整个链条。constresponse400=(error)=>{if(error.response.status!==400)return'next';console.log('Didyousubmitsomethingstrange?');};constresponse401=(error)=>{if(error.response.status!==401)return'next';console.log('需要先登录!');};constresponse403=(error)=>{if(error.response.status!==403)return'next';;console.log('Areyoutryingtosneakupanddosomebadbad?');};constresponse404=(error)=>{if(error.response.status!==404)return'next';;console.log('这里什么都没有...');};如果链中某个节点的执行结果是next,则让next方法继续处理:);if(result!=='next')中断;};};封装责任链的实现现在责任链已经实现完成,但是将判断是否给next方法(判断结果!=='next')的逻辑暴露在外面,可能会导致项目中每条链的实现方法不同,其他链可能在判断nextSuccessor或者boolean,所以最后我们需要封装责任链的实现,让团队中的每个人都能使用和遵守规范该项目。责任链要求:当前执行者。下一个收件人。判断当前执行者执行后是否需要交给下一个执行者。所以封装成一个类后应该是这样的:..args){constresult=this.handler(...args);if(result==='next'){returnthis.successor&&this.successor.passRequest(...args);}returnresult;}}当创建一个带有Chain的对象,需要将当前职责方法传入并设置到handler,可以在新对象上使用setSuccessor将链中的下一个对象分配给继承者,并在setSuccessor中返回代表整个链的this,这样就可以在操作的时候直接在setSuccessor里面设置了,以后再用setSuccessor设置下一个receiver。最后通过Chain生成的每个对象都会有一个passRequest去执行当前职责方法,...arg会把传入的所有参数变成一个数组,然后交给handler,也就是当前职责方法,对于执行。如果返回的结果是下一个结果,则判断是否有指定的后继者。如果是,它将继续执行。如果结果不是next,则直接返回结果。使用Chain,代码将变为:consthttpErrorHandler=(error)=>{constchainRequest400=newChain(response400);constchainRequest401=新链(response401);constchainRequest403=新链(response403);constqueSuustRequest404=newChain(response403);chainRequest401);chainRequest401.setSuccessor(chainRequest403);chainRequest403.setSuccessor(chainRequest404);chainRequest400.passRequest(error);};这时候感觉就像一个链条,可以根据自己的需要继续做调整,也可以不用类,因为设计模式的使用不需要局限于如何实现,只要因为有表达模式的意图。责任链的优点和缺点优点:符合单一责任,因此每个方法只有一个责任。它符合开放和封闭的原则,当需求增加时可以很容易地扩展新的职责。使用的时候不需要知道真正的处理方法是谁,减少了很多if或者switch的语法。缺点:团队成员需要对责任链有共识,否??则看到一个方法无缘无故返回一个next会很奇怪。出现问题时很难排查,因为你不知道是哪个责任造成的错误,所以需要从链条的头开始,向后看。即使不需要任何处理的方法也会被执行,因为它们在同一个链中。本文中的例子都是同步执行的。如果有异步请求,执行时间可能会更长。与策略模式的区别前面提到了策略模式,先说说两种模式的相同点,就是可以为多个相同的行为(response400、response401等)定义一个接口(httpErrorHandler),而当使用它,你不需要知道最后是谁执行的。策略模式实现起来相对简单。由于策略模式直接使用if或者switch来控制谁来做这件事情,比较适合一萝卜一坑的情况。虽然策略模式对于例子中错误的状态码也各干各的,当事情不在自己管控的时候会直接交给下一个人,但是责任链上的每个节点还是可以做一些事情的先自己管理的时候,再交给下一个节点:constresponse400=(error)=>{if(error.response.status!==400){//先做点什么...return'next';}console.log('你是不是提交了什么奇怪的东西?');};你在什么场景下使用它?比如你离职需要经过一个签字流程:你自己,你的Leader和HR都需要签字,那么责任链可以把这三个角色的签字流程串成一个流程。每个人签完之后,再交给下一个人,直到人员和资源都签完,整个流程才算完成。而如果通过责任链来处理这个流程,那么无论后面这个流程如何变化或者增加,都有办法灵活处理。以上要求是策略模型达不到的。