丁哥对如何做好软件开发工作的思考(共识,复杂度,审查,重构,测试)

网友投稿 468 2022-05-30

1      共识

其实我感觉共识,并不是很复杂的东西。比如说一条船沉没了,船上的所有的人都无法幸免。对于我们做项目也是一样的。不是争谁高谁低。还是想着一起把这个事情做好。

那么这个地方就有一个问题,怎么才算可以把事情做好?拿主意的那个人必须是个通才, 至少在面对当前这个问题的时候,能够做出非常全面的把握。然后把想法儿说出来以后大家感觉可行,大家再去执行。

1.1    举个例子:时间宝贵,不容浪费

人上100,形形色色。

打个比方说吧,你请一帮人来家里做客。大家都玩的兴高采烈,感觉客厅也非常宽敞,非常明亮,非常干净,外面的景色也很舒服。但偏偏有人喜欢跑到厕所里去看,然后拿厕所的状况跟客厅里的状况进行比较,说你的厕所没有客厅干净。

这个就没法谈了。根本就不在一个层面上,有的人喜欢去查看黑暗,有的人喜欢崇尚光明,这个是价值观的问题。

有人就是关注那些黑暗的东西,让自己不痛快,然后让别人也不痛快。碰上这种事儿,实在太闹心了,干脆,眼不见心不烦。惹不起,躲得起。

毕竟时间宝贵,有那么多有用的事情要做呢。

1.2    每一个软件产品都必须要有一个灵魂

我觉得每一个软件产品都必须要有一个灵魂。这个灵魂很多时候就来自于某个人。如果在软件产品中缺少了这个灵魂,这个产品的高质量性就会大打折扣。

一个产品的理想开发状况是这个人制定方向传达指令,让其他的人去实施去完成,如果其他人不懂他的意思,那么这个人就得亲自上阵,把这个任务做出来,作为一个示范。

随后大家可能会恍然大悟,原来是这么做。那一步步的去做自己螺丝钉的任务就可以了。这样对每个人来说都是非常舒服的过程。

这正如一辆火车,如果牵引装置向四五个方向发力,那这辆火车基本上就走不动了。所以只能有一个行进方向。

这个思想的灵魂可以是一个大的产品,也可以是这个大产品的一个功能。

根据不同的任务呢,可能有不同的灵魂把握者,一旦有个人能把握这个灵魂方向,我们团队中的所有的人都向他看齐,他要什么帮助我们就提供,根本就不用操心其他的事情,让这个人去把控就好了,这样子事情就比较好办。

如果做一件事情的时候,一直处在七嘴八舌的状态,没有一个统一的方向,任何事情都很难做成。

2      软件开发复杂度

软件开发的难度跟硬件开发最大的区别就是软件开发的不可预知性,有时候碰上很多问题,我们无法用以前的经验来框制。

2.1    代码相关的复杂度的控制

这个时候没有别的办法只能披荆斩棘先从开始到结尾画一条线,一直找到一个可行的方案。

一条线代表一个功能需求的解决。接下来再解决其他的功能需求,那么这样整个项目就会有很多很多条近乎平行的功能需求线,这样复杂度就是线性的增长。

这是我个人能够看得到的,在架构代码和模块代码方面最理想的情况。

丁哥对如何做好软件开发工作的思考(共识,复杂度,审查,重构,测试)

2.2    系统部署相关的复杂度的控制

其次就是系统部署和运行方面。越简单越好。简单到一步。一步完成部署,比如点击一个按钮,运行一条命令。开发调试部分也是越简单越好。每个小模块儿是一步,多个模块儿集合在一起的系统调试部署也是一步。

2.3    新需求相关的复杂度的控制

我以前就碰上过这样的问题。那是一个在线教育平台,里面的功能有用户管理(学生老师校长代理商),班级管理,课程管理,视频直播聊天室,考试系统,试卷评估,课程购买等等。后来老板要求加的一个功能是实时上课提醒,上课前一小时提醒,上课前24小时提醒,提醒方式有短信和邮件。

