分享

CodeReview 常见代码问题( 下 )

 xujin3 2018-08-18

相关阅读:

优秀架构师必须掌握的架构思维

CodeReview 常见代码问题( 上 )

互联网技术(java框架、分布式、集群)干货视频大全,不看后悔!(免费下载)

来源:琴水玉 ,

www.cnblogs.com/lovesqcc/p/9271781.html


可维护性问题


可维护性问题是“在当前业务变更的范围内通常不会导致BUG、故障,却会在日后埋下地雷,引发BUG、故障、维护成本大幅增加”的类别。


硬编码


硬编码主要有三种情况: a. “魔数”; b. 写死的配置; c. 临时加的逻辑和文案。


“魔数”与重复代码类似,当前或许不会引发问题,时间一长,为了弄清楚其代表的含义,增加很多沟通维护成本,且分散在各处很容易导致修改的时候遗漏不一致。务必清清除。方法也比较简单:定义含义明显的枚举或常量,代表这个魔数在代码中发言。


“写死的配置”不会影响业务功能, 不过在环境变更或系统调优的时候,就显得很不方便了。 方法: 尽量将配置抽离出来做成配置项放到配置文件里。


“临时加的逻辑和文案”也是一种破坏系统可维护性的做法。方法: 抽离出来放在单独的函数或方法里,并特别加以注释。


重复代码


重复代码在当前可能不会造成 BUG,但上线后,需要维护多处的事实一致性;时间一长,后续修改的时候就特别容易遗漏或处理不一致导致 BUG;重复代码是公认的“代码坏味”,必当尽力清除。方法: 抽离通用的部分,定制差异。重复代码还有一种情况出现,即创造新函数时,先看看是否既有方法已经实现过。


通用逻辑与定制业务逻辑耦合


这大概是每个媛猿们在开发生涯中遇到的最恶心的事情之一了。通用逻辑与具体的各种业务逻辑混杂交错,想插根针都难。遇到这种情况,只能先祈福,然后抽离一个新的函数,严格判断相应条件满足后去调用它。

如果是新创建逻辑,可以使用函数式编程或基于接口的编程,将通用处理流程抽离出来,而将具体业务逻辑以回调函数的形式传入处理。


不要让不同的业务共用相同的函数,然后在函数里一堆 if-else plus switch , 而是每个业务都有各自的函数, 并可复用相同的通用逻辑和流程处理; 或者各个业务可以覆写同样命名的函数。


复用,而非混杂。


直接在原方法里加逻辑


有业务改动时,猿媛们图方便倾向于直接在原方法里加判断和逻辑。这样做是很不好的习惯。一方面,增加了原方法的长度,破坏了其可维护性;另一方面,有可能对原方法的既有逻辑造成破坏。 可靠的方式是: 新增一个函数,然后在原方法中调用并说明原因。


多业务耦合


在业务边界未仔细划分清晰的情况下出现,一个业务过多深入和掺杂另一个非相关业务的实现细节。在项目和系统设计之初,特别要注意先划分业务边界,定义好接口设计和服务依赖关系,再着手开发;否则,延迟到后期做这些工作,很可能会导致重复的工作量,含糊复杂的交互、增加后期系统维护和问题排查的许多成本。磨刀不误砍柴工。划分清晰的业务、服务、接口边界就属于磨刀的功夫。


代码层次不合理


代码改动逻辑是正确的,然而代码的放置位置不符合当前架构设计约定,导致后续维护成本增加。


代码层次不合理可能导致重复代码。比如获取操作人和操作记录,如果写在类 XController 里, 那么类 YController 就面临尴尬局面: 如果写在 YController , 就会导致重复代码; 如果跨层去调用 XController 方法,又是非常不推荐的做法。因此, 获取操作人和操作记录,最好写在 Service 层, Controller 层只负责参数传入、检测和结果转译、返回。


不用多余的代码


工程中常常会有一些不用的代码。或者是一些暂时未用到的Util工具或库函数,或者是由于业务变更导致已经废弃不用的代码,或者是由于一时写出后来又重写的代码。尽量清除掉不用多余的代码,对系统可维护性是一种很好的改善,同时也有利于CodeReview。


使用全局变量


使用全局变量并没有“错”,错的是,一旦出现问题,排查和调试问题起来,真的会让人“一夜之间白了头”,耗费数个小时是轻微惩罚。此外,全局变量还能“顺手牵羊”地破坏函数的通用性,导致可维护性变差。务必消除全局变量的使用。当然,全局常量是可以的。


缺乏必要的注释


对重要和关键点的代码缺乏必要的注释,使用到的重要算法缺乏必要的引用出处,对特别的处理缺乏必要的说明。


原则上, 每个方法至少要用一个简短的单行注释, 适宜地描述了方法的用途、业务逻辑、作者及日期。对于特殊甚至奇葩的需求的特别实现,要加一些注释。 这样后续维护时有个基础。


