周一上午,某项目的项目经理在公司级项目周会上抱怨:各个模块都说是验证过,但集成起来以后就是不能运行,一堆人封闭起来搞了几周了,问题还是一堆。公司老板焦急地看着会议室里的研发和测试主管们,而主管们也都低头不吭声,实际上他们也不知道从何说起。老板再也安耐不住了:以后我会对开发和测试交付物进行不定期抽查!我倒是要看看作为一个高科技公司究竟是怎么搞软件开发的!
老板先来到A组。A组负责多路摄像头图像融合算法开发。老板当着组长的面亲自打开了一个源文件(C++)。这个文件中的所有函数都没有注释头,语句中夹杂着若干行看不懂啥意思的注释,格式还五花八门,有用/**/的,也有用//的。老板质问:这个模块现在就他一个人会搞,如果有新人接手,这代码看得懂吗,代码在提交前有经过集体检视吗?组长红着脸打开了一份代码检视记录表。表格上杂乱得记录了十来条检视意见,每条检视意见是否接受,是否关闭了,体现在哪个代码分支上,大多是记录不全的。老板又问:单元测试代码打开给我看看?组长只拿出了一份单元测试工具的运行结果:我们组刚引入单元测试不久,时间又紧张,所以单元测试这块没怎么做。然后,老板一脸愤怒,组长汗流浃背。
老板又来到B组。B组负责把多个同事开发的计算节点集成到客户指定的设备中。老板当着研发总监的面问一位同事:把这3个节点集成起来,要花几天?这位同事支吾了一下:一周。研发总监脸红了:为啥要一周,我估计也就是2天就能搞定!这位同事把这3个节点的代码打开:虽然这3个节点都是用C++写的,但是由3位同事分别开发的,每个的情况还不一样。其中一个节点写得很有C语言风格,我估计这是一位C语言出身的“高手”,我不得不打回去让他重构了以后再给过来;其中一个节点秀了一个新的设计模式,我花了一天时间才看懂,可能是我水平有限;另一个倒是常规的C++风格和用法,但是放到设备上跑不起来,我也不知道他做过哪些验证。然后,老板一脸愤怒,总监汗流浃背。
最后,老板来到测试组。测试组负责软件全功能集成测试。老板问组长:前段时间客户现场出现的高负载下数据转发失败问题,你为什么没测出来?组长回答说:我们设计了相关用例,只是负载没那么高,所以没触发这个问题。老板追问:那你为啥不把负载设计得高一些?组长为难道:我一开始是那样设计的,但是开发人员认为这种场景非常罕见,所以不认同这个测试用例,我就没再运行它。然后,老板一脸愤怒,组长汗流浃背。
为什么感觉很烂?在回答这个问题之前,有必要先强调几个认知。当然这几个认知看上去都是老生常谈:
1、软件开发是一项团队活动,自己写的代码不是给自己看的。
“团队”这个词,大家再熟悉不过。从大学毕业写简历的时候开始,就不停地强调自己有“团队精神”,具备良好的“团队合作能力”。但是各位是否注意到,在实际工作中,真正让人感到“别扭的”、“拧巴的”和“无语的”的,恰恰是发生在团队合作的过程中。
对于软件开发活动来说,编码的人好比是“厨师”,代码好比是“厨师”手中的一道“菜”。这道“菜”在做出来之后,通常要经过代码检视、单元测试、集成测试等不同角色的“客官”去“品味”。这道“菜”色香味如何,不是这位“厨师”说了算,还要看各位“客官”的评判。当然,在“老板”和“客户”眼里,这些所有的“角色”也都变成了“厨师”,如果这两位“客官”不喜欢这道“菜”,那就Game Over了。所以,我们着重指出“自己写的代码不是给自己看的”,目的就是要在思想上面真正把“团队利益”放在第一位,这样才能在开发过程中严格遵守编码规范、编码风格、测试规范等准则,也才有了保证软件质量的基础。
2、软件缺陷向后累积,会显著抬升质量管理成本,是导致代码质量失控的主要原因之一。
软件缺陷向后累积会显著抬升质量管理成本,相信大家都能理解:一个缺陷,如果在代码检视的时候一眼就看出,就只要编码人员改一下就行了,分分钟的事情;如果在客户验收阶段才暴露出来,那可就不是分分钟的事情了,搞不好老板都要跑去做解释。
但是,承认缺陷向后累积是导致代码质量失控的一个主要原因,却不是一件容易事情。因为在软件开发是一个环环相扣的过程,从需求分析开始,一直到各项测试完成,其中任何一个环节没做到位,都可能把缺陷遗漏到下一环节,等到缺陷在后序环节上累积的时候,其实就已经失控了。所以,对待缺陷就像对待“癌症”一样,必须“早发现早治疗”,对应的代码检视和单元测试等早期过程,是需要格外重视的。
3、没有完美的编程语言,适合自身的才是最好的。
编程语言是“双刃剑”。以C和C++为例,C++作为一种复杂的,具备面向对象特性的编程语言,其优点自不必多说,但这些高级特性同时也难于掌握,问题定位也相对困难。对于“高手”来说,这都不是事儿,但不能指望团队里面每个人都是“高手”。“人人都要成为高手”的口号要喊,但是做的时候必须全面评估自身状况,才能做出最有利的选择。
我们来看一个例子。同样是基于具备较强数据处理能力的高端处理器(类似于ARM Cortex-A):某著名通信设备公司,用C语言编写了全套无线通信协议处理软件,不仅运行稳定,话务量指标也领先于友商;某智能驾驶创业“独角兽”企业,用C++编写了自动泊车处理软件,演示效果不错,也拉来了很多投资。
那家著名通信设备公司的开发团队为什么没有选用C++?原因有两个:一是开发团队大多是使用C语言的老手,普遍对C语言掌握更深入,在性能优化上也有很多现成的实践成果可用;二是在无线通信设备领域,话务量指标是非常关键的竞争力,市场端对话务量指标提出了很高要求(高到有点不切实际),因此软件必须减少任何不必要的性能开销,要榨干硬件的“每一滴血”,综合来看,C语言更合适。
3、没有完美的开发模型,适合自身的才是最好的。
无论是传统的瀑布模型,还是被人们津津乐道的敏捷模型,还是在特定行业中规定的开发模型(如汽车电子领域中常见的V模型),都有其自身特点,盲目照搬某个开发模型是不可取的。
我们来看一个例子。某汽车行业软件创业公司,为改进开发质量,决定基于ASPICE过程去优化内部开发流程。ASPICE对于软件来说,定义了6个过程组,覆盖从软件需求分析(SWE.1)到软件合格性测试(SWE.6)的全过程,是一个“重量级”过程。如果再考虑外围的支持性过程组,如系统过程组(SYS.1到SYS.5),那就更加复杂。最后发现,这个过程看上去确实非常严谨,但光是撰写各个阶段的标准化文档和开展相关的评审工作就要占用巨大的工作量。如果照搬这个过程,对于以“短平快”见长的小型开发团队来说,是很难接受的。
代码检视是软件质量保证的第一道实质性防线,多数的低级错误(野指针、浅显的内存泄漏等)都可以通过代码检视过滤掉。这个过程相信所有开发团队都会去做,但是这里要额外强调的是“严格”二字。在一些大型软件企业里面,对代码检视的要求是非常严格的,而这里的“严格”往往体现在细节上,例如:
1)代码注释要求必须达到一定的比例,且格式必须工整规范,连/和/之前的空格数量以及换行方式都有相关规定。
2)代码中不允许出现“TAB键”,所有的“TAB键”必须替换为4个空格。
3)指针在使用完成后一律强制赋值为NULL。
4)所有涉及内存分配和释放的地方,都必须增加注释,明确写明不会出现内存泄漏的原因以及经过了何种针对性检视。
5)所有存在循环的地方(for循环、链表遍历等),都必须增加注释,明确写明不会出现死循环的原因,或者采用了何种技术手段确保即便出现了死循环也能检测出来并且恢复。
上面这些措施看起来都是非常基础的检查点,而就是这些最为基础的检查点,往往很多开发团队都不能严格检查。在规则面前,一旦开一个小口子,团队成员在意识上松懈了,那么很快就会蔓延开来,最终演化成一个堵不上的大口子。
【改进建议】
1)为代码检视建立严格的Check-List。Check-List应将代码格式、注释等编码风格相关的检查点也考虑进来。
2)建立严格的代码检视和检视问题闭环机制,对检视出问题较多的人员可考虑精神或者物质奖励,提高团队重视程度,激发团队积极检视的热情。
如果说代码检视是一种“静态检查”,那么单元测试就是代码开始“动态运行”的起点。很多开发人员往往不愿意写单元测试,因为这会占用一定的时间,不如把精力投入到写代码上来的实惠。尤其在时间紧迫的时候,单元测试往往成为被放弃或者草草了事的一个环节。而实际上,单元测试在很多方面都有它独特的优势,例如:
1)在嵌入式开发领域中,使用基于PC的单元测试环境(例如gtest开源测试框架),可以把业务代码和单元测试用例融合在一起,从而在PC端构建起一道“防护网”,而不必依赖于特定的硬件。如果谁改出了低级错误,单元测试用例会第一时间跑挂,不会等到烧录到特定硬件上才看出问题。在PC上查找问题毕竟比在特定硬件上要更为高效。
2)可以灵活构建出各种超常规场景,例如超出一般业务场景数倍的数据转发,实际业务场景中极少出现的高频率内存分配/释放等。这对于提前发现深层次的数据转发失效、内存泄漏等问题是非常有意义的。甚至在依赖于某些特殊设备的场景下,往往只能通过单元测试去复现和定位这类问题。
所以,单元测试必须引起重视并严格执行,即便是时间紧急也不能例外。
【改进建议】
1)对单元测试用例建立Check-List。在Check-List中,要求必须覆盖边界取值、数组/链表遍历、内存分配/释放、超常规业务模拟等特殊场景。
2)组织专项评审,确保单元测试用例的构建质量,同时也让开发人员在思想上引起重视。
3)通过一定时间积累形成稳定的单元测试用例库,并持续维护。
一些大型软件企业有自己专用的编码规范,在网上都能找到,如Google公司的编码规范、华为的编码规范。也有一些公司引用特定的行业化规范,如在安全关键领域中常见的“MISRA C/C++”。但是这些编码规范篇幅较大,且覆盖到编程语言的方方面面,可能到离职也没通读过。虽然有商用工具可以对代码进行静态检查,但并不是所有开发团队都具备了这类工具。因此,建立可操作性强的编码规范是很有意义的。
【改进建议】
1)对编码规范进行分级管理。第一级是“军规”,只针对绝对不能违反的、最重要的功能规范,条目要精简,比如可精简到不多于15条。如:除数不能为0、指针在使用前必须判空等。第二级是“重要规范”,即抽取出比较重要的规范,并可进一步细分为功能规范和性能规范(如函数入参数量尽量少于多少个,函数入参尽量避免直接传结构体等等)。这里之所以强调制定性能规范,是因为一个软件性能的好坏除了取决于核心算法和数据结构的设计之外,在编码过程中是否落实了必要的性能提升点也是非常有意义的。第三级是“编码规范全集”。
2)将“军规”写入代码检视Check-List。
3)对违反“军规”的人员或者开发团队可以施加一定的惩罚措施,如记“黑星”,请全体吃冰激凌等等。
测试的类型可以细分为很多,但是不管是如何分类,从整体上来说,测试是软件缺陷的“最后一道防线”,也要给出最终结论,即软件是否能够满足质量要求和客户需求。是否能通过测试将缺陷充分暴露出来,一方面取决于测试人员对需求和设计的理解程度,这会最终反映到测试用例上,这是测试技术的问题;更重要的是取决于测试执行的力度,即测试人员是否站在软件质量最终“把关者”的位置上,坚持自己的原则,这是态度的问题。不管是技术还是态度,都要有相应的机制去支撑。
【改进建议】
1)强调“任何缺陷都是可以通过测试暴露出来的”。堵死“这种缺陷根本没法测试出来”的思想退路。强迫测试人员提升自身能力,不断完善测试方法和测试工具。
2)凡是遗漏到客户的问题,都必须回溯,并且强调“测试漏测是导致缺陷遗漏的唯一原因”。由对应的测试人员组织回溯,并给出漏测原因和改进计划。将遗漏到客户的问题数量作为测试人员的主要考评依据,影响年终奖。
3)给测试人员授予一定的“权力”,强化软件最终质量“把关者”的地位,把测试人员的压力透传到开发团队。例如:问题单能否关闭由测试人员说了算,测试人员说不能关闭就一定不能关闭,所反馈的问题研发就必须解决;软件版本能否释放出去,由测试人员说了算,不管时间怎么紧张,只要测试人员说不能释放,研发就必须想办法解决测试人员提出的问题;将测试人员提出的有效问题数量作为软件开发团队的主要考评依据,影响年终奖。
4)对提出有效问题多的,问题严重度高的测试人员及时进行正向激励。
5)注意缓和测试团队和开发团队之间的摩擦,避免两方形成对立之势。
既然是“矩阵”,那么从职能划分上来说,可以分成“纵向”和“横向”两个相互垂直而又有所交叉的管理方向。所谓“纵向”,一般是各类资源组,例如某产品开发组、测试组、采购组、财务组、产品管理组等等;“横向”,一般就是项目管理(项目经理)。绝大多数软件开发企业,不论体量大小,基本上都存在矩阵式的组织管理形式。但是其中有些“矩阵”运行比较顺畅,也有些“矩阵”运行起来很“拧巴”,总达不到老板的预期。当然这里面原因也比较复杂,涉及到公司具体的组织结构、企业文化、人员激励方式、高层管理人员的管理思路等诸多方面。这里仅从软件开发的角度来给出一些局部思考和建议。
首先回答为什么“拧巴”。这一般体现在纵横两条线的配合上,具体来说,主要体现在项目经理能否顺畅调动各个资源组的人力资源,并把项目任务充分落实到资源组中。这里可能存在三方面的问题:
1、项目经理能力有限,或者干脆就是“空降兵”,“纵向”资源组认为其能力不足以了解软件开发的各个业务逻辑,进而表现出各种消极的配合态度:开会不积极,光喊人就喊半天;分配了工作任务,不催不做,反正老板又不直接找我等等。
2、“纵向”资源组的积极性没有被调动起来,工作思路趋利避害,藏在“横向”项目管理的后面,被拖着走。这样一来,项目管理工作的难度就会增大,即便是这方面的老手,也不得不花费更多精力去做各类细致的协调工作。
3、为解决以上两个问题,有公司试图建立起所谓的“强矩阵”,但是对什么才是真正的“强矩阵”并没有把握透彻。更多的是过度加强对其中一条管理线路的责任和授权(比如把项目管理推到最前面,同时给以强大的授权),但忽略了另外一条管理线路的责任和权力。这样会造成另一条管理线路的工作被过多干预,对下属成员的管理权力也被分散,想做的事情做不了,更不利于软件开发质量的提升。
【改进建议】
1)从各个资源组中挑选资历深的,能力强的人去做项目管理,并将项目管理作为向上晋升的必要实践环节。这样可以解决“横向”面对“纵向”时不能服众的问题。同时,即便其他资源组的人调配不动,最起码还有自己人能调得动,工作开展的难度也会相应降低。
2)纵横两条线的责任要平衡。可以把其中之一给突出出来,但是同时要强调另外一方的责任,避免形成“一方往前冲,一方被拖着走”的被动局面。
3)对人员考评权力的分配要合理。不建议在“纵向”和“横向”之间搞平均主义,因为这会产生员工多头汇报问题,会极大影响员工的积极性。建议的实践是:人员考评以“纵向”的资源线主管为主,处于“横向”的项目管理人员有“一票否决权”。例如,资源线主管认为员工A绩效很好,而如果项目管理人员能够举出实例,证明员工A在某项目中表现不好,或者有较大失误,则可以将资源线主管的绩效“一票否决”。这样一来,员工一方面会将日常工作汇报聚焦于直属的资源线主管,另一方面项目管理人员也有相应的权力,是一个相对比较平衡的做法。
4)处于“横向”的项目管理没有直接下属,但同时所有人又都是他/她的直接下属。可实际上,绝大多数搞项目管理的人只能感受到前半句话的难处,而远远达不到后半句话的高度。所以,在团队发展到一定规模时,必须着手建立一些清晰而必要的流程,这会大大降低项目管理人员的工作难度。
【Q】代码质量差不只你说的这些因素,需求分析和架构设计有时候也难辞其咎。
【A】是的。软件开发是个系统工程,从需求分析到架构设计,再到编码和测试,哪个环节出了问题都不行。但是本文主要针对编码和测试,其他的方面有空再写。
【Q】我来自一个大型外企,感觉上面说的东西都很夸张,很搞笑,或者从来就没遇到过。
【A】其实这也很好理解,因为大型企业早就渡过了“原始积累”阶段,组织架构和企业文化都已经固化下来,流程和制度也很健全,基本上员工往里面一坐就顺其自然地开展工作了。而对成长型团队来说,往往是另外一番光景。因为他们可能还处在爆发式发展的前夜。在这个阶段中,一切情况和做法都是有可能出现的。实际上各位姥爷们也能看出来,这篇文章主要是针对成长型团队的。
【Q】这些看上去很“肤浅”的改进建议是你自己瞎编的吗?
【A】我可以肯定得告诉您:不是。这些改进建议都是源自于所经历企业中的实践。其中有些看起来好像还挺夸张,但它们还恰恰是当前如日中天的某大型企业所使用过或者正在使用的。
【Q】这些东西谁都懂,说起来容易,做起来哪有那么简单!
【A】当然是这样,不然的话遍天下的公司都成“华为”了。但是,如果一味纠结于这里很复杂,那里也很难,那就永远也无法获得改进。好比大家在一起上课,用的是同样的课本,老师也是同一位,可为什么最后成绩差那么多?这个我想根本不用回答。成功的企业除了有时代潮流助推以外,无非也就是把人人都懂的那些道理或者事情给做下去,并且做到位了,这也正好就是我们天天喊的“执行力”。
【Q】还有啥建议没?
【A】 有:做!