7、工程规划中的编程风格与抽象层级与方法设计的看法
很多人认为在做工程设计的时候就要有接口、抽象类,要弄一些实现类、子类,信奉这样一句话就是面向接口编程会给你带来无穷的好处,但我认为这东西以及这些东西带来的好处还是分阶段分层次的。如果说你当前这个阶段的业务逻辑就是非常简单且基本上不怎么扩展的话,要这些抽象、接口就没什么用,因为这个阶段你没有可抽象的参照。我们就拿经典的宠物商店的案例来说,你一开始就知道你要做的这个项目时宠物商店,然后你一开始就分析出了宠物有哪些特征有哪些共性,然后又找了一些具体的宠物分析了又哪些可能的个性,于是你就在项目之处定义好了接口、定义好了抽象父类这些。但是如果你一开始的时候并不知道你未来的业务走向,也无法拿到第二个相似的需要做抽象的业务,那我认为在这个阶段就是没必要做这个事情的,因为你在只有一个参照对象的情况下可以有非常多的抽象角度,抽象粒度太细了不好,太粗了也不好,分析对象特征的角度也是各种各样,总不能在这个阶段就拍脑袋说我就这么分析,我就要这么抽象,这也不合理呀。
所以我认为在开发初期,可以通过文档或注释的方式把几个可能性比较大的抽象方案给记录下来,然后后期需要抽象的时候再来进行合适的代码改造和接口抽取,完全没必要在一开始的初期就弄一堆没必要的接口和抽象类。我在项目构建初期的时候有过这样的体会,就是构思不完整需要进行大量的修改尝试的时候,如果有个接口就会很别扭,往往就是改到最后接口时肯定要改的,这种情况何必一开始的时候就作茧自缚。
不过当开始抽象的时候,推荐着重考虑泛型的参与使用,特别是在抽象一些概念和模型的时候,最好使用一下泛型并且是有边界的泛型以提供编译期的检查来确保类型的安全。虽然在javac编译的时候会擦除泛型,但是只会擦除到边界处,同时使用泛型也能使编码看起来更优雅。
回到工程规划,其实从某种角度来看就是方法的定义的规划在就是方法调用的规划,那么可以看作是规划我们要传哪些参数、什么类型的参数、多少个参数,规划是否返回值、返回怎样的值、什么类型的值。既然是写Java,从面向对象的角度来看,我们在参数比较多的时候,我们应该要定义一个JavaBean,不过从四舍五入的角度来看,我觉得在参数小于等于4的时候就直接传吧,就不用JavaBean了。返回值是多个的话,也要定义JavaBean以确保类型一致语义一致。有时候会觉得传Map会比较灵活,诚然,灵活是非常灵活,但是用Map来开发是需要基于一个文档来约定约束的,不过前后台参数的约定本来就是通过文档来完成的。不过如果你的业务逻辑深度很深,需要层层传参,那我觉得还是用Javabean来做会比较好,这样就能有一个编译期检查来保障一致。
上面说的都是最细节的方法规划,这种规划其实对应了四种方法类型,第一种是没有入参也没有返回值,第二种是没有入参但是有返回值,第三种是有入参没有返回值,第四种是有入参有返回值。我在网上看到过这样的说法,函数与方法的区别在于函数有返回值而方法没返回值。也就是说方法只是使用了方法体中产生的副作用,而函数更关注的是方法结束后的返回值。
在面向对象的Java中我还是习惯统称之为方法,我认为有两种类型需要返回值,一种是交互型方法,从语义上讲就是与这个对象进行交互,当然这个方法是要有返回值的,比如getter、generate等;第二种就是需要做到链式编程的方法,比如Builder模型,比如我见过的Netty的ServerBootstrap的初始化。
另外一类没有返回值的方法,我觉得称之为“自身能力方法”会比较合适,这种方法就是你调用我之后我就把该做的事情做好就行了,我不说有问题那就是没问题。虽然没有返回值,但是也分我会不会改变你传过来参数对象这两种情况,也就是说没有返回值也并不意味着你不关心我这个方法操作后的结果。比如你告诉我说去操场跑10圈,跑完了就完了,这就是传了一个参数10然后我执行了跑步的方法,然后我跑完了,然后就完了,这种就是我不会改变你传过来的参数,就算我偷懒,我只跑了1圈,然后跑完了就完了,我仍然没有改变你传过来的参数。又比如有个奇怪的冰淇淋店,特色是冰淇淋都是现做的,买法是你要先买一根上面写有你想买的味道的冰淇淋棍子,然后去自助的冰淇淋机器上,把棍子插进去,然后机器给你现做,做完后你的棍子上就有现做的冰淇淋了,你就可以自己拿走了,至于是拿着吃还是拿着使出一招星爆弃疗斩就随你开心了。
有些应该属于自身能力方法的,有很多人还是喜欢给个返回值以表示是成功还是失败,我个人是觉得如果确定该方法属于自身能力方法,这个返回值就是非常多余的,成功就成功了,不成功就抛异常。顺带还是再说一下,尽量避免返回null的情况,null的含义是未初始化或不存在,如果确定出现的是异常情况那就想办法给处理掉。
这些规划方法的细节最终会为整个项目的编程风格定下一个基调,一般在经过这样的规划后编程风格是能统一的。我们常见的编程风格又函数式编程、面向对象编程、面向过程编程。其实我觉得不管是函数式还是面向对象还是面向过程,总归是绕不开函数或者称之为方法的东西,往底层硬件方向看,我认为函数就是CPU指令的有序调用,然后函数实现了CPU指令块的复用;往高级语言方向看,我认为类和面向对象实现了数据和函数的复用。我认为编程风格就是如何组织这些方法,有的是将函数作为参数传入方法而有的是将一个函数式接口传入方法甚至有的就是直接传入基本数据,各自有各自的适用场景,只要能实现功能且效率高,然后代码易于阅读能准确体现写作思想,我觉得面向对象、面向过程还是函数式都行;至于普适性的风格,我觉得只要能做到高效、高复用、易维护,检查实现时的逻辑路径是否最短,接着再看复用性及复用时的冗余性高不高就好,反正大家都能看得懂。
8、建立业务逻辑模型
我还在Java培训班的时候,每次写完作业代码后我都会跟其他同学交流切磋一番,虽然表面上看起来我好像是为了装逼,其实我真的是为了交流切磋。我坐在第一排,坐我身后第二排的是培训班中唯三的女生,其中一个就是计算机专业的,作业还很简单的时候我会经常向她请教,我都是问她完成这个作业用了多少行代码,怎么实现的。当时打印三角形、菱形的作业她写的行数比我少,日期选择、时间计算的作业她写的行数比我少,我每次都会借她的代码观摩感悟一番,仔细思考为什么我会用这么多代码,究竟有没有冗余的逻辑,究竟是她的写法好还是我的写法好。我印象比较深刻的是那次练习流程循环的时候,她使用了do...while的循环方式,代码比我就直接少了一半,我当时如遭雷击,怀疑自己是否真的有这方面的天赋。但是培训班的时间非常紧张,没时间让我对怀疑进行仔细思考,而且再往后开始进行一些小项目的作业的时候,这位姑娘似乎也不那么积极地做作业了,我写完的时候她才刚开头。
尽管我没有时间对自己进行怀疑,但是我仍然抽出时间来思考实现功能与代码行数的问题。那个时候天真的我总以为,一个功能实现所用的代码行数越少越好。那个时候还没有接触到工程这个方向,都是单一的编程基础训练,那个时候还是用main方法启动应用来写一个“学生管理系统”,通过while(true)的循环来维持应用的可持续运行,通过if else来进行流程选择,作出的修改还是保存在内存中,我还是因为有了一个大胆的想法才将这个Map写到文件中以持久化。后来开始进行工程项目训练的时候,那时才对代码行数看淡了,因为一套写下来就是七八百行代码上千行了,跟当时的基础训练几十行代码就解决战斗的阶段已经有质的差别了,对于功能实现所用的代码行数也就看淡了——因为反正都写了这么多行了,多那一两行少那一两行也就没所谓了,这个时候的心态才使得我能更好地面对这个问题。写了两套之后,又查阅了许多资料,明白了工程需要有灵活性、可扩展性、健壮性,了解了设计模式有六大原则,有单一职责、里氏替换的父类子类原则、依赖倒置、接口隔离、最少了解外部的独立性原则、开闭原则。
后来经过一番思考后我得到的结论是,要义在于透过现象看本质,方法是基于这些设计模式的大原则在具体实现的时候选择最短的逻辑路径,这样写出来的代码才是有灵性。
这个要义我感觉有难度,想来想去似乎只能在当前的环境状态下尽可能地向这个方向靠近。想要透过现象看本质,我觉得就需要对这个事物有一个透彻的理解,透彻地理解就需要经过大量的试验和摸索,大量的试验和摸索意味着这是一个持续的而且有可能持续时间比较长的一个过程,这也就意味着透过现象看本质是一个不断提升更新的结果。往往在工程之初,经验不是很丰富的时候,我们的代码行文风格可能就很类似于脚本风格,就是分析在完成哪些原子性的步骤后就能实现功能,然后一步一步写出对应的指令代码。这个时候的总体思路可能就是线性的,但是事物的本质不一定的是线性的,我们需要思考为什么在经过这些步骤后就能实现目标功能,这些步骤有没有什么内在联系或共同的特征,我们可以将一些步骤、方法进一步抽象、编排后让计算机来执行,毕竟计算机智能顺序执行代码。这个就是由脚本向工程的演进,使我们的软件外在能表现这些现象,内在更接近事物的本质。
基于设计模式的原则选取最短逻辑路径感觉也不简单,你当前觉得这是最短最优的逻辑路径,但实际上可能有更好的。不过说这到这里,从另一个角度来看,有时候我们并不是要最优解,而是要最满意的解,我们还要考虑开发成本、维护成本、后续更新扩展的成本、销售角度的更新升级卖钱的策略等。不过从技术的角度来看,我们就不去理会那些可能属于勾心斗角范畴的内容,关注点就应该在如何取得最短逻辑路径上。
我认为寻找最短逻辑路径的方式在于选取更恰当的看待问题的角度。例如某个功能一开始考虑的是遍历匹配,但是如果这个集合非常大,那开销就线性甚至几何增长,然而我们的这个步骤的目的是为了找到相匹配的结果,除了遍历之外,我们还有什么方法呢?遍历是线性的方法,我们应该可以通过一定的算法来避免一些不必要的匹配,采用更恰当的解题步骤。我想起了初中的时候,曾经有一道数学题在分析的时候我列出了四个四元一次方程,也就是说这个方程组是可解的,但是变量代换会需要非常多的计算步骤,耗时是很长的,但是肯定能解出来,于是我就没有去解题,直接抄了同学的答案,后来被老师问起我怎么解的,我就说我是硬解出来的,当时我还是有点能力的所以于当时老师也没能确定我是不是在说谎。现在回想起来,说谎肯定不好,当时的方程组在认真分析条件后是可以分两个步骤来解更简单的方程组来解题的,解题更是要认真动动脑经采用更恰当的算法。
采用一个大胆的说法来讲,在建立业务逻辑模型的时候,要做到道心无漏、念头通达,你深刻理解了业务逻辑并建立了恰当的模型,你写的代码能正确实现你的模型。只不过,任重而道远啊。
9、构建项目的工具
项目源代码编写得差不多了之后肯定是要集成编译部署的,当然那些不需要编译的就当我没说。我所了解到的C/C++的源代码是要编译的,用的是make或者是cmake来进行构建的。Java的集成构建我所了解的第一代是Ant,第二代是maven,第三代是gradle。现在在国内Java构建工具似乎用maven的比用gradle多,可能是因为maven是使用xml来描述,大多数国人都能看得懂吧?可能是人们对xml解析和描述能力还不满足,于是就定义一些描述力更高但并不是很通用的只在这个领域使用的语言类型,于是就有了gradle了吧。不过现在开发的时候也都没怎么精研构建工具,因为如果有什么问题就去百度,百度不到就去谷歌,只要能满足构建需求就行了,更多精力还是放在开发的业务逻辑上。比如我们使用maven的时候,希望在所有的jar包都编译打包之后还能拷贝到指定的位置,我们就需要额外的拷贝插件。gradle的方式好像是直接编写代码,就不需要像maven一样先弄一个插件,然后再将插件绑定goal绑定生命周期才能使用。
我觉得这两者都要掌握,虽然曾经有一波潮流说gradle比maven的功能强,但是既然都是还在维护的活跃的工具,增加维护一些功能还是没什么难度的,要做到你有我也有也是没什么问题的。这两者就是工具,工具好用就行,需要用的时候再去查看使用说明书也未尝不可——前提是你的自身能力有把握临时看的时候一看就懂。
10、单元测试
单元测试说好写也好写说难写也难写,因为我们在写代码的时候,基本上都会是写一部分就单元测试一部分,但是写着写着就变成了改着改着,然后改着改着前面的业务逻辑甚至就已经被推翻了重写了。一般来讲,勉强及格的单元测试的编写应该是跟开发代码所用的时间是差不多的,但是往往在业务不稳定的阶段,一旦业务逻辑发生较大变动,之前写的单元测试很有可能也就完全废了。一个很亲切的大师告诉我说,当你写单元测试写到吐了,你就会明白名为TDD的测试驱动开发的精神了。
TDD的其中一个观点认为,可以通过测试用例来进行需求与开发与测试的沟通,这个观点认为这样就只需要定义好输入和输出即可,直接以结果说话,这样可以提高沟通效率和管理的效果,同时还能保证代码的质量。从抽象的层面,我是很赞同这个观点的,因为这基本上就是最好的选择,就相当于一开始我们就很明确我们需要什么东西,我作为一个开发我也是很乐于直接拿到要开发的最终目的结果。然而,这个结果应该怎样去表述呢?以怎样的形式去传达呢?是具体的单元测试代码吗?还是就是需求分析人员将需求描述写得更明确,然后开发自己根据这个描述去写自己的单元测试?
我觉得测试代码还是要开发自己去写的,这可是占很大比重工作量的任务,还需要会写一点代码,别人也不太愿意去写,写错了还要负责任。最关键的是,开发的时候会有大量的定义工作,相应的测试代码的编写肯定是要基于这些定义的结果来进行开发的,至少你总得知道你在写单元测试的时候,需要输入参数的变量名还有输出结果的变量名才行呀。
TDD有他的道理,也有他的使用范围,也有他的实践成本和相应的效果,但是我认为这应该不会是软件开发前中期的最佳实践。那位很亲切的大师告诉我,让我尝试在开发初期就先写好测试用例再进行业务逻辑的开发,但是我回想起来,最蛋疼的是我自己一开始根本不知道究竟怎样的结果是正确的,如果强行要在开发开始的时候就确定好期望结果,要么要花费大量的时间和精力让人觉得很划不来,要么就是暂定的期望结果并不正确。
撇开TDD的理念,我们单元测试还是要写的,说了这么多,核心还是在于为什么我没有写那么多单元测试,或者说为什么我没留下对应数目的单元测试用例代码。大部分时候都还是因为不想写,觉得单元测试太麻烦,写完了之后直接部署集成测试了,说到底就是很多时候不知道该怎么写单元测试,不知道怎样才能写出一个好的单元测试,不知道怎样才能快速地写出好的单元测试代码。所以我们需要一个套路,一个可以直接实践的靠谱的模板。
多的不说,你开发的代码总归是实现了一个个的功能,这些功能的入口处是肯定要来一波单元测试的。就拿经典的MVC模式来讲,你的Controller肯定每个方法要写一个单元测试,你Service层的每个方法肯定也要写一个单元测试,dao层当然也需要单元测试只不过最后是可以都ignore掉,因为dao的单元测试依赖于具体的数据环境和具体的数据,而且难以定下具体的预期结果。
我认为功能入口处的单元测试主要目的是来测试对应部分业务逻辑的主要流程,这个时候是还能使用mock来模拟一些结果不确定的对象和一些环境配置强依赖的对象以缩小单元测试范围,我们还需要一个主流程真实模拟的单元测试,或许这个就是被称为集成测试的单元测试,当然这个也是应该要被ignore的。
还有一个话题,就是私有方法究竟要不要写单元测试。一般来说私有方法都会要被某个公共方法调用的,否侧编译的时候会给警告,所以从理论上讲公共方法的单元测试用例是可以覆盖到私有方法的。不过我们在对照需求和工程设计来写的时候,往往也会遇到业务逻辑体量很大、业务流程很长的部分,这种情况我们在编码时会更倾向于将这一大段逻辑拆分得更小更细,有时候会写好几个私有的方法,甚至私有方法调用私有方法,有好几层的私有方法调用。对于这一块我们往往就会忽视写单元测试,想着运行不报错就对了,是那种能跑通就万岁的心态。
这种情况下来预先设定好入参和期望结果是很麻烦的一件事情,特别是当入参和出参的字段特别多的时候,复杂度大大增加。如果这个私有方法会在工程中持续存在并且是关键点的私有方法的时候,很有必要将其反射出来来写一个单元测试,否则就随着公共方法一起测试好了。
其实很多时候我们都是一边写代码一边写单元测试的,因为需要简单而快速地检验一下写出来的东西是否能运行。那种能一口气写几千行代码然后再回过头一口气写几百个单元测试用例的人然后一口气运行通过的已经是神了,就不讨论了。这种时候我们很需要提高敏感性,单元测试很多时候不仅仅是开发过程中简单而快速地检验一下写出来的东西是否能运行,更多地是以后代码优化的时候功能不会出错,所以我们需要定时补充结果正确的关键点单元测试。不过定时补充这个事情估计是很难的,因为代码写完了没什么问题,谁还去管单元测试的事情╮(╯_╰)╭。我想起年轻的我自己做饭吃的时候,都是做饭之前去洗锅吃之前去洗碗,吃完就直接摆在那不管了,只管自己肚子饱,哪管锅碗瓢盆脏。不过IDE里集成的单元测试代码覆盖率工具倒是能很好地促使一些强迫症去完善单元测试用例,因为你要是开启了这个覆盖检测的会话,要是没覆盖到的话你的代码背景就一直是红色的。
决定哪些地方要写单元测试,总结下来有这些情况,一个是所有功能点的入口方法,一个是关键的私有方法,一个是确保功能正确的用例。其他的单元测试用例还可以苟且一下,但是确保功能正确的用例中一定要使用Assert进行断言,这是回归测试时最关键的保障。这个时候我们可以考虑通过命名的方式来区分正确性单元测试和联通性单元测试,因为很多时候一开始就只是考虑代码跑不跑得通,后来才开始考虑跑出来的结果对不对,所以这两点区分开来后在后期维护单元测试的时候能合理分配时间和精力。
那么怎样又快又好地写出高质量的单元测试呢?有一个敏捷开发的专家认为,对于重要逻辑进行小心谨慎的单元测试,对于那种有把握有信心的简单代码就尽可能少地单元测试。我觉得就是说对于那种字段正确性校验的代码,如果字段之间没有关联那就不用写了,如果字段之间有关联性的逻辑那就需要写单元测试以确保逻辑不会紊乱。
确定哪些要写哪些不要写之后,就是确定要怎么写。还能怎么写,当然是用手写啊!不过我还是想,如果能代码自动生成就好了,在设置单元测试用的输入参数和期望结果的时候真的很烦,基本上就是复制粘贴,复制粘贴还很容易出错,但该写的还是要写啊。
其实说了这么多,关键点还是在于怎么去mock,去把变化的因素给固定下来。但是需求变更就已经带来了巨大的变化,很有可能技术方案甚至整个架构就发生变化了。但还是要mock,就算是需求变化了,也要预先设定好输入和输出,然后写好单元测试,定好正确性的基调。在写的时候分层,Spring自己是集成了mockito的,可以在运行Spring单元测试的时候使用mockito的功能,具体怎么用,还有待进一步的探索,但是至少在写的时候做好代码分层,这样就可以在恰当的分层点进行合适的mock行为。
其实mock的用法很简单,甚至说我们很多时候只需要很简单地使用mock即可。从语义上讲,我们使用mockito会更易读,但是mockito不能模拟私有的和静态的方法,这个时候可以和jmockit混用。不过一般来说当出现需要混用的情况的时候,需要优先反思一下代码层次结构是否合理。
单元测试总归到底是单元性的测试,组装起来对不对也还是要测试的,但是我们不能因此就轻视了单元测试,有些时候因为基础数据异常和违反需求约规出现的异常很多时候还是要通过单元测试来帮忙复现的。这让我想起了我修电脑的时候,把电脑拆开进行零件的替换、维护时,一般都会先把零件插好先点亮测一波看能不能正常启动,能正常启动的话就再简单地用一会看看会不会有什么问题,确定没什么大问题后才将整机零件一个一个都装回去。所以我想说的就是单元测试是组装前的检测,整体测试是功能拼接的大一级的单元测试,还有最后打包集成部署测试,要好好利用mock的功能来尽可能地消除变化因素给单元测试结果带来的影响以确保最后拼装对接的时候少发生意外。
11、健壮性与异常抵抗
我们自己会进行单元测试,但是一般很少会对非业务的异常情况进行单元测试验证,因为这种人为的异常本身就是很神经病的操作,而也正是因为这些神经病的操作往往会把应用给搞垮。我们的代码不能太脆弱,不能我们在打开界面操作的时候畏畏缩缩生怕把应用给弄崩了,我们的代码应该要是健壮的。像数组下标越界这种情况就不应该出现,在任何时候都不应该出现,如果一些骚操作引起了这种问题就只能说写代码的时候太粗心了。
但是像一些业务异常的情况我们是否应该要抵抗呢?因为一些操作已经不符合业务基础要求了,肯定产生的会是异常的结果,一个错误的输入就应该产生错误的输出甚至没有输出,那我们的代码是否应该要cover掉这种情况呢?比如说乱序,我们肯定是已经知道了排序的规则才能判断乱序,那既然能判断出乱序那我们就能在程序中作出乱序抵抗,但是我们写的代码应该要做出抵抗吗?
还有比如业务字段缺失或不符合枚举值范围,这种情况我们应该进行校验并抛出异常吗?那要校验的话岂不是每个字段都要进行校验?既然要校验,那校验的逻辑又是什么呢?我想说,开发代码,本就应该是基于一套约规或规范来展开的,如果基于这套约规规范还要考虑这异常那缺失,那要这套东西来界定范围干嘛,难道我们写代码还要考虑如果发生了大停电的时候我们的应用该怎么样正常运行吗?
我觉得,我们还是要考虑异常抵抗的。我们或许可以从恶意攻击的角度来考虑异常抵抗。以前做运维的时候,有朋友为了避免别人暴力刷服务器流量,使用了fail2ban这个工具,判断ip尝试的失败次数,失败了就加入黑名单。比如web接口中我们可以考虑防止sql注入——当然现在使用sql预编译的方式写代码已经可以不用考虑这个事情了。似乎现在这种情况也越来越少了,恶意攻击似乎也并没有那么容易了,我们只开放必要的端口,也没什么办法能进行攻击。
说到异常,我想到的就是减速、昏迷、中毒、点燃、破甲、虚弱、混乱、致盲……这些是我第一时间能想到的异常,Java中常见的就是空指针、除0、非法参数、数组下标越界。不过这些异常应该都是致命的,我们非常需要对这些异常进行预防,这个预防也就是所谓的抵抗。空指针的全预防有点难,但是其他的真的就是不应该出现,在可能出现异常之前就进行必要的判断。
不过话又说回来,如果征得业务同意确定需要对某些已知的可能发生的致命的异常需要进行恢复,那我们还是可以做一做的,因为毕竟到时候到了生产上真出现了还能有手段恢复,那就能省去不少麻烦。
12、问题排查思路与排查效率与代码review与代码语义优化
即便是我们在开发的过程中进行了相应的单元测试,有时候也难免在集成部署运行的时候会出现问题。既然出现了问题,我们就得去查,但是怎么查?怎样查才能尽快得出结论才能破案?
既然出问题了,肯定是有报告者有reporter的,根据这个初步的线索去尝试复现现象,去缩小问题的范围。在界定范围后,再判断这是代码级的问题还是环境级的问题,比如后台有没有返回值、返回值是否正确、消息有没有收到、原始消息的内容是否正确。如果是出现了异常,我们基本可以根据异常报的行数来去检查代码,根据代码附近的逻辑来推断问题的症结。
对于问题排查的效率,很多时候就是经验的积累,然后利用已有的经验去扩展增加经验。不过这个增加经验的的根本方法,还是根据现象去分析其运行的本质,在梳理本质的时候去复盘案发时的行为。对于问题的定位,我觉得一个有用的技巧就是选择恰当的问题样本,然后拿到这个样本之后在自己的环境中进行测试分析验证——好像那些科研狂人再采集到样本后也是这么做的。根据样本,通过单元测试来debug代码运行的过程,查看运行过程中的流程走向,查看运行过程中的值的变化。
问题查完并解决了,那么就到了反思的时候了,为什么这问题在单元测试的时候没测出来呢?又回到那个问题,很多时候我们没写那么多单元测试,没做那么多的数据校验,然后就提交了,然后就部署测试了。这样简单地就提交了,没有代码review,确实不太好,于是我百度了一下,看到一篇文章中说笔者认为review不应该承担发现问题、纠正风格的职责,Code Review主要是审核代码的质量,如可读性,可维护性,以及程序的逻辑和对需求和设计的实现。是的,很多时候我们出现bug就是因为我们的实现与需求描述不相符,所以从某种意义上说review检查代码实现与需求逻辑是否一致就是在协助发现检查代码中的bug。
但是工期紧一直都是我们没时间review代码的因素,或者说借口。我们每个人都会收到自己要负责开发的模块,于是我们就会去专注对应部分的需求并简单地了解相关联模块的需求的业务逻辑,如果要我们去review别人的代码,就意味着我们需要再去关注别的需求去理解别的业务逻辑。如果我们要review别人代码前需要自己去研读需求需要自己再去将需求分析成代码的设计思路就很吃力了,因为大家自己都有自己的未完成项目,自己都有一堆问题要考虑,哪有时间去帮别人去考虑问题。所以我认为这种code review活动应该由一个没有繁重开发任务的人员来牵头,然后在各开发人员的代码方向较为明确稳定之后再来不定期举行,这个时候也基本上到了项目的中后期。
review的时候有很多事情需要做,发现bug只是顺带的事情,我个人会更倾向于在review的时候进行语义的优化,这样代码的可读性就会好上很多,语义优化之后就能更容易地进行更高一层的业务逻辑抽象,这或许就是review代码真正的价值所在了吧。所以,我认为最好主动去请别人review代码,然后最好能主动介绍业务需求,再主动介绍自己的设计思路以及如何产生这样的设计思路的,接下来就可以闭嘴请对方静静地看代码了。
很多人认为在做工程设计的时候就要有接口、抽象类,要弄一些实现类、子类,信奉这样一句话就是面向接口编程会给你带来无穷的好处,但我认为这东西以及这些东西带来的好处还是分阶段分层次的。如果说你当前这个阶段的业务逻辑就是非常简单且基本上不怎么扩展的话,要这些抽象、接口就没什么用,因为这个阶段你没有可抽象的参照。我们就拿经典的宠物商店的案例来说,你一开始就知道你要做的这个项目时宠物商店,然后你一开始就分析出了宠物有哪些特征有哪些共性,然后又找了一些具体的宠物分析了又哪些可能的个性,于是你就在项目之处定义好了接口、定义好了抽象父类这些。但是如果你一开始的时候并不知道你未来的业务走向,也无法拿到第二个相似的需要做抽象的业务,那我认为在这个阶段就是没必要做这个事情的,因为你在只有一个参照对象的情况下可以有非常多的抽象角度,抽象粒度太细了不好,太粗了也不好,分析对象特征的角度也是各种各样,总不能在这个阶段就拍脑袋说我就这么分析,我就要这么抽象,这也不合理呀。
所以我认为在开发初期,可以通过文档或注释的方式把几个可能性比较大的抽象方案给记录下来,然后后期需要抽象的时候再来进行合适的代码改造和接口抽取,完全没必要在一开始的初期就弄一堆没必要的接口和抽象类。我在项目构建初期的时候有过这样的体会,就是构思不完整需要进行大量的修改尝试的时候,如果有个接口就会很别扭,往往就是改到最后接口时肯定要改的,这种情况何必一开始的时候就作茧自缚。
不过当开始抽象的时候,推荐着重考虑泛型的参与使用,特别是在抽象一些概念和模型的时候,最好使用一下泛型并且是有边界的泛型以提供编译期的检查来确保类型的安全。虽然在javac编译的时候会擦除泛型,但是只会擦除到边界处,同时使用泛型也能使编码看起来更优雅。
回到工程规划,其实从某种角度来看就是方法的定义的规划在就是方法调用的规划,那么可以看作是规划我们要传哪些参数、什么类型的参数、多少个参数,规划是否返回值、返回怎样的值、什么类型的值。既然是写Java,从面向对象的角度来看,我们在参数比较多的时候,我们应该要定义一个JavaBean,不过从四舍五入的角度来看,我觉得在参数小于等于4的时候就直接传吧,就不用JavaBean了。返回值是多个的话,也要定义JavaBean以确保类型一致语义一致。有时候会觉得传Map会比较灵活,诚然,灵活是非常灵活,但是用Map来开发是需要基于一个文档来约定约束的,不过前后台参数的约定本来就是通过文档来完成的。不过如果你的业务逻辑深度很深,需要层层传参,那我觉得还是用Javabean来做会比较好,这样就能有一个编译期检查来保障一致。
上面说的都是最细节的方法规划,这种规划其实对应了四种方法类型,第一种是没有入参也没有返回值,第二种是没有入参但是有返回值,第三种是有入参没有返回值,第四种是有入参有返回值。我在网上看到过这样的说法,函数与方法的区别在于函数有返回值而方法没返回值。也就是说方法只是使用了方法体中产生的副作用,而函数更关注的是方法结束后的返回值。
在面向对象的Java中我还是习惯统称之为方法,我认为有两种类型需要返回值,一种是交互型方法,从语义上讲就是与这个对象进行交互,当然这个方法是要有返回值的,比如getter、generate等;第二种就是需要做到链式编程的方法,比如Builder模型,比如我见过的Netty的ServerBootstrap的初始化。
另外一类没有返回值的方法,我觉得称之为“自身能力方法”会比较合适,这种方法就是你调用我之后我就把该做的事情做好就行了,我不说有问题那就是没问题。虽然没有返回值,但是也分我会不会改变你传过来参数对象这两种情况,也就是说没有返回值也并不意味着你不关心我这个方法操作后的结果。比如你告诉我说去操场跑10圈,跑完了就完了,这就是传了一个参数10然后我执行了跑步的方法,然后我跑完了,然后就完了,这种就是我不会改变你传过来的参数,就算我偷懒,我只跑了1圈,然后跑完了就完了,我仍然没有改变你传过来的参数。又比如有个奇怪的冰淇淋店,特色是冰淇淋都是现做的,买法是你要先买一根上面写有你想买的味道的冰淇淋棍子,然后去自助的冰淇淋机器上,把棍子插进去,然后机器给你现做,做完后你的棍子上就有现做的冰淇淋了,你就可以自己拿走了,至于是拿着吃还是拿着使出一招星爆弃疗斩就随你开心了。
有些应该属于自身能力方法的,有很多人还是喜欢给个返回值以表示是成功还是失败,我个人是觉得如果确定该方法属于自身能力方法,这个返回值就是非常多余的,成功就成功了,不成功就抛异常。顺带还是再说一下,尽量避免返回null的情况,null的含义是未初始化或不存在,如果确定出现的是异常情况那就想办法给处理掉。
这些规划方法的细节最终会为整个项目的编程风格定下一个基调,一般在经过这样的规划后编程风格是能统一的。我们常见的编程风格又函数式编程、面向对象编程、面向过程编程。其实我觉得不管是函数式还是面向对象还是面向过程,总归是绕不开函数或者称之为方法的东西,往底层硬件方向看,我认为函数就是CPU指令的有序调用,然后函数实现了CPU指令块的复用;往高级语言方向看,我认为类和面向对象实现了数据和函数的复用。我认为编程风格就是如何组织这些方法,有的是将函数作为参数传入方法而有的是将一个函数式接口传入方法甚至有的就是直接传入基本数据,各自有各自的适用场景,只要能实现功能且效率高,然后代码易于阅读能准确体现写作思想,我觉得面向对象、面向过程还是函数式都行;至于普适性的风格,我觉得只要能做到高效、高复用、易维护,检查实现时的逻辑路径是否最短,接着再看复用性及复用时的冗余性高不高就好,反正大家都能看得懂。
8、建立业务逻辑模型
我还在Java培训班的时候,每次写完作业代码后我都会跟其他同学交流切磋一番,虽然表面上看起来我好像是为了装逼,其实我真的是为了交流切磋。我坐在第一排,坐我身后第二排的是培训班中唯三的女生,其中一个就是计算机专业的,作业还很简单的时候我会经常向她请教,我都是问她完成这个作业用了多少行代码,怎么实现的。当时打印三角形、菱形的作业她写的行数比我少,日期选择、时间计算的作业她写的行数比我少,我每次都会借她的代码观摩感悟一番,仔细思考为什么我会用这么多代码,究竟有没有冗余的逻辑,究竟是她的写法好还是我的写法好。我印象比较深刻的是那次练习流程循环的时候,她使用了do...while的循环方式,代码比我就直接少了一半,我当时如遭雷击,怀疑自己是否真的有这方面的天赋。但是培训班的时间非常紧张,没时间让我对怀疑进行仔细思考,而且再往后开始进行一些小项目的作业的时候,这位姑娘似乎也不那么积极地做作业了,我写完的时候她才刚开头。
尽管我没有时间对自己进行怀疑,但是我仍然抽出时间来思考实现功能与代码行数的问题。那个时候天真的我总以为,一个功能实现所用的代码行数越少越好。那个时候还没有接触到工程这个方向,都是单一的编程基础训练,那个时候还是用main方法启动应用来写一个“学生管理系统”,通过while(true)的循环来维持应用的可持续运行,通过if else来进行流程选择,作出的修改还是保存在内存中,我还是因为有了一个大胆的想法才将这个Map写到文件中以持久化。后来开始进行工程项目训练的时候,那时才对代码行数看淡了,因为一套写下来就是七八百行代码上千行了,跟当时的基础训练几十行代码就解决战斗的阶段已经有质的差别了,对于功能实现所用的代码行数也就看淡了——因为反正都写了这么多行了,多那一两行少那一两行也就没所谓了,这个时候的心态才使得我能更好地面对这个问题。写了两套之后,又查阅了许多资料,明白了工程需要有灵活性、可扩展性、健壮性,了解了设计模式有六大原则,有单一职责、里氏替换的父类子类原则、依赖倒置、接口隔离、最少了解外部的独立性原则、开闭原则。
后来经过一番思考后我得到的结论是,要义在于透过现象看本质,方法是基于这些设计模式的大原则在具体实现的时候选择最短的逻辑路径,这样写出来的代码才是有灵性。
这个要义我感觉有难度,想来想去似乎只能在当前的环境状态下尽可能地向这个方向靠近。想要透过现象看本质,我觉得就需要对这个事物有一个透彻的理解,透彻地理解就需要经过大量的试验和摸索,大量的试验和摸索意味着这是一个持续的而且有可能持续时间比较长的一个过程,这也就意味着透过现象看本质是一个不断提升更新的结果。往往在工程之初,经验不是很丰富的时候,我们的代码行文风格可能就很类似于脚本风格,就是分析在完成哪些原子性的步骤后就能实现功能,然后一步一步写出对应的指令代码。这个时候的总体思路可能就是线性的,但是事物的本质不一定的是线性的,我们需要思考为什么在经过这些步骤后就能实现目标功能,这些步骤有没有什么内在联系或共同的特征,我们可以将一些步骤、方法进一步抽象、编排后让计算机来执行,毕竟计算机智能顺序执行代码。这个就是由脚本向工程的演进,使我们的软件外在能表现这些现象,内在更接近事物的本质。
基于设计模式的原则选取最短逻辑路径感觉也不简单,你当前觉得这是最短最优的逻辑路径,但实际上可能有更好的。不过说这到这里,从另一个角度来看,有时候我们并不是要最优解,而是要最满意的解,我们还要考虑开发成本、维护成本、后续更新扩展的成本、销售角度的更新升级卖钱的策略等。不过从技术的角度来看,我们就不去理会那些可能属于勾心斗角范畴的内容,关注点就应该在如何取得最短逻辑路径上。
我认为寻找最短逻辑路径的方式在于选取更恰当的看待问题的角度。例如某个功能一开始考虑的是遍历匹配,但是如果这个集合非常大,那开销就线性甚至几何增长,然而我们的这个步骤的目的是为了找到相匹配的结果,除了遍历之外,我们还有什么方法呢?遍历是线性的方法,我们应该可以通过一定的算法来避免一些不必要的匹配,采用更恰当的解题步骤。我想起了初中的时候,曾经有一道数学题在分析的时候我列出了四个四元一次方程,也就是说这个方程组是可解的,但是变量代换会需要非常多的计算步骤,耗时是很长的,但是肯定能解出来,于是我就没有去解题,直接抄了同学的答案,后来被老师问起我怎么解的,我就说我是硬解出来的,当时我还是有点能力的所以于当时老师也没能确定我是不是在说谎。现在回想起来,说谎肯定不好,当时的方程组在认真分析条件后是可以分两个步骤来解更简单的方程组来解题的,解题更是要认真动动脑经采用更恰当的算法。
采用一个大胆的说法来讲,在建立业务逻辑模型的时候,要做到道心无漏、念头通达,你深刻理解了业务逻辑并建立了恰当的模型,你写的代码能正确实现你的模型。只不过,任重而道远啊。
9、构建项目的工具
项目源代码编写得差不多了之后肯定是要集成编译部署的,当然那些不需要编译的就当我没说。我所了解到的C/C++的源代码是要编译的,用的是make或者是cmake来进行构建的。Java的集成构建我所了解的第一代是Ant,第二代是maven,第三代是gradle。现在在国内Java构建工具似乎用maven的比用gradle多,可能是因为maven是使用xml来描述,大多数国人都能看得懂吧?可能是人们对xml解析和描述能力还不满足,于是就定义一些描述力更高但并不是很通用的只在这个领域使用的语言类型,于是就有了gradle了吧。不过现在开发的时候也都没怎么精研构建工具,因为如果有什么问题就去百度,百度不到就去谷歌,只要能满足构建需求就行了,更多精力还是放在开发的业务逻辑上。比如我们使用maven的时候,希望在所有的jar包都编译打包之后还能拷贝到指定的位置,我们就需要额外的拷贝插件。gradle的方式好像是直接编写代码,就不需要像maven一样先弄一个插件,然后再将插件绑定goal绑定生命周期才能使用。
我觉得这两者都要掌握,虽然曾经有一波潮流说gradle比maven的功能强,但是既然都是还在维护的活跃的工具,增加维护一些功能还是没什么难度的,要做到你有我也有也是没什么问题的。这两者就是工具,工具好用就行,需要用的时候再去查看使用说明书也未尝不可——前提是你的自身能力有把握临时看的时候一看就懂。
10、单元测试
单元测试说好写也好写说难写也难写,因为我们在写代码的时候,基本上都会是写一部分就单元测试一部分,但是写着写着就变成了改着改着,然后改着改着前面的业务逻辑甚至就已经被推翻了重写了。一般来讲,勉强及格的单元测试的编写应该是跟开发代码所用的时间是差不多的,但是往往在业务不稳定的阶段,一旦业务逻辑发生较大变动,之前写的单元测试很有可能也就完全废了。一个很亲切的大师告诉我说,当你写单元测试写到吐了,你就会明白名为TDD的测试驱动开发的精神了。
TDD的其中一个观点认为,可以通过测试用例来进行需求与开发与测试的沟通,这个观点认为这样就只需要定义好输入和输出即可,直接以结果说话,这样可以提高沟通效率和管理的效果,同时还能保证代码的质量。从抽象的层面,我是很赞同这个观点的,因为这基本上就是最好的选择,就相当于一开始我们就很明确我们需要什么东西,我作为一个开发我也是很乐于直接拿到要开发的最终目的结果。然而,这个结果应该怎样去表述呢?以怎样的形式去传达呢?是具体的单元测试代码吗?还是就是需求分析人员将需求描述写得更明确,然后开发自己根据这个描述去写自己的单元测试?
我觉得测试代码还是要开发自己去写的,这可是占很大比重工作量的任务,还需要会写一点代码,别人也不太愿意去写,写错了还要负责任。最关键的是,开发的时候会有大量的定义工作,相应的测试代码的编写肯定是要基于这些定义的结果来进行开发的,至少你总得知道你在写单元测试的时候,需要输入参数的变量名还有输出结果的变量名才行呀。
TDD有他的道理,也有他的使用范围,也有他的实践成本和相应的效果,但是我认为这应该不会是软件开发前中期的最佳实践。那位很亲切的大师告诉我,让我尝试在开发初期就先写好测试用例再进行业务逻辑的开发,但是我回想起来,最蛋疼的是我自己一开始根本不知道究竟怎样的结果是正确的,如果强行要在开发开始的时候就确定好期望结果,要么要花费大量的时间和精力让人觉得很划不来,要么就是暂定的期望结果并不正确。
撇开TDD的理念,我们单元测试还是要写的,说了这么多,核心还是在于为什么我没有写那么多单元测试,或者说为什么我没留下对应数目的单元测试用例代码。大部分时候都还是因为不想写,觉得单元测试太麻烦,写完了之后直接部署集成测试了,说到底就是很多时候不知道该怎么写单元测试,不知道怎样才能写出一个好的单元测试,不知道怎样才能快速地写出好的单元测试代码。所以我们需要一个套路,一个可以直接实践的靠谱的模板。
多的不说,你开发的代码总归是实现了一个个的功能,这些功能的入口处是肯定要来一波单元测试的。就拿经典的MVC模式来讲,你的Controller肯定每个方法要写一个单元测试,你Service层的每个方法肯定也要写一个单元测试,dao层当然也需要单元测试只不过最后是可以都ignore掉,因为dao的单元测试依赖于具体的数据环境和具体的数据,而且难以定下具体的预期结果。
我认为功能入口处的单元测试主要目的是来测试对应部分业务逻辑的主要流程,这个时候是还能使用mock来模拟一些结果不确定的对象和一些环境配置强依赖的对象以缩小单元测试范围,我们还需要一个主流程真实模拟的单元测试,或许这个就是被称为集成测试的单元测试,当然这个也是应该要被ignore的。
还有一个话题,就是私有方法究竟要不要写单元测试。一般来说私有方法都会要被某个公共方法调用的,否侧编译的时候会给警告,所以从理论上讲公共方法的单元测试用例是可以覆盖到私有方法的。不过我们在对照需求和工程设计来写的时候,往往也会遇到业务逻辑体量很大、业务流程很长的部分,这种情况我们在编码时会更倾向于将这一大段逻辑拆分得更小更细,有时候会写好几个私有的方法,甚至私有方法调用私有方法,有好几层的私有方法调用。对于这一块我们往往就会忽视写单元测试,想着运行不报错就对了,是那种能跑通就万岁的心态。
这种情况下来预先设定好入参和期望结果是很麻烦的一件事情,特别是当入参和出参的字段特别多的时候,复杂度大大增加。如果这个私有方法会在工程中持续存在并且是关键点的私有方法的时候,很有必要将其反射出来来写一个单元测试,否则就随着公共方法一起测试好了。
其实很多时候我们都是一边写代码一边写单元测试的,因为需要简单而快速地检验一下写出来的东西是否能运行。那种能一口气写几千行代码然后再回过头一口气写几百个单元测试用例的人然后一口气运行通过的已经是神了,就不讨论了。这种时候我们很需要提高敏感性,单元测试很多时候不仅仅是开发过程中简单而快速地检验一下写出来的东西是否能运行,更多地是以后代码优化的时候功能不会出错,所以我们需要定时补充结果正确的关键点单元测试。不过定时补充这个事情估计是很难的,因为代码写完了没什么问题,谁还去管单元测试的事情╮(╯_╰)╭。我想起年轻的我自己做饭吃的时候,都是做饭之前去洗锅吃之前去洗碗,吃完就直接摆在那不管了,只管自己肚子饱,哪管锅碗瓢盆脏。不过IDE里集成的单元测试代码覆盖率工具倒是能很好地促使一些强迫症去完善单元测试用例,因为你要是开启了这个覆盖检测的会话,要是没覆盖到的话你的代码背景就一直是红色的。
决定哪些地方要写单元测试,总结下来有这些情况,一个是所有功能点的入口方法,一个是关键的私有方法,一个是确保功能正确的用例。其他的单元测试用例还可以苟且一下,但是确保功能正确的用例中一定要使用Assert进行断言,这是回归测试时最关键的保障。这个时候我们可以考虑通过命名的方式来区分正确性单元测试和联通性单元测试,因为很多时候一开始就只是考虑代码跑不跑得通,后来才开始考虑跑出来的结果对不对,所以这两点区分开来后在后期维护单元测试的时候能合理分配时间和精力。
那么怎样又快又好地写出高质量的单元测试呢?有一个敏捷开发的专家认为,对于重要逻辑进行小心谨慎的单元测试,对于那种有把握有信心的简单代码就尽可能少地单元测试。我觉得就是说对于那种字段正确性校验的代码,如果字段之间没有关联那就不用写了,如果字段之间有关联性的逻辑那就需要写单元测试以确保逻辑不会紊乱。
确定哪些要写哪些不要写之后,就是确定要怎么写。还能怎么写,当然是用手写啊!不过我还是想,如果能代码自动生成就好了,在设置单元测试用的输入参数和期望结果的时候真的很烦,基本上就是复制粘贴,复制粘贴还很容易出错,但该写的还是要写啊。
其实说了这么多,关键点还是在于怎么去mock,去把变化的因素给固定下来。但是需求变更就已经带来了巨大的变化,很有可能技术方案甚至整个架构就发生变化了。但还是要mock,就算是需求变化了,也要预先设定好输入和输出,然后写好单元测试,定好正确性的基调。在写的时候分层,Spring自己是集成了mockito的,可以在运行Spring单元测试的时候使用mockito的功能,具体怎么用,还有待进一步的探索,但是至少在写的时候做好代码分层,这样就可以在恰当的分层点进行合适的mock行为。
其实mock的用法很简单,甚至说我们很多时候只需要很简单地使用mock即可。从语义上讲,我们使用mockito会更易读,但是mockito不能模拟私有的和静态的方法,这个时候可以和jmockit混用。不过一般来说当出现需要混用的情况的时候,需要优先反思一下代码层次结构是否合理。
单元测试总归到底是单元性的测试,组装起来对不对也还是要测试的,但是我们不能因此就轻视了单元测试,有些时候因为基础数据异常和违反需求约规出现的异常很多时候还是要通过单元测试来帮忙复现的。这让我想起了我修电脑的时候,把电脑拆开进行零件的替换、维护时,一般都会先把零件插好先点亮测一波看能不能正常启动,能正常启动的话就再简单地用一会看看会不会有什么问题,确定没什么大问题后才将整机零件一个一个都装回去。所以我想说的就是单元测试是组装前的检测,整体测试是功能拼接的大一级的单元测试,还有最后打包集成部署测试,要好好利用mock的功能来尽可能地消除变化因素给单元测试结果带来的影响以确保最后拼装对接的时候少发生意外。
11、健壮性与异常抵抗
我们自己会进行单元测试,但是一般很少会对非业务的异常情况进行单元测试验证,因为这种人为的异常本身就是很神经病的操作,而也正是因为这些神经病的操作往往会把应用给搞垮。我们的代码不能太脆弱,不能我们在打开界面操作的时候畏畏缩缩生怕把应用给弄崩了,我们的代码应该要是健壮的。像数组下标越界这种情况就不应该出现,在任何时候都不应该出现,如果一些骚操作引起了这种问题就只能说写代码的时候太粗心了。
但是像一些业务异常的情况我们是否应该要抵抗呢?因为一些操作已经不符合业务基础要求了,肯定产生的会是异常的结果,一个错误的输入就应该产生错误的输出甚至没有输出,那我们的代码是否应该要cover掉这种情况呢?比如说乱序,我们肯定是已经知道了排序的规则才能判断乱序,那既然能判断出乱序那我们就能在程序中作出乱序抵抗,但是我们写的代码应该要做出抵抗吗?
还有比如业务字段缺失或不符合枚举值范围,这种情况我们应该进行校验并抛出异常吗?那要校验的话岂不是每个字段都要进行校验?既然要校验,那校验的逻辑又是什么呢?我想说,开发代码,本就应该是基于一套约规或规范来展开的,如果基于这套约规规范还要考虑这异常那缺失,那要这套东西来界定范围干嘛,难道我们写代码还要考虑如果发生了大停电的时候我们的应用该怎么样正常运行吗?
我觉得,我们还是要考虑异常抵抗的。我们或许可以从恶意攻击的角度来考虑异常抵抗。以前做运维的时候,有朋友为了避免别人暴力刷服务器流量,使用了fail2ban这个工具,判断ip尝试的失败次数,失败了就加入黑名单。比如web接口中我们可以考虑防止sql注入——当然现在使用sql预编译的方式写代码已经可以不用考虑这个事情了。似乎现在这种情况也越来越少了,恶意攻击似乎也并没有那么容易了,我们只开放必要的端口,也没什么办法能进行攻击。
说到异常,我想到的就是减速、昏迷、中毒、点燃、破甲、虚弱、混乱、致盲……这些是我第一时间能想到的异常,Java中常见的就是空指针、除0、非法参数、数组下标越界。不过这些异常应该都是致命的,我们非常需要对这些异常进行预防,这个预防也就是所谓的抵抗。空指针的全预防有点难,但是其他的真的就是不应该出现,在可能出现异常之前就进行必要的判断。
不过话又说回来,如果征得业务同意确定需要对某些已知的可能发生的致命的异常需要进行恢复,那我们还是可以做一做的,因为毕竟到时候到了生产上真出现了还能有手段恢复,那就能省去不少麻烦。
12、问题排查思路与排查效率与代码review与代码语义优化
即便是我们在开发的过程中进行了相应的单元测试,有时候也难免在集成部署运行的时候会出现问题。既然出现了问题,我们就得去查,但是怎么查?怎样查才能尽快得出结论才能破案?
既然出问题了,肯定是有报告者有reporter的,根据这个初步的线索去尝试复现现象,去缩小问题的范围。在界定范围后,再判断这是代码级的问题还是环境级的问题,比如后台有没有返回值、返回值是否正确、消息有没有收到、原始消息的内容是否正确。如果是出现了异常,我们基本可以根据异常报的行数来去检查代码,根据代码附近的逻辑来推断问题的症结。
对于问题排查的效率,很多时候就是经验的积累,然后利用已有的经验去扩展增加经验。不过这个增加经验的的根本方法,还是根据现象去分析其运行的本质,在梳理本质的时候去复盘案发时的行为。对于问题的定位,我觉得一个有用的技巧就是选择恰当的问题样本,然后拿到这个样本之后在自己的环境中进行测试分析验证——好像那些科研狂人再采集到样本后也是这么做的。根据样本,通过单元测试来debug代码运行的过程,查看运行过程中的流程走向,查看运行过程中的值的变化。
问题查完并解决了,那么就到了反思的时候了,为什么这问题在单元测试的时候没测出来呢?又回到那个问题,很多时候我们没写那么多单元测试,没做那么多的数据校验,然后就提交了,然后就部署测试了。这样简单地就提交了,没有代码review,确实不太好,于是我百度了一下,看到一篇文章中说笔者认为review不应该承担发现问题、纠正风格的职责,Code Review主要是审核代码的质量,如可读性,可维护性,以及程序的逻辑和对需求和设计的实现。是的,很多时候我们出现bug就是因为我们的实现与需求描述不相符,所以从某种意义上说review检查代码实现与需求逻辑是否一致就是在协助发现检查代码中的bug。
但是工期紧一直都是我们没时间review代码的因素,或者说借口。我们每个人都会收到自己要负责开发的模块,于是我们就会去专注对应部分的需求并简单地了解相关联模块的需求的业务逻辑,如果要我们去review别人的代码,就意味着我们需要再去关注别的需求去理解别的业务逻辑。如果我们要review别人代码前需要自己去研读需求需要自己再去将需求分析成代码的设计思路就很吃力了,因为大家自己都有自己的未完成项目,自己都有一堆问题要考虑,哪有时间去帮别人去考虑问题。所以我认为这种code review活动应该由一个没有繁重开发任务的人员来牵头,然后在各开发人员的代码方向较为明确稳定之后再来不定期举行,这个时候也基本上到了项目的中后期。
review的时候有很多事情需要做,发现bug只是顺带的事情,我个人会更倾向于在review的时候进行语义的优化,这样代码的可读性就会好上很多,语义优化之后就能更容易地进行更高一层的业务逻辑抽象,这或许就是review代码真正的价值所在了吧。所以,我认为最好主动去请别人review代码,然后最好能主动介绍业务需求,再主动介绍自己的设计思路以及如何产生这样的设计思路的,接下来就可以闭嘴请对方静静地看代码了。