更难发现的错误


更难发现的错误是指“复杂并发场景下的有一定技术难度的、需要丰富开发与设计经验才能看出来的错误”。


并发


并发的问题更难检测、复现和调试。常见的问题有:a. 在可能由多线程并发访问的对象中含有共享变量却没有同步保护;b. 在代码中手动创建缺乏控制的线程或线程池;c. 并发访问数据库时没有做任何同步措施;d. 多个线程对同一对象的互斥操作没有同步保护。


对于 a, 在大部分Java应用中,通常由Spring框架来控制和创建请求和服务实例,因此,保证“Controller, Service 类中的实例变量只允许 Service, DAO 的单例,不允许业务变量实例”基本确保没有并发不正确更新的问题;不过,包含缓存策略的对象要特别注意多线程并发访问的问题,出于性能考量, 尽量只对共享实例部分加锁。


对于 b, 禁止在应用中手动创建线程或线程池,失控的线程池很容易导致应用崩溃(有线上应用崩溃的教训)。


对于 c, 并发访问数据库时,要特别注意时序和状态同步。如果时序控制不对,会导致状态同步和更新出错。


对于 d, 对同一对象的互斥操作需要加分布式锁同步。


使用线程池、并发库、并发类、同步工具而不是线程对象、并发原语。在复杂并发场景下,还需注意多个同步对象上的锁是否按合适的顺序获得和释放以避免死锁,相应的错误处理代码是否合理。


事务


事务方面常出现的问题是:多个紧密关联的业务操作和 SQL 语句没有事务保证。 在资金业务操作或数据强一致性要求的业务操作中,要注意使用事务,保证数据更新的一致性和完整性。


SQL问题


SQL的正确性通常可以通过 DAO 测试来保证。 SQL问题主要是指潜在的性能问题和安全问题。


要避免SQL性能问题, 在表设计的时候就要做好索引工作。在表数据量非常大的情况下,SQL语句编写要非常小心。查询SQL需要添加必要索引,添加合适的查询条件和查询顺序,加快查询效率, 避免慢查; 尽量避免使用 Join, 子查询;避免SQL注入。


SQL优秀书籍推荐: SQL语言艺术


https://book.douban.com/subject/3012601/


安全问题


安全问题一向是互联网产品研发中极容易被忽视、而在爆发后又极引发热议的议题。安全和隐私是用户的心理红线之一。应用、数据、资金的安全性应当仅次于产品功能的准确性和使用体验。


安全问题的CodeReview可参见检查点清单:信息安全 。主要是如下措施: a. 严格检查和屏蔽非法输入; b. 对含敏感信息的请求加密通信; c. 业务处理后消除任何敏感私密信息的任何痕迹; d. 结果返回前在反序列化中清除敏感私密信息; e. 敏感私密信息在数据存储设备中应当加密存储; f. 应用有严格的角色、权限、操作、数据访问分级和控制; g. 切忌暴露服务器的重要的安全性信息,防止服务器被攻击影响正常服务运行。


设计问题


设计问题通常体现在: a. 是否有潜在的性能问题; b. 是否有安全问题; c. 业务变化时是否容易扩展; d. 是否有遗漏的点。


较轻微的问题


较轻微问题是指“没有技术难度、通过良好习惯即可避免的问题”。


较轻微问题一般不会造成负面影响的BUG或故障,不过建立一些好的习惯,主动使用代码检测工具,消除这些较轻微错误,也是一种修行。


命名不贴切


命名不贴切不会影响功能实现,却会误导理解或增加理解难度。


方法:先查查字典,找个通俗易懂而且比较贴近的名字。可以参考 jdk 的命名、通用词汇和行业词汇; 作用域小的采用短命名,作用域大的采用长命名。取名字是一种重要技能,—— 多少父母为此愁灰了头!


声明时未初始化


声明时未初始化通常情况下都不会是问题,因为后面会进行赋值。不过,如果赋值的过程中出现异常,那么可能会返回空值,从而导致空值异常。通常,变量声明时赋予默认初始值是个好习惯。


风格与整体有不一致


工程通常求稳,一致性能更好地维护。在工程项目中,最好能够遵循工程约定的风格,在个人项目中可以凸显个性风格。Java编程一般要遵循《Java编程规范》,有追求的程序猿媛还会追求更高层次的,比如《Google Java 规范》等。


类型转换错误


编程语言的类型系统是非常重要的。如何在不同类型之间可靠地互转,尤其是在父子类型之间相互赋值,也是一个微技能。滥用类型转换,也会导致BUG 。


Java 中容易出现的错误是:a. 字符串转数值,字符串含有非数字部分;b. JSON字符串转对象,某个字段含有不兼容的值类型导致解析出错;c. 子类型转不兼容的父类型,滋生运行时异常 ClassCastException;d. 相同特质的类型不兼容。比如 Long 与 Integer 都是数值型,却不能互转。


