影响工业软件开发周期稳定性的根因分析
2021-04-28李红波柏立悦
陈 晖 李红波 柏立悦
(浙江中控技术股份有限公司)
软件开发周期逐渐成为相关企业考量的关键因素,这也是快速迭代的开发模式在如今的应用软件开发中越来越流行的原因之一。 对于一个工业领域的公司来讲,软件质量是重中之中。 那么在工业信息化程度日渐提高的今天,保证软件质量与实现快速交付成了工业软件公司的主要矛盾。 因此,笔者介绍了在遇到工业应用软件开发效率瓶颈时, 一种分析和寻找根本原因的思路。工控行业一直以来的主流技术栈为C/C++,特别是应用层的软件主要集中在C++,因此笔者针对C++软件开发中遇到的相关问题,分析造成这些问题的根本原因。
1 把控项目进度的关键因素
对软件项目经理来说,最重要的是把握软件的发布节点。 关键点在于要对总任务量和总生产力两个方面进行准确评估,以确保两者相契合。
1.1 总任务量的评估
总任务量即完成软件项目需要的全部客观有效工作量,计算方式可简化如下:
如果想要进行更细致的评估,则需要采取更复杂的方式,如类比建模法[1]、阶段评估法[2]等。这些方法基于以往类似项目的经验进行推导,往往能较准确地对项目的总任务量进行评估。
1.2 总生产力的评估
总生产力指的是一个团队在一定时期内实际可以提供的开发成果产出:
由此可见,想要精确地掌握时间节点,不仅要准确评估总任务量,还要准确地评估出团队的总生产力,以确保其能覆盖项目的总任务量。 而想要准确评估总生产力,就必须准确地了解每个开发人员在特定领域的能力。 一旦某个人的实际生产效率与预估的发生较大的偏差,那么关键路径就会大幅变长。 而如果在项目后期才发现偏差,那么即使投入人海战术也不能对加快进度有明显的改善,甚至会产生负面作用——因为原先的开发人员需要对新加入的开发人员进行培训,并且更多的人意味着更高的沟通成本[3]。
因此,准确地评估每个开发人员可以做出的贡献,消除项目中的不稳定因素,是保障软件项目更快、更及时完成的关键点。
2 开发贡献率的评估
2.1 开发贡献率的差异化
一个优秀的编程者的标志在于能够快速写出精简而又清晰易懂、符合客户需求、具有良好扩展性的代码。 这样的编程者们,是一个项目组的核心骨干,往往可以承担一个软件项目中大量核心代码的编写工作。 Sackman H 等早期的研究指出,水平较差的编程者的编码时间最多可达优秀编程者的25 倍,调试时间则最多为28 倍[4]。
而笔者也在已开发完成的软件项目中做过统计,约20%的核心技术骨干可以完成约50%的功能开发, 而考虑到这些代码重要程度的权重,其贡献率最高可以达到80%。 由此可见,每个软件开发人员所能做出贡献的差异很大,所以找到合适的评估方法就显得尤为重要。
2.2 评估方法及其缺陷
开发贡献率的评估方法有很多,但大多是基于一个开发人员的已有开发成果进行推断,例如传统的方法有考察日均代码行数、千行代码缺陷率、代码圈复杂度,而近年来则有人尝试更复杂的建模方法,来达到更准确的评估目的。 笔者将这些方法及其缺陷整理如下:
a. 日均代码行数。 即考察开发人员每天产出的代码数量。 该方法完全忽略了代码的质量。
b. 千行代码缺陷率。 即通过测试结果来考察开发人员所写代码在逻辑上的质量。 但该方法忽略了开发过程的差异性——如果相同的功能,人员A 比人员B 抽象提炼能力更强,在更好地考虑了复用性、扩展性、简洁性后,产出的代码比人员B 产出的代码数量要少,在缺陷数同样的情况下,人员A 的千行代码缺陷率比人员B 要高,但显然人员A 产生的效益比人员B 要高。
c. 圈复杂度。 即考察一个代码模块的复杂度。 在没有对照的情况下,该方法也不能很好地反映出开发人员的贡献率,因为它反映的是一个模块最后实际产出的代码呈现出的复杂度,而不是其应有的复杂度,而一个模块本身应有的复杂度是很难衡量的,所以也就无从比较。 除非两个程序员都实现同一个模块的代码,那么这两份代码的圈复杂度能较好地体现出他们技术水平的差距(圈复杂度越低的反而越好,表明其能把复杂的事情简化)。
d. 数学建模。 以上的方法单一应用时都有相当大的局限性,因此近年来有人试图使用数学建模的方法把各种因素结合在一起,并使用人工智能的方法得出一套实际可用的评价系统。 例如Ren J L 等基于其研究成果开发的Merico 评价系统将代码的贡献值分成结构化数据和非结构化数据,并用机器学习的方法将这两种数据拟合在一起,得出最终的贡献值[5]。其中结构化数据主要由一个函数的被调用次数决定,而非结构化数据则由机器学习来判断某条代码提交的重要性。Merico 评价系统确实在一定程度上解决了编程贡献值评估难的问题,但也有其局限性。 例如该系统判断结构化贡献值的方式是统计一个函数的被调用次数,其验证手段是考察3 个开源社区贡献者的开源代码来实现的。 由此可知其局限性在于,首先,开源社区贡献者的开发水平属于中上游,不具有全面的代表性;其次,一般地,在一个大中型公司中, 软件的模块划分更为细致,基础模块(通常来说代码质量、稳定性及可阅读性等要求都相对更高)比上层的应用模块有更多的机会被调用到,但无法就此推断出负责基础模块的程序员其代码贡献率就一定比负责应用模块的大。 而基础模块和应用模块通常是不同的项目组(例如公共组件组和产品项目组)负责的,对于同一个组的成员来说,他们负责的模块层次是相对扁平化的,那么函数被调用的几率实质上是均等的。 所以实际上这是一个从结果推出条件的伪命题,因为越底层越基础的模块,通常会被多个产品线共用,其重要性不言而喻,一般会让技术功底深厚的老员工来担纲,那么他们编写的代码自然而然地具有更好的质量,而不是因为他们的接口被使用更多就认为他们的代码质量更好。
3 产生偏差的原因分析
3.1 造成偏差的不稳定因素
由上文可见,目前大部分方法都不能客观准确地评估贡献率,实际日常操作也仍是以项目经理依据对团队成员能力上的印象进行判断为主。如果不是一个参与架构设计和日常代码评审的项目经理,很难做到准确公平地评估每个组员的真实贡献。 事实上这种情况在企业中很常见,由于项目管理流程的需要,项目经理无法在具体开发工作上投入过多精力,一般会把这些任务转交给组内的技术专家或资深员工,这就造成项目经理对开发人员的真实水平很难有准确的把握。
通常在项目初期,项目经理会较均匀地分配任务,原因是出于对团队成员的信任,避免他们认为被边缘化。 而随着开发过程的推移,在最初任务分配均等的前提下,高水平开发者的进度一般领先于其他员工。 此时项目经理出于项目进度考虑,基本会转移部分可分离的任务到骨干员工身上。 以上的做法往往导致高水平的开发人员不堪重负——不但要承担架构设计,还要负责大部分功能的实现。 而低水平的开发人员只能在边缘的功能上做文章, 对于他们的技术成长收效甚微,即便公司给予培训上的支持,但缺乏实践使得知识很难得到沉淀。 如果给予他与其水平不符的任务,然后花费高水平开发者的时间来仔细评审他们的设计和代码,确实能更快速地提高其水平,从长远来看也更有意义,但也得忍受当前项目数倍的开发周期和更多的错误数量。
因此,一个有时间节点压力的项目型团队就容易陷入到一个不好的循环中来,整个团队总是疲于应付开发任务,其整体水平只能以一个很平缓的曲线上升。 这对于公司和员工来说是一个双输的局面:对公司来说,一个开发团队的成本日益上升,但其开发水平并没有得到相应程度的提高,其价值其实是日益下降的;对于员工来说,在相当长的时间内没有得到成长,那么对自身的发展和职业生涯是很不利的。
可以推断,软件项目周期的稳定性主要是依据非核心开发人员。 一个项目的关键因素在核心员工身上, 但关键路径却体现在非核心员工身上。 如前文所述,项目经理依赖于工作经验丰富的核心员工来保障项目进度,那么相反地,非核心员工在开发不同产品或不同模块时所表现出的不稳定性正是影响项目进度的最大因素。 他们更容易受开发平台、产品特点的影响,这种不稳定性就反映在他们的开发周期和开发质量上。
综上所述,非核心员工的开发效率、质量相对不稳定,导致项目组的资源配置也不能达到最优化,因此项目的进度风险就会变大。
3.2 根本原因分析
首先对两个不同水平的C++开发人员所能做出的贡献有巨大差距这个现象做出一个分析:
a. 面向对象比面向过程在设计上需要更多的努力和领悟[6]。对于大部分C++程序员来说,在学校学习的课程以C 语言居多,或者学习过C++课程,但并未领悟面向对象的思想,在刚投入开发工作中时仍把C++当成C 语言来使用。 事实上领悟面向对象很难,也需要一定的天赋,因为面向对象只是一个概念, 其背后蕴含的是抽象、提取、 总结事物内在逻辑和联系的本领与技巧,即便一个有多年C++使用经验的程序员都不敢宣称完全掌握其精髓。
b. 新手往往知识面不够丰富。对于一个核心开发人员来说,他可能兼具编程知识的广度与深度,拥有不同项目的开发经验,负责过不同领域的产品和模块,文件读写、网络通信等不同方面的知识对于他们来说已经成为常规武器。 而对于一个新手来说,处理产品的业务逻辑就已经耗费其精力,其他方面不可能以一种十分自然、毫不费力的方式把它们处理正确。
c. 新手往往自信心不足。 新手由于缺乏项目经验,即便已经储备了很丰厚的理论知识,但在实际开发过程中, 对自己写的代码的不自信,追求尽量少犯错,依然会用一种比较初级但自己更熟悉的方式去编写代码。 但在商业化项目的开发中,逻辑上的正确往往是不够的,性能、可维护性等方面都是需要考量的因素。
以上原因使新手在开发过程中会遇到很多困难和疑惑。 以下是一个案例,该段代码用于实现查询用户列表中是否有“admin”用户的功能:
运行这段代码虽然能获得正确的结果,但可从一个代码评审者的角度来讨论其不足之处:
a. bFound 变量多余,iter 和userIdList.end()比较即可实现同样的效果;
b. iter++改为++iter 更佳,避免每次产生一个临时变量和赋值动作;
c. 在if(*iter==“admin”)分 支 中 应 该 添 加break,否则找到“admin”后仍会继续执行,导致浪费计算机的性能;
d. for 循环终止条件中的userIdList.end(),应该使用临时变量进行缓存,以避免重复的函数调用削弱性能。
一个简单功能的实现,却有至少4 处改进之处,且b、d 两条是绝大多数新手没有理解和掌握的。 在一些服务器端的开发中,这些缺陷可能造成一定程度的性能问题,而针对这些缺陷进行优化后,某些情况下性能甚至可以提升20%以上[7]。
因此, 一个新手或者低水平的开发人员,与资深开发人员的差距是全方位的,如果要求他们一开始就去处理编程的各个方面, 如数据结构、网络交互及操作系统等, 那么他们便会顾此失彼,从而影响编程效率和质量,最终开发周期变长甚至成倍增长。 如果想减少这种不利的因素,那么就必须创造环境来尽量避免新手同时面对多个他不熟悉的领域,从而使其精力足以应付一定的不确定性,并稳步地成长。
4 结束语
笔者阐述了把控项目进度的关键因素为团队总生产力的评估,介绍了评估开发人员贡献率的方法及其局限性,指出开发贡献率的评估难点是软件项目发生进度偏差的一大原因,进一步指出造成偏差的深层原因是新手在面对多个不熟悉领域时所表现出的不稳定性,这些为企业优化软件开发流程、改进开发环境提供了一种思路。