4.5 症结 3:抽象不够、逻辑纠缠——High Level 业务逻辑和 Low Level 实现逻辑纠缠
当我们说“代码中包含的业务逻辑”的时候,我们到底在说什么?业界并没有一个标准,大家经常讲的 CRUD 增删改查其实属于更底层的数据访问逻辑。
我的观点是:所谓代码中的业务逻辑,是指这段代码所表现出的所有输入输出规则、算法和行为,通常可以分为以下 5 类:
-
输入合法性校验;
-
业务规则校验:典型的如检查交易记录状态、金额、时限、权限等,通常包含数据库或外部接口的查询作为参考;
-
数据持久化行为:数据库、缓存、文件、日志等任何形式的数据写入行为;
-
外部接口调用行为;
-
输出/返回值准备。
当然具体到某一个组件实例,可能不会包括上述全部 5 类业务逻辑,但是也可能每一类业务逻辑存在多个。
单这样看你可能觉得并不是特别复杂,但是现实中上述 5 类业务逻辑中的每一个通常还包含着一到多个底层实现逻辑,如 CRUD 数据访问逻辑或第三方 API 的调用。
例如输入合法性校验,通常需要查询对应记录是否存在,外部接口调用前通常需要查询相关记录以获得调用接口需要的参数,调用接口后还需要根据结果更新相关记录状态。
显然这里存在两个 Level 的逻辑——High Level 的与业务需求对应且关联紧密的逻辑、Low Level 的实现逻辑。
如果对两个 Level 的逻辑不加以区分、混为一谈,代码质量立刻就会遭到严重损害:
-
可读性变差:两个维度的复杂性——业务复杂性和底层实现的技术复杂性——被掺杂在了一起,复杂度 1+1>2 剧增,给其他人阅读代码增加很大负担;
-
可维护性差:可维护性通常指排查和解决问题所需花费的代价高低,当两个 level 的逻辑纠缠在一起,会使排查问题变的更困难,修复问题时也更容易出错;
-
可扩展性无从谈起:扩展性通常指为系统增加一个特性所需花费的代价高低,代价越高扩展性越差;与排查修复问题类似,逻辑纠缠显然也会使添加新特性变得困难、一不小心就破坏了已有功能。
下面这段代码就是一个典型案例——High Level 的逻辑流程(参数获取、反序列化、参数校验、缓存写入、数据库持久化、更新相关交易记录)完全淹没在了 Low Level 的实现逻辑(字符串比较、Json 反序列化、redis 操作、dao 操作以及前后各种琐碎的参数准备和返回值处理)。下一节我会针对这段问题代码给出重构方案。
@Override
4.6 药方 3:控制逻辑分离——业务模板 Pattern of NestedBusinessTemplate
解决“逻辑纠缠”最关键是要找到一种隔离机制,把两个 Level 的逻辑分开——控制逻辑分离,分离的好处很多:
-
根据经验,当我们着手维护一段代码时,一定是想先弄清楚它的整体流程、算法和行为,而不是一上来就去研究它的细枝末节;
-
控制逻辑分离后,只需要去看 High Level 部分就能了解到上述内容,阅读代码的负担大幅度降低,代码可读性显著增强;
-
读懂代码是后续一切维护、重构工作的前提,而且一份代码被读的次数远远高于被修改的次数(高一个数量级),因此代码对人的可读性再怎么强调都不为过,可读性增强可以大幅度提高系统可维护性,也是重构的最主要目标。
-
同时,根据我的经验,High Level 业务逻辑的变更往往比 Low Level 实现逻辑变更要来的频繁,毕竟前者跟业务直接对应。当然不同类型项目情况不一样,另外它们发生变更的时间点往往也不同;
-
在这样的背景下,控制逻辑分离的好处就更明显了:每次维护、扩充系统功能只需改动一个 Levle 的代码,另一个 Level 不受影响或影响很小,这会大幅降低修改成本和风险。
我在总结过去多个项目中的教训和经验后,总结出了一项最佳实践或者说是设计模式——业务模板 Pattern of NestedBusinessTemplat,可以非常简单、有效的分离两类逻辑,先看代码:
public class XyzService {
@SuppressWarnings({ “unchecked”, “rawtypes” })
如果你熟悉经典的 GOF23 种设计模式,很容易发现上面的代码示例其实就是 Template Method 设计模式的运用,没什么新鲜的。
没错,我这个方案没有提出和创造任何新东西,我只是在实践中偶然发现 Template Method 设计模式真的非常适合解决广泛存在的逻辑纠缠问题,而且也发现很少有程序员能主动运用这个设计模式;一部分原因可能是意识到“逻辑纠缠”问题的人本就不多,同时熟悉这个设计模式并能自如运用的人也不算多,两者的交集自然就是少得可怜;不管是什么原因,结果就是这个问题广泛存在成了通病。
我看到一部分对代码质量有追求的程序员 他们的解决办法是通过"结构化编程"和“模块化编程”:
-
把 Low Level 逻辑提取成 private function,被 High Level 代码所在的 function 直接调用;
-
问题 1 硬连接不灵活:首先,这样虽然起到了一定的隔离效果,但是两个 level 之间是静态的硬关联,Low Level 无法被简单的替换,替换时还是需要修改和影响到 High Level 部分;
-
问题 2 组件内可见性造成混乱:提取出来的 private function 在当前组件内是全局可见的——对其它无关的 High Level function 也是可见的,各个模块之间仍然存在逻辑纠缠。这在很多项目中的热点代码中很常见,问题也很突出:试想一个包含几十个 API 的组件,每个 API 的 function 存在一两个关联的 private function,那这个组件内部的混乱程度、维护难度是难以承受的。
-
把 Low Level 逻辑抽取到新的组件中,供 High Level 代码所在的组件依赖和调用;更有经验的程序员可能会增加一层接口并且借助 Spring 依赖注入;
-
问题 1 API 泛滥:提取出新的组件似乎避免了“结构化编程”的局限性,但是带来了新的问题——API 泛滥:因为组件之间调用只能走 public 方法,而这个 API 其实没有太多复用机会根本没必要做成 public 这种最高可见性。
-
问题 2 同层组件依赖失控:组件和 API 泛滥后必然导致组件之间互相依赖成为常态,慢慢变得失控以后最终变成所有组件都依赖其它大部分组件,甚至出现循环依赖;比如那个拥有 130 个 import 和 40 个 Spring 依赖组件的 ContractService。
下面介绍一下 Template Method 设计模式的运用,简单归纳就是:
-
High Level逻辑封装在抽象父类AbsUpdateFromMQ的一个final function中,形成一个业务逻辑的模板;
-
final function保证了其中逻辑不会被子类有意或无意的篡改破坏,因此其中封装的一定是业务逻辑中那些相对固定不变的东西。至于那些可变的部分以及暂时不确定的部分,以abstract protected function形式预留扩展点;
-
子类(一个匿名内部类)像“做填空题”一样,填充模板实现Low Level逻辑——实现那些protected function扩展点;由于扩展点在父类中是abstract的,因此编译器会提醒子类的程序员该扩展什么。
那么它是如何避免上面两个方案的 4 个局限性的:
-
Low Level 需要修改或替换时,只需从父类扩展出一个新的子类,父类全然不知无需任何改动;
-
无论是父类还是子类,其中的 function 对外层的 XyzService 组件都是不可见的,即便是父类中的 public function 也不可见,因为只有持有类的实例对象才能访问到其中的 function;
-
无论是父类还是子类,它们都是作为 XyzService 的内部类存在的,不会增加新的 java 类文件更不会增加大量无意义的 API(API 只有在被项目内复用或发布出去供外部使用才有意义,只有唯一的调用者的 API 是没有必要的);
-
组件依赖失控的问题当然也就不存在了。
SpringFramework 等框架型的开源项目中,其实早已大量使用 Template Method 设计模式,这本该给我们这些应用开发程序员带来启发和示范,但是很可惜业界没有注意到和充分发挥它的价值。
NestedBusinessTemplat 模式就是对其充分和积极的应用,前面一节提到过的复用的两种正确姿势——打造自己的 lib 和 framework,其实 NestedBusinessTemplat 就是项目自身的 framework。
无论你的编程启蒙语言是什么,最早学会的逻辑控制语句一定是 if else,但是不幸的是它在你开始真正的编程工作以后,会变成一个损害项目质量的坏习惯。
几乎所有的项目都存在 if else 泛滥的问题,但是却没有引起足够重视警惕,甚至被很多程序员认为是正常现象。
首先我来解释一下为什么 if else 这个看上去人畜无害的东西是有害的、是需要严格管控的:
-
if else if …else 以及类似的 switch 控制语句,本质上是一种 hard coding 硬编码行为,如果你同意“magic number 魔法数字”是一种错误的编程习惯,那么同理,if else 也是错误的 hard coding 编程风格;
-
hard coding 的问题在于当需求发生改变时,需要到处去修改,很容易遗漏和出错;
-
以一段代码为例来具体分析:
if (“3”.equals(object.getString(“type”))){
-
if (“3”.equals(object.getString(“type”)))
-
显然这里的"3"是一个 magic number,没人知道 3 是什么含义,只能推测;
-
但是仅仅将“3”重构成常量 ABC_XYZ 并不会改善多少,因为 if (ABC_XYZ.equals(object.getString(“type”))) 仍然是面向过程的编程风格,无法扩展;
-
到处被引用的常量 ABC_XYZ 并没有比到处被 hard coding 的 magic number 好多少,只不过有了含义而已;
-
把常量升级成 Enum 枚举类型呢,也没有好多少,当需要判断的类型增加了或判断的规则改变了,还是需要到处修改——Shotgun Surgery(霰弹式修改)
-
并非所有的 if else 都有害,比如上面示例中的 if (list1 !=null) { 就是无害的,没有必要去消除,也没有消除它的可行性。判断是否有害的依据:
-
如果 if 判断的变量状态只有两种可能性(比如 boolean、比如 null 判断)时,是无伤大雅的;
-
反之,如果 if 判断的变量存在多种状态,而且将来可能会增加新的状态,那么这就是个问题;
-
switch 判断语句无疑是有害的,因为使用 switch 的地方往往存在很多种状态。
4.8 药方 4:充血枚举类型——Rich Enum Type
正如前面分析呈现的那样,对于代码中广泛存在的状态、类型 if 条件判断,仅仅把被比较的值重构成常量或 enum 枚举类型并没有太大改善——使用者仍然直接依赖具体的枚举值或常量,而不是依赖一个抽象。
于是解决方案就自然浮出水面了:在 enum 枚举类型基础上进一步抽象封装,得到一个所谓的“充血”的枚举类型,代码说话:
- 实现多种系统通知机制,传统做法:
enum NOTIFY_TYPE { email,sms,wechat; } //先定义一个enum——一个只定义了值不包含任何行为的“贫血”的枚举类型
- 实现多种系统通知方式,充血枚举类型——Rich Enum Type 模式:
enum NOTIFY_TYPE { //1、定义一个包含通知实现机制的“充血”的枚举类型
-
充血枚举类型——Rich Enum Type 模式的优势:
-
不难发现,这其实就是 enum 枚举类型和 Strategy Pattern 策略模式的巧妙结合运用;
-
当需要增加新的通知方式时,只需在枚举类 NOTIFY_TYPE 增加一个值,同时在策略接口 NotifyMechanismInterface 中增加一个 by 方法返回对应的策略实现;
-
当需要修改某个通知机制的实现细节,只需修改 NotifyMechanismInterface 中对应的策略实现;
-
无论新增还是修改通知机制,调用方完全不受影响,仍然是 NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg);
-
与传统 Strategy Pattern 策略模式的比较优势:常见的策略模式也能消灭 if else 判断,但是实现起来比较麻烦,需要开发更多的 class 和代码量:
-
每个策略实现需单独定义成一个 class;
-
还需要一个 Context 类来做初始化——用 Map 把类型与对应的策略实现做映射;
-
使用时从 Context 获取具体的策略;
-
Rich Enum Type 的进一步的充血:
-
上面的例子中的枚举类型包含了行为,因此已经算作充血模型了,但是还可以为其进一步充血;
-
例如有些场景下,只是要对枚举值做个简单的计算获得某种 flag 标记,那就没必要把计算逻辑抽象成 NotifyMechanismInterface 那样的接口,杀鸡用了牛刀;
-
这时就可以在枚举类型中增加 static function 封装简单的计算逻辑;
-
策略实现的进一步抽象:
-
当各个策略实现(byEmail bySms byWechat)存在共性部分、重复逻辑时,可以将其抽取成一个抽象父类;
-
然后就像前一章节——业务模板 Pattern of NestedBusinessTemplate 那样,在各个子类之间实现优雅的逻辑分离和复用。
5. 重构前的火力侦察:为你的项目编制一套代码库目录/索引——CODEX
========================================================================================================
以上就是我总结出的最常见也最影响代码质量的 4 个问题及其解决方案:
-
职责单一、小颗粒度、高内聚、低耦合的业务逻辑层组件——倒金字塔结构;
-
打造项目自身的 lib 层和 framework——正确的复用姿势;
-
业务模板 Pattern of NestedBusinessTemplate——控制逻辑分离;
-
充血的枚举类型 Rich Enum Type——消灭硬编码风格的 if else 条件判断;
接下来就是如何动手去针对这 4 个方面进行重构了,但是事情还没有那么简单。
上面所有的内容虽然来自实践经验,但是要应用到你的具体项目,还需要一个步骤——火力侦察——弄清楚你要重构的那个模块的逻辑脉络、算法以致实现细节,否则贸然动手,很容易遗漏关键细节造成风险,重构的效率更难以保证,陷入进退两难的尴尬境地。
我 2019 年一整年经历了 3 个代码十分混乱的项目,最大的收获就是摸索出了一个梳理烂代码的最佳实践——CODEX:
- 在阅读代码过程中,在关键位置添加结构化的注释,形如://CODEX ProjectA 1 体检预约流程 1 预约服务 API 入口
-
所谓结构化注释,就是在注释内容中通过规范命名的编号前缀、分隔符等来体现出其所对应的项目、模块、流程步骤等信息,类似文本编辑中的标题 1、2、3;
-
然后设置 IDE 工具识别这种特殊的注释,以便结构化的显示。Eclipse 的 Tasks 显示效果类似下图;
-
这个结构化视图,本质上相对于是代码库的索引、目录,不同于 javadoc 文档,CODEX 具有更清晰的逻辑层次和更强的代码查找便利性,在 Eclipse Tasks 中点击就能跳转到对应的代码行;
-
这些结构化注释随着代码一起提交后就实现了团队共享;
-
这样的一份精确无误、共享的、活的源代码索引,无疑会对整个团队的开发维护工作产生巨大助力;
-
进一步的,如果在 CODEX 中添加 Markdown 关键字,甚至可以将导出的 CODEX 简单加工后,变成一张业务逻辑的 Sequence 序列图,如下所示。
===========================================================================================
毫无疑问这是程序员最好的时代,互联网浪潮已经席卷了世界每个角落,各行各业正在越来越多的依赖 IT。过去只有软件公司、互联网公司和银行业会雇佣程序员,随着云计算的普及、产业互联网和互联网+兴起,已经有越来越多的传统企业开始雇佣程序员搭建 IT 系统来支撑业务运营。
资本的推动 IT 需求的旺盛,使得程序员成了稀缺人才,各大招聘平台上,程序员的岗位数量和薪资水平长期名列前茅。
但是我们这个群体的整体表现怎么样呢,扪心自问,我觉得很难令人满意,我所经历过的以及近距离观察到的项目,鲜有能够称得上成功的。这里的成功不是商业上的成功,仅限于作为一个软件项目和工程是否能够以可接受的成本和质量长期稳定的交付。
最后
答应大伙的备战金三银四,大厂面试真题来啦!
这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
给文章留个小赞,就可以免费领取啦~
《960全网最全Android开发笔记》
《379页Android开发面试宝典》
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。
如何使用它?
1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数
《507页Android开发相关源码解析》
只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。
真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。
腾讯、字节跳动、阿里、百度等BAT大厂 2020-2021面试真题解析
资料收集不易,如果大家喜欢这篇文章,或者对你有帮助不妨多多点赞转发关注哦。文章会持续更新的。绝对干货!!!
文中列出的知识点会大大增加通过前两轮技术面试的几率。
如何使用它?
1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数
[外链图片转存中…(img-CsT0HBfe-1643797126761)]
《507页Android开发相关源码解析》
只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。
真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。
[外链图片转存中…(img-V6YSdilP-1643797126762)]
腾讯、字节跳动、阿里、百度等BAT大厂 2020-2021面试真题解析
[外链图片转存中…(img-wSt2iHSj-1643797126762)]
资料收集不易,如果大家喜欢这篇文章,或者对你有帮助不妨多多点赞转发关注哦。文章会持续更新的。绝对干货!!!