作者:卢建至
前言
“这是谁写的代码,依托答辩!我要看看提交记录!”
看完之后...
“啊?原来是一个月前的我写的,那没事了...优化一下优化一下...emmm...顺眼多了”
过了一个月...
“这是谁写的代码,依托答辩!我要看看提交记录!”
看完之后...
“啊?原来是一个月前的我写的,那没事了...优化一下优化一下...emmm...顺眼多了”
......
好了,大家来讨论讨论,这个惊悚悬疑小故事运用了什么叙事手法?
言归正传,我们要“严肃”地讨论一个“主观”的问题——代码之丑。
先问大家几个问题,你的代码在被人说过丑吗?有人吐槽过你的代码吗?有人在 CR 的时候对你的代码提出过挑战吗?我相信肯定是有的,每一个人都会有,就像人的审美是主观的,每个人对代码原则的理解也是主观的。
- 我觉得这代码写得好,它做了模块设计,它考虑到了后面的拓展,它做了业务分层...
- 我觉得这代码写的不好,它的模块设计太局限,它的拓展性不高,它的业务分层不够彻底...
那这些观点都是怎么来的呢?在 CR 过程中受到挑战时,我们要如何判断这一次挑战是要接受,还是进行合理反驳呢?
归根到底,所有代码风格上的争论,都是大家对“代码设计原则”的理解不一致、执行不一致导致的。
不同工作阶段的我们,对同一个代码设计原则也是不同的,再加上做到“知行合一”也是一件很难的事,这就导致我们最后写出来的代码已经和设计原则的要求偏差比较大了。
这段时间我一直在看郑晔老师的《代码之丑》,我发现里面很多“代码坏味道”都是我经常在项目里看到的,一看代码的作者,项目涉及的开发人员无一幸免,全部中招。
这里就列举几个最常见的问题,结合我的理解,给出一些改进的想法,大家可以基于自己的实际情况进行留言讨论,毕竟我的理解肯定不是 100% 正确的,“代码坏味道”没有标准答案,甚至可能在某些人眼里这些代码是没问题的。
好的命名要描述意图
我要说的第一个常见问题就是,类、方法、属性的命名。
举例
dart
复制代码
Future<bool> requestInitData() async {
isEffect = await ProductionPlanDataSchedule().requestIfTakeEffect();
if (isEffect) {
_preparationConfigData =
await ProductionPlanDataSchedule().requestMaterialPreparationConfig();
}
return isEffect;
}
这段代码从 ProductionPlanDataSchedule 单例中获取了 isEffect 字段,并在 requestInitData() 方法中对 isEffect 进行了逻辑判断。
逻辑是不是很简单,但是我问你,你知道这段代码背后的业务含义是什么吗?
ProductionPlanDataSchedule:生产计划数据安排 isEffect:是否生效 requestInitData:请求初始化数据
在请求初始化数据的时候,从生产计划数据安排中获取是否生效,如果生效,那么通过生产计划数据安排请求物料备料的设置信息,最后返回是否生效。
是不是有点云里雾里的,请求什么初始化数据?什么东西是否生效?生产计划数据安排类具体干了什么?
这段代码是我去年 3 月份写的,讲实话作为代码作者我都看不懂这代码背后到底在处理什么业务逻辑了,于是我只能又重新看一遍上下文去理解这块业务。
看,问题就出现了,太过于技术化、模糊化的命名是很难正确表达业务含义的。一个比较好的解法就是:用描述意图的方式去对类、方法、属性进行命名。
什么是描述意图呢?就是业务上这个类、这个方法、这个属性要做的事情是什么。
调整
- ProductionPlanDataSchedule 的意图是:生产计划数据调度器,数据的请求、处理都在这里进行,然后供外部消费。
那么这个类的命名应该调整为 ProductionPlanDataScheduler。 我把 Schedule 改成了 Scheduler,加了一个 r,差异可以看如下解释,显然用 Scheduler 是更符合的。这里其实有个附属的“代码坏味道”,就是正确使用英文进行命名,这也是很多程序员容易犯的问题,这里就不深讲了。
- isEffect 的意图是:当前门店的生产计划功能是否生效。
那么这个属性的命名应该调整为 isProductionPlanEffective。
- requestInitData() 的意图是:检查当前门店是否开启了生产计划这个功能。
那么这个方法的命名应该调整为 checkProductionPlanEffective()(当然英语水平高的人可以取出更加专业的方法命名)。
- requestIfTakeEffect() 的意图是:通过网络请求获取当前门店是否开启了生产计划这个功能。
那么这个方法的命名应该调整为 requestEffective()。 这里要做个额外补充,由于这个方法是 ProductionPlanDataScheduler 单例的公开方法,那么就没必要写成 requestIsProductionPlanEffective(),因为这个单例类的意图之一就是请求生产计划的数据,那么只需要缩写成 requestEffective()。 这里可能不同的人会有不同的见解,可以评论讨论,只要是能描述清楚意图,那就都是好的命名。
所以上述代码在调整之后就是
dart
复制代码
Future<bool> checkProductionPlanEffective() async {
isProductionPlanEffective = await ProductionPlanDataScheduler().requestEffective();
if (isProductionPlanEffective) {
_preparationConfigData =
await ProductionPlanDataScheduler().requestMaterialPreparationConfig();
}
return isProductionPlanEffective;
}
我们再来“读”一遍这段代码背后的业务逻辑:检查生产计划是否生效,如果生效了,那么去请求物料备料的设置信息,最后返回生效结果。
这样一来是不是就清晰多了?下一个维护的人也不用去看这段代码的上下文了,只要看这几行代码就能了解这个方法背后的业务逻辑了。
小结
其实做好这个点,那么会是正向收益的循环:好的命名需要描述意图,描述意图需要做到理解业务,理解业务后能设计出合理代码,合理代码的一个体现就是好的命名。
基于行为封装
第二个点是对于“封装”的探讨,什么叫封装好了?这个话题就更主观了,原谅我只能先搬出一个正儿八经的设计原则了来唬唬人了。
迪米特法则(Law of Demeter,LoD)
又称为最少知识原则(Least Knowledge Principle,LKP),它是一种设计原则,强调一个对象应当对其他对象尽可能少的了解。该原则的目的是降低类之间的耦合度,提高模块的相对独立性。
- 每个单元对其它单元只拥有有限的知识,而且这些单元是与当前单元有紧密联系的;
- 每个单元只能与其朋友交谈,不与陌生人交谈;
- 只与自己最直接的朋友交谈。
这个原则要求我们在写代码时需要格外注意哪些是朋友,哪些是陌生人,我们只能调用朋友的方法,不能调用陌生人的方法。
对于面向对象思维较为薄弱的人来说,这个有点抽象难以理解,那其实只要记住一个点就行——基于行为封装。无论我们在封装类还是封装方法,都要基于行为去封装。我们要知道这个类是提供哪些行为的,这些行为是同一类行为吗,或者说这些行为是属于同一个业务流程的吗,具体到方法就是它提供的是什么行为。
其实就跟命名需要描述意图一样,我们的代码块需要描述行为。你对业务流程环节中的行为描述地越准确,你的方法、类就会封装地更好。
按这个思路去封装,自然而然就不会和“陌生人交谈”了。
举例
作为奶茶行业,最受重视的就是食安问题,后厨制作的物料都会被贴上一个叫“效期贴”的贴纸,同时在系统中记录到期提醒时间,等到了时间就会提醒店员物料临近到期,需要进行提前的处理。
那本着“基于行为”封装的原则,我是这么设计的:
- 物料提醒列表(material_remind_linked_list)
行为:记录每一次物料打印效期贴时的信息实体类,包括物料名称、制作人、打印时间、到期提醒时间等信息,并按一定规则排序。
- 物料处理类(material_remind_sender)
行为:将用户的每一次物料打印效期贴行为进行数据的生成和组装,并将组装后的信息添加到物料提醒列表和本地数据库中。
- 物料引擎类(material_remind_engine)
行为:应用启动时按规则从本地数据库获取物料打印效期贴的信息实体类,通过 material_remind_sender 加载到 material_remind_linked_list 中,并启动轮询遍历 material_remind_linked_list。
做过 Android 开发的小伙伴对这套一定很熟悉了,就是经典的 Handler 机制。
在我接手前,上一任同事不是这么做的,他的做法是每次打印一个物料都会产生一个对应的定时器。
一旦门店的生意很好,一天打个几百个效期贴,那么程序里就会存在几百个定时器,这几百个定时器互相都是陌生人,但是随着业务的发展,这些陌生人不得不去相互调用对方的方法,
如果是按我这套,那不管业务再怎么变化,我都可以通过这三个类进行逻辑的调整,因为这三个类彼此之间都是朋友。
小结
我们不需要对这几大设计原则进行死记硬背,只要遵循最基本的本质就可以写出与设计原则相近的代码,这对设计模式也是同理,只要掌握了最本质的设计原则,那我们写出来的代码总会和其中某一类设计模式相似的。
了解业务,然后基于行为封装,那我们就自然而然能写出更好维护的代码,除非业务本身进行重大调整,如果很不幸真的遇到了重大调整(就像我举的这个例子),那就只能基于重大调整后的行为再封装一次。
总结
在学习《代码之丑》这门课程时,几乎每一节课我都在组内周会时进行了分享,分享时讨论还是蛮多的,因为我们组业务的特殊性,同时存在多门编程语言,多个技术栈,有些观点很难理解,有些观点会有理解上的偏差,但是对于“代码坏味道”的认知是能保持一致的。
所以其实明确“代码坏味道”这件事的成本并不高,高就高在怎么在自己的技术栈中消灭这些坏味道,不同编程语言特性不同,解决的业务问题也不同,如果一个团队是统一的编程语言,统一的技术栈,那这件事就会很好做,但如果像我们一样啥都有,那目前除了自立门户关门整顿,也想不出什么更好的办法了。
除了技术栈的问题,另一个问题就是对业务的理解,上面的两个例子都是围绕着“业务”讨论的,其实还有很多代码的坏味道没有列出来,其中很多坏味道是可以通过充分理解业务去解决的。
条条大路通罗马,水流千里归大海,作为程序员来说,我们有 N 种方式去实现一个业务功能,但是作为一个工程师来说,我们要以找到那条最佳路径为奋斗目标。
小茗推荐
最后
关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享。
如有侵权请联系站点删除!
技术合作服务热线,欢迎来电咨询!