前面这套系统已经跑了一段时间了,后来加的这个功能跟前面的系统有关联,但是关联度不大。主要是后端的工作,而前边应用的后端主要是提供API的功能。这个提醒的功能,主要是后端的服务轮询功能。如果在以前系统的后端上增加功能的话,势必会增加系统的复杂度和不稳定性。所以我采用的方案是单写一套系统,包含前端和后端,共享前套系统的数据库。

这样在不影响前套系统正常运行的情况下,也解决了新需求。 我们做软件系统就是这样,做的东西越多,经验就越丰富,就越能够对整个系统做出更全面的把握。

对,正如大家所想的,这就是微服务架构的一个具体实现。

3     代码审查

对于代码的修改,我们主要关注如下几点:

3.1    代码的复杂度是不是降到了最低?

先说一下代码设计的复杂度。

代码设计的复杂度应该是收敛的,而不应该是发散的。也就是说在起始点复杂度为10的话,那到终止点的时候复杂度不能超过10。

如果终止点的复杂度超过了起始点的复杂度,committer 需要对代码的整个设计结构进行审视。

再说一下代码行的复杂度。

有人为了减少修改代码的行数,在一行代码中写上多个函数调用。比如通过在函数的参数中传入另外的函数。这种编写代码的方式会提高单行代码的复杂度,我们应该把参数中的函数调用提到前面去,先把参数算出来,然后把计算出来的参数值再传入到函数调用中。

这是大道至简,好的代码要返璞归真,每个人都能看懂。

3.2    如果有重复性的代码,这种代码的重复是不是必要的?

比方说,减少重复性的代码会增加复杂度的话,我们一定要避免增加复杂度而允许这些代码的重复。

这是两害相权取其轻。

3.3    验证是必要的。

一定要创造条件跑一跑所审查的代码。比较简易的方式就是写好充足的单元测试和集成测试。

这是以客观结果为前提,不要盲目相信自己的主观判断。

4      软件编译与调试

4.1    增量编译缺乏

现在编译这一块,大部分程序员最大的问题就是无法做增量编译。这个主要是因为资源的问题,虚拟机, docker都不够用的。每次流水线触发以后,都会把以前已经编译过的内容全部删除。这样每次编译都是在做全量编译,就是你改一行代码也是这样。

跟很多人聊过,说这个问题是一个长期的问题。

4.2    调试难度大

还有的教练说,现在很多程序员写完代码以后,直接在实际的产品环境下进行调试。这个也是比较恐怖的,开发效率肯定高不起来。开发环境应该特别舒适,调试修改代码,应该非常快。

这一步开始的时候没有做,现在项目规模大起来以后呢,再做就显得有点困难。

从各个教练反映的情况看,很多项目都是这样的情况。

这也就涉及到一个问题,如何很好的做重构,重构绝不仅仅是代码层级的。

5      软件重构

关于老产品整改的问题。如果只是缝缝补补的话,可能起不到化繁为简的目的。其实做类似这种工作的话,有一个比较可行的方案。就是把现有的产品当做一个成型系统也就是现有运行的产品,不要做大的改动,顶多就是修改bug。

然后以这些成型的系统为基准,去写新的系统。相当于参照一个大的白盒就写一个小的白盒,这样新的小的白盒质量上肯定比大的白盒性能上要有优势。因为相当于开卷考试。

这样子按部就班去做的话,就会比较靠谱。

有朋友会说上面的做法是重写,字面意义上没错的。

5.1    重构还是重写?

实际上不矛盾。区别就是重构的方式应该从下往上还是从上往下。比如说我们现在大部分的重构都理解为从下往上来做。也就是感觉这个文件里头有坏代码的味道,然后就改这个文件,这样做是没有问题的。

比如现在有些教练遇到的问题,就是发现上下文不是很清晰,这个代码为什么要这么写?为什么一个文件有1万行或者3万行,这个来龙去脉不是很清楚。

这个时候可能就需要从整个子模块来进行一个自上而下的分析。梳理出这个子模块的功能需求是怎样的,需要有多少个公共接口?内部公共接口的实现方式是不是应该像目前这样的?

一个文件能够写成1万行或者3万行,肯定是有一定历史原因的,绝大程度是由于全局把握的编程能力不够造成的。

