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

实在受不了了,老大让我干掉if-else

时间:2023-03-21 11:57:35 科技观察

图片来自Pexels如何优化这些if-else?本文分享一种设计模式:责任树模式。通过将责任链和策略模式相结合,成为广义的责任链模式,既可以完成任务的逐级委托,又可以在任意层次选择不同的下游策略进行处理,抽象出责任树模式变成了一个通用的框架。扪心自问,写业务代码时是否也习惯了堆砌if-else?问题背景最近开发一个需求,接口需要根据多个输入参数p1,p2,p3的不同组合,版本根据其对应的业务策略给出结果数据。由于接口已经开发了三个阶段,所以每次开发新阶段的需求时,为了兼容旧的业务逻辑,大家往往不删改,而是增加。因此,这段代码已经产生了一些“恶臭”。函数入口通过不断添加“guard语句”判断版本来跳转到新的业务逻辑方法。各个时期的业务逻辑也通过p1、p2、p3的if-else组合形成不同的分支逻辑。这已经是我的简化表达了。总之,对于我一个新同学来说,整理这段业务代码还是费了一番功夫的。而且,这段逻辑相当于一个通用的业务能力。未来肯定会有第五、第六、N相的需求,输入参数的值会不断扩大。“不良品味”会越来越严重。总结一下,当前场景面临的问题是:如何在保证兼容旧版本的同时,解决接口升级,轻松开发新版本业务逻辑?如何根据输入参数p1、p2、p3等的不同组合进行策略定位?解决方案在思考解决方案时,很容易想到两种可以优化相似场景的设计模式:责任链模式和策略模式。ChainofResponsibilityMode责任链模式实现了类似于“流水线”结构的逐级处理,通常是链式结构,将“抽象处理器”的不同实现串联起来。如果当前节点可以处理任务,则直接处理。如果无法处理,则委托给责任链中的下一个节点,以此类推,直到有节点可以处理该任务。我们可以通过责任链模式完成不同版本业务逻辑隔离的处理,比如节点1处理version=1的请求,节点2处理version=2的请求,等等。但问题是我们遇到的场景还是需要按照一定的策略路由到不同的下游节点去处理。这就是策略模式擅长解决的问题。策略模式策略模式的目的是解耦算法的使用和定义,使其可以根据规则路由到不同的策略类进行处理。我们可以使用策略模式来解决根据不同的参数组合执行不同的业务逻辑的场景。但是在我们的场景中,仅仅一层策略路由是无法满足任务处理需求的。请求的分层处理又是责任链模式擅长的。可以看出,这两种设计模式都不能完全契合当前场景:责任链模式可以实现逐级委托,但不能像策略模式那样每一级都路由到不同的处理器;策略模式通常只有一层路由,不容易实现多个参数的策略组合。因此,我们自然可以想到:是否可以将两种模式结合起来?广义责任链模式:责任树模式将责任链与策略模式融合,成为广义责任链模式,我简称为“责任树”。model”。这种模式不仅可以完成任务的逐级委派,还可以在任意层级选择不同的下游策略进行处理。那么问题来了,如何通过责任树模型解决我们之前遇到的问题?首先我们看第一个问题,新旧接口的隔离和兼容如何解决:每个版本接口的逻辑都可以作为一个责任树中第一层的不同实现,比如对应上图中的Strategy1、Strategy2、Strategy3节点,这样在接口入口处,先将policy路由到不同的分支,不再是没有节点命中就直接向下游delegate返回error,然后第二个问题,参数的组合定位在不同的策略实现上:同一个思路,一个参数对应责任树上一层的路由,参数的不同值可以路由到下一层的不同实现,这样逐级委托,新增入参的枚举值,甚至是新入参的扩展都可以很方便。优化收益通过“责任树模型”重构该业务后,可以获得以下收益:降低后续迭代的人力成本。代码结构更清晰,可维护性提高:没有各种可维护性差的“guard语句”&巨方法的跳转,功能可以收敛在理想的50行以内。后续修改代码的新需求不容易出错:策略之间隔离,不需要读大函数理清逻辑再修改,只需要增加一个路由+一个新的策略实现方法想都没想。易于定位问题:同样由于策略间的隔离,调试时可以直接定位到指定策略的业务逻辑代码,无需逐句查看。相信有开发经验的同学应该都有体会。即使是自己写的代码,一时不看也会忘记。当有其他修改时,需要按照代码的逻辑进行。那就更酸了。因此,对庞大的功能进行拆分和解耦是非常重要的。虽然抽象框架通过“责任树模型”解决了我在这个需求开发中遇到的问题,但是类似的问题仍然普遍存在。本着助(shǎo)人(zào)(lún)幸福(zi)的精神,我更进一步,将责任树模型抽象成一个通用的框架,让大家在遇到类似问题时可以快速“种树”.这个框架由一个Router和Handler组成:Router是一个抽象类,负责定义如何路由到下游的多个子节点。Handler是一个接口,负责实现各个节点的业务逻辑。通过Router和Handler的组合,我们可以很容易的组装出整个树状结构。从图中可以看出以下几个关键点:除根节点(入口)外,每个节点都实现了Handler接口。根节点只继承Router抽象类。所有叶子节点只实现Handler接口,不继承Router抽象类(不需要向下委托)。除了根节点和叶子节点,其他节点是上层的Handler和下层的Router。那么话不多说,先看框架代码。①AbstractStrategyRouter抽象类:/***通用的“策略树”框架,通过树结构实现分发和委托,每一层通过指定的参数向下分发委托,直到到达最终的执行者。*框架包含两个类:{@codeStrategyHandler}和{@codeAbstractStrategyRouter}*其中:实现{@codeAbstractStrategyRouter}抽象类完成策略分发,*实现{@codeStrategyHandler}接口实现策略。*第二层的A、B等节点既是Root节点的策略执行者,又是策略A1、A2、B1、B2的分发者。这样的节点只需要继承{@codeStrategyHandler},同时实现{@codeAbstractStrategyRouter}接口即可。**

*+--------+*|Root|------------第1层策略入口*+--------+*/\--------------根据词条P1分配策略*/\*+------++------+*|A||B|-------第二层不同策略的实现*+-----++-----+*/\/\----------根据输入参数P2的策略分发*/\/\*+---++---++---++---+*|A1||A2||B1||B2|-----layer3种不同的策略实现的*+---++---++---++---+*
**@author*@date*@seeStrategyHandler*/@ComponentpublicabstractclassAbstractStrategyRouter{/***PolicyMapper,根据指定的入参路由到相应的PolicyProcessor。**@paramstrategy的入参类型*@param策略的返回值类型*/publicinterfaceStrategyMapper{/***根据入参获取对应的策略处理器。可以通过if-else实现,也可以通过Map实现。**@paramparam输入参数*@returnstrategyprocessor*/StrategyHandlerget(Tparam);}privateStrategyMapperstrategyMapper;/***类初始化时注册分发策略Mapper*/@PostConstructprivatevoidabstractInit(){strategyMapper=registerStrategyMapper();Objects.requireNonNull(strategyMapper,"strategyMapper不能为null");}@Getter@Setter@SuppressWarnings("unchecked")privateStrategyHandlerdefaultStrategyHandler=StrategyHandler.DEFAULT;框架会根据策略自动分发给下游Handler进行处理**@paramparam入参*@return下游executor给定的返回值*/publicRapplyStrategy(Tparam){finalStrategyHandlerstrategyHandler=strategyMapper.get(param);if(strategyHandler!=null){returnstrategyHandler.apply(param);}returndefaultStrategyHandler.apply(param);}/***抽象方法,需要子类实现策略分发逻辑**@返回分发逻辑Mapper对象*/protectedabstractStrategyMapperregisterStrategyMapper();}继承AbstractStrategyRouter如果子节点路由逻辑比较简单,可以直接通过if-else进行分发。当然,为了更好的性能和适应更复杂的分布逻辑,也可以使用Map等方式来保存映射。对于实现了这个抽象类的Router节点,只需要调用它的publicRapplyStrategy(Tparam)方法就可以得到节点想要的输出。框架会根据定义好的路由逻辑,自动将param传给对应的子节点,然后子节点继续向下分发到叶子节点或者能够提供业务输出的层。这个过程有点类似于递归或者分而治之的思想。②StrategyHandler接口:/***@author*@date*/publicinterfaceStrategyHandler{@SuppressWarnings("rawtypes")StrategyHandlerDEFAULT=t->null;/***applystrategy**@paramparam*@return*/Rapply(Tparam);}除了根节点,还必须实现StrategyHandler,所以不再需要继承AbstractStrategyRouter。对于其他责任树中的中间节点,需要继承Router抽象类,同时实现Handler接口。在R中应用(T参数);方法,首先拦截某些异常的输入参数,遵循fail-fast原则,避免将本层可以拦截的错误传递给下一层,同时避免“越过”非本地层的拦截和验证职责避免了耦合,为以后的业务扩展挖了坑。拦截逻辑后,直接调用Router自身的publicRapplyStrategy(Tparam)方法路由到下游节点。至此,关于如何通过“责任树模式”来优化这个需求场景的介绍就基本结束了。这不是一个复杂的要求,也不是一个微妙的优化。这只是日常需求开发中通过设计模式优化代码的一个小例子。最后简单说一下自己在日常需求开发过程中对架构设计部分的一些思考。其实并不是说使用“if-else”就低级,使用设计模式就是Niubility。两者都有各自的应用场景,在合适的场景使用合适的代码才是正道。事实上,“if-else”足以满足大部分日常需求的开发,而且简单、灵活、可靠。这里的“if-else”泛指一种简单直接的编程方式,一种只为实现所需业务功能的编码方式。当然,也有一些同学并不满足于此,希望通过深思熟虑的更好的架构设计,让代码更简洁、扩展性更好、性能更好、可读性更好等。但是,也有反对这种说法的说法,叫做“过早的优化是万恶之源”。这句话出自他的老人DonaldKnuth:我们应该忘记小效率,大约97%的时间都说:过早的优化是万恶之源。我当然承认这句话是对的,但我也认为需要注意以下几点:①任何“结论”都有其背景、语境等,用一句话来指导工作是站不住脚的。一个优秀的架构师能给出的架构设计是建立在理论基础上,大量实践,不断思考和总结,无数矿坑的经验,而不是他知道一个别人不知道的“咒语”。②Knuth的话更侧重于对立的技巧和细微的性能优化,因为不可能在“过早”的时候准确地知道系统的瓶颈,局部优化不仅不会带来任何好处,反而会造成更大的成本。他所批判的恰恰是不着眼于整体架构的局部视角对系统的破坏,而架构设计则需要从整体视角进行取舍和取舍。因此,直接将Knuth的话引申为“架构设计”是不合适的。③很多人觉得在项目开发过程中,需求经常“变化很快”、“天天变”,优化需要花很多时间思考,根本没有精力去优化。我不认为这个论点是有效的。为什么你认为在恶趣味严重、历史包袱沉重的情况下,你还会有精力、能力和勇气去优化?④所谓“不早”是什么时候,很难界定。你永远无法确定你有足够的细节来进行绝对正确的优化。在现实世界中,受时间维度的限制,我们永远无法达到全局最优,只能以局部最优逼近全局最优。我认为在臭味严重到我不得不重构之前考虑优化已经太晚了。⑤这句话不能成为不设计的借口。即使最终提交的代码还是“if-else”版本,思考、推演、取舍的过程也不应该省略。日常需求是练兵场,是进阶科技的必备条件。通过马路。所以,我觉得不要被这句话束缚,当然也不要闭门造车,在开发过程中勤于思考,多请教有经验的人,在架构设计上不断学习和探索,以摆脱日复一日的“过客”。if-else”来堆砌业务逻辑循环。作者:寻一编辑:陶家龙来源:转载自公众号闲鱼科技(ID:XYtech_Alibaba)