写这个文章是因为前段时间确实因为公司的业务开发太忙太紧,所有开发都处在于加班赶项目,并且加入的新人较多造成了一系列代码不可控的质量问题。
文章针对这段时间代码出现的各种各样的问题进行了一个概况和整理,主要集中在代码编码的问题,抽象化的问题,还有就是涉及到微服务中调用和编写接口的问题。
其实按道理来说,这些应该属于编程的基本功,貌似不太值得写一篇文章,不过倒是可以通过这些基本功的出发,去讨论一个代码编程系统构建的一个本质,所以还是比较值得去展开。
大概先铺垫下,会按照一个原则和建议来展开一个一个的进行讨论。
编码的问题
避免过多的 IF 嵌套
所谓的“箭头形”代码基本都是因为大量的 IF 嵌套导致,一方面形成一个深深的箭头形状,在阅读代码造成缩进夸张的语句块。
更要命的是过深的嵌套层次导致代码逻辑复杂度加深,当阅读到第 N 层嵌套时根本不清楚是什么逻辑才能进入,严重降低代码的可阅读性和可维护性。
其实对应 IF-ELSE 过长的主要原因无非就是对当前状态进行检查并决定继续还是跳转。
①使用卫语句(Guard Clauses)提前返回,避免层层嵌套
先对 IF/ELSE 的逻辑结构进行一些分析,我们基本上有两种用法:
"优先考虑满足条件,进行处理流程",代码如下:
if(user.getId() == 10){
//满足条件,执行
}else{
//不满足条件,退出
}
“优先考虑不满足条件,让其逻辑退出流程”,代码如下:
if(user.getId() != 10){
//不满足条件,退出
}else{
//满足条件,执行
}
这是两个不同的逻辑结构,他们都可以写出同样的代码逻辑,但是在第一种中,如果代码量增大,嵌套增多,就很容易在条件中迷失了方向。
如果采用第二种方式把条件反过来写,尽早的能把退出型逻辑及早的退出,这样就可以把箭头型的代码解脱掉,如下图:
②规划好判断条件和状态模型
代码如下图:
如果是业务允许其实是可以将多个判断条件进行整合,这样可以避免箭头形代码的出现。
但是仅仅一段 IF 条件判断的语句又变得非常的臃肿一行都放不下,如果出现了非常复杂多状态判断和组合,可以使用“状态表”,或者是状态机等设计模式来进行解耦。
③将 IF 中的业务细节进行抽象成函数
将 IF 中繁琐的业务细节抽成函数,一方面可以减少又长又臭的代码,更利于屏蔽细节,将不关流程的业务逻辑锁定在一个特定的区域。
也利于进行代码阅读,让阅读关注于业务的流程而不是业务实现的细节,要善于应用函数用于代码的封装和抽象。
谨慎多层循环嵌套中的操作
有的时候,确实几层 for 循环的嵌套是业务实现的必须,但我们需要警惕的是经过几层循环的放大,最内层循环执行的数量是多层循环数量的乘积。
例如,这段代码总共经历了 4 层的循环,如果循环是 10x10x10x10,那么最终的 DB 操作是要经历单独的开销 10000 次。
第一,这 10000 次开销如果是程序员在写代码已经明确知道的开销属于业务必须那倒无妨,只怕程序员在写代码的时候还无意识到这个点是会被随时放大。
第二,即使 10000 次开销是属于业务必须,那按照这个代码来看,还是存在可以优化的空间,可以在循环中将所有查询条件都进行拼凑,然后在进行一定程度的批量查询,可以较大程度降低 DB 的开销。
不要随意定义局部变量名
命名风格我们可以参考阿里的《Java开发手册》,这里主要指出来的是局部变量随意命名的现象比较严重,大家一般都会以为局部变量只是在本方法内使用,又不会对其他方法和其他人造成影响。
但殊不知局部变量名起得不好或随意也对开发者本身造成困扰甚至连自己到不知道的错误,以下是一个比较经典的随意起变量名的例子:
变量名 ma 和 map 没有本身含义,并且他们的泛类又是一样,很难保证不会再下面的代码不小心使用错误。
避免又臭又长的类和方法
一点都不夸张,之前看到过一类一千多行,一个方法长达 300 行,IDE 大概一页正常来说 30-50 行(取决屏幕大小),这个叫阅读者怎么查看。
阅读的时候,不断的滚轮翻页,就算是原作者,恐怕时间一长也很难驾驭这个类,就不用说后来的维护者了。
更重要的是一个类,一个方法过长时,会严重阻碍你的扩展和修改,方法中每一个逻辑都牵扯到很多分散的上下文,会让修改和扩展异常困难。
按照《重构》所说,出现类过长的情况很多是职责不明确,一个类存在着几十个方法,那绝对是职责过多或职责不细分。
简单列一下针对又长又臭的重构处理:
- 分析需要重构类的功能。
- 将职责相同的方法使用组合或集成的方式抽取为独立的类。
- 分析各个方法,将重复的代码提取为函数。
- 命名,对类有一个好的命名有利于对类的定位和确立职责。
Log 日志要提供明确的指向,辅助定位
Log 日志要有明确的指向性,一个可以辅助调试,一个可以记录事件,和确立定位错误。
像以下的这个例子,打印了一个 log.error 日志,但这个错误,就算我们事后去查看日志,只知道这里有一个错误日志,但究竟是哪一个用户日志,哪一张优惠券的日志,无从得知,不能有助于我们直接定位错误。
再看一下的日志,将返回的一个 List 进行直接打印,此处的打印并无助于保留和定位问题,只会留下无价值的信息并且让日志变得乱糟糟。
通常,我们留下实体名字和逻辑关键字就足以识别一条记录。
复杂模块,代码未动,大纲注释先行
要阻止一个初级的程序员一上来就写代码的难度堪比阻止一饥饿的人要饱餐一顿,有多少程序员被称之为码农,一上来就想搬砖。
在流程和系统的设计上,我们有 E-R 图和流程图,帮我们建立模型和流程。
当我们碰到逻辑比较复杂的类或方法,我们也需要先梳理好逻辑和流程,用注释或伪代码定好逻辑和流程,把整体的思路确立后,搭起一个骨架,再往里面填肉(写代码)。
只要流程清晰,逻辑明朗,这个时候写代码其实是最简单的事情。
功能相同尽量抽象,不要发散式修改
举这次我们构建订单的一个例子,见下图:
下单在后端使用了适配者的一个设计模式,主要是包装同一个接口对外暴露,然后根据情况(商品的逻辑)进行实现类的分离。
把逻辑统一并包装成统一接口对外暴露这个本意是良好的,但是在这里例子中,只在意了商品逻辑的分离,而忽略了,其实逻辑,例如库存,支付,优惠券等逻辑其实是统一的,是可以被抽象的。
导致的结果是例如需要修改优惠券逻辑式,需要同时进行三次几乎一模一样的修改。
可以从上图看出来,过早的使用适配模式,将业务在入口处进行分离,导致了后续其实相同逻辑的业务代码也进行了分离,本来 “扣库存” “扣优惠券” “支付”等逻辑应该是一样,但也使用了三套代码进行维护。
微服务编码问题
RPC 接口必须是业务职责
RPC 接口是微服务的生产者提供一定的能力给到消费者进行使用,这个时候的 RPC 接口千万不要定义大而全的接口。
之前就发现有部分同学把 RPC 接口定义成:
insertXXX
updateXXX
listXXX
这样无异于把 DAO 层直接搬到了 RPC,把整个 DAO 直接进行暴露,这样违背了微服务的接口调用原则,RPC 接口只提供最原子的功能,限制消费者在生产者定义好的业务中进行使用。
严禁循环调用 RPC 接口
与项目内编程不同的是,每个 RPC 接口的调用都会伴随着一次的网络开销,需要需要对一个接口进行反复请求,这个时候可以要求 RPC 接口的提供方另外提供一个可以批量的接口,将单次反复的请求变成一次请求,减少网络开销。
使用工具辅助清理恶性代码
P3C 插件
在使用 Eclipse 或 idea 编程中,首推使用阿里的 P3C 插件进行辅助,代码规范检查插件 P3C,是根据《阿里巴巴Java开发手册》转化而成的自动化插件。
使用 Skywalking 找出恶性代码
与 P3C 直接辅助编码不同的是,Skywalking可以在生产环境中通过链路的跟踪确定某一个微服务的接口性能或调动出现异常。
这里不累赘介绍 Skywalking 的用处,其实链路跟踪不仅仅是运维或架构师应该关注的点,普通的开发者也可以借助链路跟踪去回溯自己的代码,站在一个高的角度在生产环境中审视代码在链路中表现。
善于使用链路跟踪往往可以发现在平时编码中被忽略的问题,例如,一次不经意的循环调用 RPC 很容易就造成超大的调用跨度,而往往在编程中开发者是未能及时感知的。
小结
在分享的时候其实还讲了抽象的原则和一些设计模式的使用,这里就不累赘的复述了。
简单的说,要写出好的性能,可读性高,逻辑明了的代码,往往靠的不是一次一次的 CURD,而是平时的总结和思考。
作者:陈于喆
简介:十余年的开发和架构经验,国内较早一批微服务开发实施者。曾任职国内互联网公司网易和唯品会高级研发工程师,后在创业公司担任技术总监/架构师。
注意:本文归作者所有,未经作者允许,不得转载