像这种情况,如果从这个文件本身去做重构的话,难度非常之大,但是如果从上往下,从模块的整个设计角度来做重构的话,可能就容易一些。

5.2    具体的参考步骤

这个时候需要做的具体步骤可以参考如下:

1.根据功能需求定义公共接口。

2.根据公共接口写出测试案例代码。

3.这个时候可以按照测试驱动开发的理念去填充代码。

4.代码可以从现有的代码中抽取出来。

5.在抽取的过程中进行整理重构。

这样,这个子模块完成以后,就可以尝试去替代现有的子模块,看看能不能在整个系统中安全的运行。

这个是我理解的,从子模块儿角度来看待一个重构过程。

5.3    依此类推

对于整个系统来说,我们又可以分成很多个子模块。然后又可以对各个子模块各个击破,最终完成对整个系统的重构。

如果一开始对整个系统进行重构的话,也是可以从自上而下的角度来看的。

比如说开始的时候先把所有的子模块看成一些占位符,假定他们已经完成他们的接口了。那对于整个系统来说,它本身就是一个子模块,属于提纲挈领的那个模块。

这个过程,从字面意义上可以理解成重写,实际上,它也是一个重构的过程,因为我们肯定会重用这个系统本身的一些现有代码和现有的逻辑。

6      软件测试

软件测试部分,我说说我对单元测试,集成测试,测试案例选取以及工作量问题的看法。

6.1    单元测试

说一下单元测试部分的思考。

关于测试案例的编写,主要是有两个方面,一个是代码物理行的覆盖率,一个是代码逻辑的覆盖率。

物理行的覆盖率相对来说比较简单,有工具可以直接报出来。经常说的,100%的覆盖率,85%以上的覆盖率,都是物理行的覆盖率。

代码逻辑的覆盖率就相对隐蔽一些。

打个比方说一个函数里面有两个条件判断分支。每个分支有两种条件: if-else。

要做到物理行的100%覆盖,我们可以这样做到: 写两个测试案例。一个是if同时成立,另一个是两个else同时成立。

要做到代码逻辑的100%覆盖,我们需要创建4个测试案例。包括上面的两个测试案例以外,还要加上另外两个if else交叉的情况。

这也就解释了为什么100%的物理行覆盖率,无法杜绝bug的产生。

因为没有做到代码逻辑的100%覆盖。

从上面的例子可以看出,如果做到代码逻辑的100%覆盖,工作量比较大。但是如果没有集成测试做配合的话,这一部分是必须要做的。

6.2    集成测试

再说一下集成测试的思考。

针对上面的代码逻辑覆盖率的问题,一个折中的方案就是使用集成测试来结合物理行的代码覆盖率。

集成测试主要是用户的需求功能单元。也就是通过某个测试案例,可以集中地反映在指定情况下用户是如何获得对应功能的。这一部分是模拟真实的用户使用场景。

6.3    测试案例选取

关于测试案例选取的思考。

测试案例的选取主要有如下方面,一是边界值的指定,用于测试一些不常用,但有可能会出现的用户输入,二是常规值的制定,这一部分主要是演示用户在大多数情况下所提供输入的处理情况。

6.4    测试代码工作量

关于测试代码工作量问题的思考。

一般情况下我们都承认没有测试的代码是不完整的代码。因此测试代码无论如何都是要写的。

那么如何写测试代码才能最省时省力呢?

我的体会是在模块或者项目编写的时候,同时配置好测试框架。先针对一些成型的代码进行测试案例编写作为示范。如果代码还没有成型就写测试案例的话,会带来非常不必要的测试代码修改工作量。所以这里有一个时机把握的问题。

除了最开始的测试框架代码部分,一般来说大面积的测试案例代码的添加是在项目已经成型的时候才开始的。案例代码除非特别情况,不要有任何条件判断。基本上都是平铺直叙的一条直线的逻辑。测试案例代码,由于其特殊性,可能存在很多重复的现象,这个都不是问题。

软件开发 架构设计 单元测试

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:机器学习工程师第一年的 12 点体会
下一篇:开发者何时最需要重构?
相关文章