类型转换中最容易出BUG的地方是非布尔类型取反。受C语言的影响,很多高级语言支持各种数据类型转布尔类型,比如 PHP 字符串、数组、数字等都可以转布尔类型,相应的就喜欢写 if (!notBoolVar) 这种表达式, 容易隐藏看不出的BUG甚至错误。


否定式风格


变量含义、表达式语句倾向于使用否定式风格,可能不知不觉耗费大量脑细胞,因为每次理解的时候都要绕个弯子。 比如 isNoExpress 是否无需物流, 就有点绕。 为什么呢? 无需物流是针对快递发货的, 如果快递发货占发货的90%, 无需物流只占10%,那么, isNoExpress = false 几乎总为真。 涉及到判断的时候,可能不得不写 if (!isNoExpress) , 双重否定足够弄晕你。


容器遍历的结构变更


绝大多数语言都承袭了 C 语言的 for(int i=0;i


API参数传递错误


如果API参数有多个,而且相邻参数的类型相同,那么要特别留意是否参数顺序是正确的,而不会张冠李戴。


当然,在设计API参数的时候,就可以仔细用更精准类型进行区分,并将相同类型的参数错开。比如 calc(int accountNo, int pay, int timestamp) , 就容易传错,比较可靠的是 calc(int accountNo, Currency pay, Timestamp now) ,这样是不可能将参数传递错误的。


单行调用括号过多


为了简便,常常会写出 wapper(calc(now, String.format(“%s\n”, new BufferedFileReader(filename, “UTF-8″).readLines() ))) 的语句 , 嗯,你得好好瞧瞧和算算右边的括号数量是否正确了。更糟糕的时候,结合API参数传递错误,IDE 可能没有报错, 而你很可能没有意识到自己的参数传递错误了。 可靠的方式是, 拆出一部分变量,并将调用之间的括号用空格隔开,显示出层次感。


String fileContent = new BufferedFileReader(filename, 'UTF-8').readLines();

wapper( calc( now,  String.format('%s\n', fileContent) ) )


修改方法签名


对某个方法有业务改动时,程序猿媛们倾向直接修改原方法的签名。这时,要特别注意:a. 不要修改原方法的参数顺序; b. 在最后面增加可选参数。 从另一个角度来看,复杂的业务方法应当分两层: 最外层负责调度,方法参数具有包容性,里面包含的字段比较多 ; 内层方法负责特定业务逻辑的实现,方法参数少而精。


修改原方法签名本身就是容易产生问题的习惯, 篡改原方法的参数顺序更是大忌。 最好的方法是新建一个方法去复用原方法, 然后调用新的方法。代码变更始终铭记“开闭”原则。


打印日志太多


打印过多的日志并不好。一方面遮掩真正需要的信息,导致排查耗费时间, 另一方面造成服务器空间浪费、影响性能。生产环境日志一般只开放 INFO及以上级别的日志; Debug 日志只在调试或排错的时候使用,生产环境可以禁止debug日志。


多级数据结构


使用多级数据结构时,要确定父级数据一定有值,或者进行检测。比如 $order['baole']['ump']['money'],必须确保 $order['baole'], $order['baole']['money'] 一定有值或做非空检测。


作用域过大


由于C语言的影响,猿媛们会在开头就定义好一些变量或要返回的对象,在很靠后的地方才使用到。不必要的过大的作用域对变量和对象的变化产生不可测的影响,并增大理解的成本。可靠的方法是,仅当在使用时才定义,并尽快返回结果。


另一种情况是,暴露的访问域过大,比如 public 字段。 尽可能地缩小可访问的范围,可以增大变更和重构的空间; 减少可变性,则可以自然地获得并发安全性,降低CodeReview的理解成本。


比如,不可变的类和字段定义成 final , 最小化包,类,接口,方法和域的可访问性,默认为 private , 若需要继承,可定义为 protected , 仅当需要作为 API 服务暴露出去时,使用 public.


分支与循环


条件与循环偶尔也会导致错误, 不过通常错误可以在发布前解决掉。


对于 if-else 嵌套条件, 需要仔细检查是否符合业务逻辑; 如果嵌套太深,是否可以使用另一种方式“解结” ; 对于 switch 语句, 大多数语言的 case 有 fall through 问题, 要注意加上 break ; 最好加上 default 的处理。


对于 for 循环, 编写合理的结束条件避免死循环; 对于循环变量的控制, 避免出现 -1或 +1 错误, 消除越界错误; for 循环也要特别注意对空值和空容器的处理,避免抛出空值异常。可以通过单测来确保 for 循环的准确性。

看完本文有收获?请转发分享给更多人


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多