图片来自 Pexels 如何优化这些 if-else 呢?扛不住了本文分享一种设计模式:责任树模式。 通过将责任链与策略模式融合,让干成为一种广义的扛不住了责任链模式,不仅可以完成任务的让干逐级委托,也可以在任一级选择不同的扛不住了下游策略进行处理,并将责任树模式抽象出一个通用的让干框架。 扪心自问,扛不住了你在写业务代码时是让干不是也习惯狂堆 if-else 呢? 问题背景 最近开发了一个需求,该接口需要根据 p1、扛不住了p2、让干p3、扛不住了version 多个入参的让干不同组合按照其对应的业务策略给出结果数据。 由于该接口已经开发了三期了,扛不住了每次开发新一期的让干需求时为了兼容老的业务逻辑,大家都倾向于不删不改只新增。扛不住了 因此这块代码已经产生了一些“坏味道”,函数入口通过不断添加“卫语句”判断 version 的方式跳转到新一期的业务逻辑方法中。 而每一期的业务逻辑也是通过 p1、p2、p3 的源码库 if-else 组合形成不同的分支逻辑。 这已经是我简化后的表述,总之刚开始对于我这个新同学来说,梳理这块业务代码着实花了一些功夫。 而且,这块逻辑相当于是一个业务上的通用能力,未来一定还会有五期、六期、N 期的需求进来,入参的取值也会不断拓展,因此以现有方式膨胀下去只会“坏味道”会越来越重。 总结一下,当前场景面临的问题是: 解决思路 在思考解决方案时,很容易想到两种可以优化类似场景的设计模式:责任链模式和策略模式。 责任链模式 责任链模式是实现了类似“流水线”结构的逐级处理,通常是服务器租用一条链式结构,将“抽象处理者”的不同实现串联起来。 如果当前节点能够处理任务则直接处理掉,如果无法处理则委托给责任链的下一个节点,如此往复直到有节点可以处理这个任务。 我们可以通过责任链模式完成对不同 version 业务逻辑隔离的处理,比如节点 1 处理 version=1 的请求,节点 2 处理 version=2 的请求等等。 但问题在于我们遇到的场景还需要根据一定策略,路由到不同的下游节点进行处理。这就是策略模式擅长解决的问题了。 策略模式 策略模式的目的是将算法的使用与定义解耦,能够实现根据规则路由到不同策略类进行处理。 我们可以通过策略模式解决根据不同参数组合执行不同业务逻辑的场景。但是我们的场景仅仅通过一层策略路由无法满足任务处理需求。请求的分层处理又是服务器托管责任链模式所擅长的了。 可以看到,两种设计模式都不完全符合目前这个场景:责任链模式可以实现逐级委托,但每一级又不能像策略模式那样路由到不同的处理者上;策略模式通常只有一层路由,不易实现多个参数的策略组合。 因此我们自然而然地可以想到:是不是可以将两种模式结合起来? 广义责任链模式:责任树模式 将责任链与策略模式融合,即成为了一种广义的责任链模式,我简称为“责任树模式”。 这种模式不仅可以完成任务的逐级委托,也可以在任一级选择不同的下游策略进行处理。 那么问题来了,如何通过责任树模式解决前面我们遇到的问题呢? 首先看如何解决第一个问题,新老接口的隔离和兼容:可以将接口每个版本的逻辑作为一个责任树上第一层的不同实现,如分别对应上图中的 Strategy1、Strategy2、Strategy3 节点。 这样在接口入口,就首先把策略路由到不同的分支上去。如果没有节点命中,则不再向下游委托直接返回错误。 然后第二个问题,参数的组合定位到不同的策略实现上:同样的思路,一个参数对应责任树上的一层的路由,将该参数的不同取值路由到下一层的不同实现即可,这样逐级委托,后面新增入参的枚举值、甚至再拓展新的入参都可以非常方便地进行拓展。 优化收益 将这块业务通过“责任树模式”重构之后,可以收获以下几个收益点: 相信有开发经验的同学应该都有体会,即使是自己写过的代码,一阵子不看也会忘掉,等到再有修改时,还要顺着代码理一遍逻辑,如果文档、注释没写好,那就更加酸爽了。因此,将巨型函数拆分解耦非常重要。 抽象框架 虽然通过“责任树模式”解决了我这个需求开发中遇到的问题,但是类似的问题还是普遍存在的。 本着助(shǎo)人(zào)为(lún)乐(zi)的精神,我更进一步,将责任树模式抽象出一个通用的框架,方便大家在遇到类似问题时快速“种树”。 这个框架由一个 Router 和 Handler 组成: 我们可以非常方便地通过 Router 和 Handler 的组合拼装成整棵树的结构。 从图中我们可以看出以下几个要点: 那么我们话不多说,先看下框架代码。 ①AbstractStrategyRouter 抽象类: 继承 AbstractStrategyRouter 如果子节点路由逻辑比较简单,可以直接通过 if-else 进行分发。当然如果为了更好地性能、适应更复杂的分发逻辑也可以使用 Map 等保存映射。 对于实现了该抽象类的 Router 节点,只需要调用其 public R applyStrategy(T param) 方法即可获取该节点的期望输出。 框架会自动根据定义的路由逻辑将 param 传递到对应的子节点,再由子节点不断向下分发直到叶子节点或可以给出业务输出的一层。这个过程有点类似递归或者分治的思想。 ②StrategyHandler 接口: 除了根节点外,都要实现 StrategyHandler 因此不再需要同时继承 AbstractStrategyRouter 对于其他责任树中的中间层节点,都需要同时继承 Router 抽象类和实现 Handler 接口。 在 R apply(T param); 方法中首先进行一定异常入参拦截,遵循 fail-fast 原则,避免将这一层可以拦截的错误传递到下一层,同时也要避免“越权”做非本层职责的拦截校验,避免产生耦合,为后面业务拓展挖坑。 在拦截逻辑后直接调用本身 Router 的 public R applyStrategy(T param) 方法路由给下游节点即可。 完结撒花 至此,关于如何通过“责任树模式”优化这个需求场景的介绍就基本结束了,这不是一个复杂的需求,更不是一个多么精妙的优化,这只是日常需求开发中通过设计模式优化代码的一个小例子。 最后再简单聊聊我在日常需求开发过程中关于架构设计部分的一些思考。 其实并不是说用“if-else”很 Low,用设计模式就 Niubility,二者各有其擅长的应用场景,在合适的场景使用合适的代码才是正道。 其实“if-else”足以满足大部分日常需求的开发,且简单、灵活、可靠。 这里的“if-else”泛指朴素直白的编程模式,仅以实现需求业务功能为目的的编码方式。 当然,有些同学不满足于此,希望可以通过经过思考的、更优的架构设计使代码变的更简洁、拓展性更好、性能更优、可读性更好等等。 不过对于此也存在反对的论述,谓之“过早优化乃万恶之源”。 这句话源自 Donald Knuth 他老人家: we should forget about small efficiencies,say about 97% of the time:premature optimization is the root of all evil. 这句话我当然承认其正确性,但我同样觉得需要注意以下几点: ①任何“结论”都有其所处背景、上下文细节等,通过一句话指导工作是不成立的。 优秀的架构师可以给出架构设计是在理论基础、大量实践、不断思考总结以及无数采坑的经验的基础上得来的,而不是他知道一句别人都不知道的“咒语”。 ②Knuth 这句话更偏重于反对奇技淫巧、细枝末节的性能优化,因为在“过早”的时候无法准确获知系统的瓶颈且局部的优化不仅不能带来收益,反而会造成更大的代价。 他批评的恰恰是不着眼于整体架构的局部视角对系统的破坏,而架构设计正是需要从整体视角去做选择与权衡。因此将 Knuth 这句话直接推广到“架构设计”上并不妥当。 ③很多人觉得在项目开发时需求经常“瞬息万变”、“朝令夕改”,而做优化又需要花费大量时间思考,根本没有精力优化。 我认为这种论述也是不成立的,凭什么认为等到坏味道严重、历史包袱沉重的时候就有精力、能力和胆量做优化了呢? ④何时是所谓的“不早”很难界定,其实我们永远都无法确定自己掌握了足够的细节可以进行绝对正确的优化。 在现实世界中,受到时间维度的限制,我们永远无法达成全局最优,只能以局部最优不断去逼近全局最优。我觉得等到坏味道严重不得不重构的时候才想起优化已为时过晚。 ⑤这句话不应该成为不做设计的借口,即使最终提交的代码仍是“if-else”版本,也不应省略思考、推演、权衡的过程,日常需求是练兵场,是精进技术的必经之路。 所以,我觉得不要被这句话束缚手脚,当然更不要闭门造车,在开发过程中勤于思考,向更有经验的人请教,在架构设计上不断学习、探索,才能摆脱日复一日通过“if-else”堆砌业务逻辑的循环。 作者:寻弈 编辑:陶家龙 出处:转载自公众号闲鱼技术(ID:XYtech_Alibaba)