DevOps强调开发运维过程的可度量与透明化。而通常情况下我们把软件质量分为内部质量和外部质量。所以我们应该对内部质量和外部质量分别进行度量,以便持续改进和优化软件质量。
软件的内部质量通常指代码和设计的质量。内部质量可以通过应用设计和编程达到最佳实践,也可以通过持续一致的开发和交付流程来提高。
通常,软件的外部质量是通过查看和使用软件(例如验收测试)来度量的。
比较常见的情况是,有的软件外部质量很好(所有功能都能正常使用),但是内部质量却很差(可能有糟糕的代码、不可维护的代码)。从长远的角度看,内部质量不佳最终会影响外部质量。应用程序会持续不断地冒出新的bug,而且开发时间会由于“技术债务”的增加而变长。
1技术债务的形成
技术债务类似于金融债务,它也会产生利息。由于初始鲁莽的设计决策,在未来的开发中我们需要付出更多努力,消耗更多的时间。我们可以选择继续支付“利息”,也可以通过重构设计来将一次付清本金。虽然一次性付清本金需要付出一定的代价,但这样可以降低未来的利息。
1、鸡和蛋的故事
有人用下面这个的故事形象地描述了技术债务的形成过程:
有一个农民,他有三只鸡,每只鸡每天生一个蛋。农民与当地的杂货店有生意来往,杂货店老板每天从农民那里买两个鸡蛋,放在店里售卖。一切都有条不紊地进行着,直到有一天杂货店老板突然出现在农民的家门口。
老板:老王,吃饭了吗。
农民:呀,刘老板,你怎么来了。你看,我刚吃完饭,正准备给你送鸡蛋呢。有什么事还麻烦你亲自跑一趟?
老板:今天我来是想要拿一些鸡肉的。
农民:肉吗?这个我们合同里没写呀。
老板:我知道,但我确实需要鸡肉啊。最近生意做得好,我就琢磨着可以搞一个企业家禽服务平台。
农民:啥?
老板:就是给一些重要人物提供新的商品。总之,我现在等着要鸡肉,你能给我一些吗?
农民:刘老板,这事没你想的那么容易啊。你看,我必须孵化鸡蛋,然后把小鸡养大。估摸着也要一个月。
老板:一个月?太久了…我现在就要。
农民:这个我也没办法,我们没法违背自然规律啊。
老板:你杀一只鸡就阔以了嘛。这样一来,我能拿到肉,你每天还是能拿到两个蛋。说起来,你每天也只要两个呀。你自己算算这笔帐。
农民:可是这样一来如果有一只鸡病了死了,那我咋办啊,我可给不了你两个鸡蛋了。
老板:(催眠)你不相信我老刘吗,不会有什么事情发生的。我需要肉,这事十万火急!你就给我吧。
农民:行吧…
说完,农民拿起屠刀将一只鸡送上了天堂。
杂货店老板开心地拿着肉回到了店里。
过几天杂货店老板又来了。
老板:老王啊,我告诉你一个好消息。上次那个鸡肉一下子就抢空了,太受欢迎了。快快快,再给我一只鸡。我们大赚一笔。
农民:可是刘老板,再给你一只鸡,我每天就没法给你两个鸡蛋了。
老板:哎呀,别管那么多。客户想要肉,已经下订单了。
农民:不行不行,这合同里写了,我每天要给你两个鸡蛋。你这不是要我违约吗。
老板:但是我特别,特别,特别需要肉!就在明天!否则客户会生气,地球会毁灭!给我一只鸡,现在!
农民:那你拿走吧!但是我要再提醒你一次,从现在开始,每天没有两个蛋了。
老板:晓得,晓得。但是你脑瓜子好使,肯定有办法。
杂货店老板满意地走了。
然而,一天后...
老板:老王,怎么每天只有一个鸡蛋?
农民:你问我?我有三只鸡,你拿走了两只,现在,这里只有一只鸡。一只鸡每天只能下一个蛋。
老板:合同规定的条款就摆在这里。你每天要给我两个鸡蛋!现在你要我怎么向客户解释?
农民:老板啊,问题出在鸡身上,这逼死我也没有办法啊。
老板:好吧,好吧。算了,说点别的吧…我想哈,不然我再拿一些肉吧。
2、技术债务的分类
Steve McConnell将技术债务分为两类:
-
无意识的 - 由于经验的缺乏导致初级开发者编写了质量低劣的代码。
-
有意识的 - 团队根据当前而非未来进行设计选型,这种方式能很快解决当前的问题,但却很拙劣。
Martin则将技术债务划分为4个象限:
(1) 不计后果,有意识的。团队没有时间做设计,匆忙给出了一个的方案,缺乏对质量的预见。
(2) 谨慎,有意识的。尽管有很多已知的缺陷,但团队必须现在交付产品。团队对此造成的后果心中有数。
(3) 不计后果,无意识的。团队压根就不知道基本的设计原则,更不用说引入的“坏味道”了。
(4) 谨慎,无意识的。拥有优秀设计师的团队很容易遇到这种情况。他们交付的方案具有商业价值,但在完成方案后才明白什么才是最好的方案。
实际情景中项目的技术债务问题是不可避免的。问题的关键是,千万不能引入不计后果的债务,因为它会持续不断地产生“坏味道”,而且也很难对付。
2技术债务与质量投资
技术债务的主要问题是它通常只代表了系统的内部质量,而质量有哪些影响并不明确。特别值得注意的是,技术债务的经济影响无法简单地表现出来。很奇怪一点是,如果这些代码不需要修改,那技术债务就完全没影响。但是,一旦要修改这些代码,技术债务就成为代码的重要属性。所以,技术债务很可能对项目的成功与否、外部质量完全没有影响。
因此业界有人提出,不要单纯强调技术债务的重要性,如果要想通过DevOps消除技术债务,让开发有效地处理掉技术债务,建议使用“质量投资”的概念。
使用质量投资,处理技术债务就有可能获得利润。使用财务概念积极地管理代码质量,可以很容易决定哪些质量问题应该立即解决,哪些可以暂时接受。
假设我们有一个系统,它包括三个模块:客户、订单和发票。客户管理是个非常老的模块,已经不再开发了。因此,这个模块不适合做质量投资。因为仅当代码被修改时才会产生修复成本。而在这个例子中,修改的可能性为0。因此支付任何修复成本都将导致损失。
然而,我们知道在接下来的迭代中,对订单流程进行了大量修改。根据经验,发票管理也必须进行一些修改。因此接下来就要重点评估订单模块和发票模块的质量问题。订单管理的测试覆盖率非常低,客户管理的代码非常复杂,也就是说方法和类有大量的代码,并且有很多复杂的循环。
我们来评估一下修复成本和非修复成本。非修复成本的评估也应考虑模块修改的可能性。在下一个迭代中,如果代码质量相同,订单管理估计要投入20天。而如果测试覆盖率能提高,那么估计只需投入13天。因此,非修复成本是7天。这非常高,因为质量确实太差了,同时也因为有大量代码要修改。
-
订单管理:很低的测试覆盖率,修复成本:5天,非修复成本:7天。
-
发票模块:很高的复杂度,修复成本:5天,非修复成本:4天。
这些评估表明订单模块的质量投资产生了2天(=7天-5天)的利润。相反,发票模块的质量投资没有利润,甚至会导致亏损。由此可见,投资当前的订单系统是有价值的。因为根据团队的评估,提高测试覆盖率就有利润。对于发票模块,情况并不明确。就现在来说,其质量投资没有利润。然而,发票模块很可能在未来的几个迭代中也需要修改。这样你就能从发票模块的质量投资中得到利润。
根据给出的评估和计算出的利润,也可以为每个质量投资推算出投资回报率(ROI)。投资回报率表示相应的成本产生了多少利润。因此,投资回报率等于利润除以修复成本。订单模块质量投资的投资回报率大约是40%(=2天/5天)。我们通常会寻找机会去获取比较高的投资回报率,使用这些财务术语,我们可以看到代码质量提高后的收益。当然,这些数字都是估计值。然而,在这种做法下,团队不只是从基于自己的原因寻求质量的提高,更重要的是由于经济原因。我们也可以在迭代计划中更有效地对技术债务消除的工作任务项进行选择和排序。
3技术债务处理方法
虽然质量投资的表达方式有利于开发团队出于经济原因考虑技术债务的消减。但是,我们在实际操作过程中发现,尤其是在传统IT领域,技术债务消减仍然显得动力不足。
在某电信运营商的DevOps平台规划建设实践过程中,我们提出“基于持续集成,内部质量透明化 + 外部质量验收驱动”的方法,对技术债务进行消减处理。
1、外部质量验收驱动
技术债务的形成往往是由于赶进度忽略了非功能质量特性而导致的,由于内部质量的不佳(设计或代码质量不高)导致外部质量的低下。传统IT领域通常有上线前的验收测试,如果能够在验收测试过程中重点关注非功能需求的实现质量,则可以“由外而内”地驱动开发团队在开发过程中重视和改善软件系统的内部质量。
按照某电信运营商的《业务支撑网非功能需求管理办法》,把非功能需求体系划分为:
· 性能
· 可靠性
· 可维护性
· 可监控性
· 安全性
并且制定了相应的验收标准。早期由于缺乏细化的入网验收执行规范和相应的资源投入,管理办法和验收标准成了一纸空文。目前,我们逐渐加大了资源投入,组建了相应的入网验收测试团队,制定了性能基线管理、安全基线管理等持续长效的质量管控机制,向非功能需求的规范化管理目标逐渐迈进。
这种“自后而前,由外而内”的方式可以驱动开发重视和改善软件系统的内部质量,在迭代计划中加入技术债务消减工作任务项,从而改善软件系统的外部质量。这也是DevOps强调的“运维前移”实践。
2、内部质量透明化
开发之所以做出忽略内部质量,不理会技术债务的选择。除了进度压力、缺乏经济驱动力的原因之外,还有一点是:缺乏内部质量数据化的管理与技术债务发展趋势的透明化展示。
在我们所规划的DevOps平台中,会借助一些代码分析工具,把代码设计的“臭味道”嗅探出来,例如,当我们的软件出现下面一种“气味”时,可以表明我们的软件正在“腐化”:
僵化性:很难对系统进行改动,因为每个改动都会迫使对系统许多其他部分的改动。
脆弱性:对系统的改动会导致系统中和改动的地方在概念上无关的许多地方出现问题。
牢固性:很难解开系统的纠结,使之成为一些可以在其他系统中重用的组件。
粘带性:做正确的事情要比做错误的事情要困难些。
不必要的复杂性:设计中包含有不具任何直接好处的基础结构。
不必要的重复:设计中包含有重复的结构,而该重复的结构本可以使用单一的抽象进行统一。
晦涩性:很难阅读,理解,没有很好的表现出意图。
而这些代码的“坏味道”,通过JavaNCSS、CheckStyle、PMD等工具,都可以找出来并进行数值度量。通过这种方式,我们可以观察代码设计内部质量的变化趋势,及时提醒开发注意技术债务的积累,并及时修正和优化,从而防止情况恶化。这就好比健康监控,在量变达到质变之前,及时进行治疗。
3、基于持续集成的技术债务消减
在实践中,我们发现可以通过很多不同的维度评价代码设计的内部质量,借助工具也能度量出很多方面的数值。如果单单拿一个值来作为质量控制的阀值不是很科学,因为业界也缺乏一定的标准可参考。而且每个软件系统的业务、架构,甚至编程语言都不一样,很难有一个统一的标准阀值。
例如,对于代码复杂度的度量,业界通常用圈复杂度。IBM的某些数据调查也表明圈复杂度超过10的代码就会比较复杂、容易出错、不易维护。但是具体到某些项目,我们还是比较难定义这个阀值,是不是圈复杂度超过10的方法就一定要修改呢?
实践中我们认为不一定。例如某电信运营去中心化项目中,对于由老CRM代码切过来的新项目代码,在进度压力下,很多代码重构的工作量比较大,某些核心业务逻辑算法本身就比较复杂。在这种情况下,如果一刀切地要求圈复杂度超过10的方法就必须修改,否则不能签入代码,就未免太简单粗暴,可操作性也不强。
我们的处理办法是:基于持续集成的代码构建和自动化分析,在一段时间内,观察代码复杂度的变化趋势。首先确保新加入的代码不能超过一定的阀值。其次,确保对老CRM代码的修改后,代码复杂度保持原有水平,尽量下降。最后,要求在若干次迭代版本后,代码复杂度下降幅度要达到30%。
另外,对于复杂度偏高的代码,同时做出单元测试覆盖率的要求,例如分支覆盖100%。
关注变化趋势,而不是单一的一个阀值。这样,开发关注技术债务的积累过程,在早期就能做出技术债务消减的处理计划并持续实施。在这个过程中,Sonar质量度量平台可以帮助我们实现代码设计质量的持续监控,包括对代码复杂度、重复度、代码规范吻合度的监控等。
借助Sonar定义“质量门”,进行技术债务的预警,提醒开发人员及时处理技术债务。
并且,在趋势发展上可以给出直观的数据参考。帮助开发团队制定技术债务消减计划。
4小结
本文首先通过分析技术债务的形成过程,细分了技术债务的类型。然后从外部质量和内部质量的角度分析了技术债务处理的可行办法。最后提出方法:1、通过外部质量验收驱动技术债务消减。2、内部质量透明化,基于持续集成度量技术债务发展趋势。这样可以促进技术债务的及时处理,从而提高软件系统的总体开发运维效益,实现DevOps战略在传统IT领域的实施落地。
作者介绍:陈能技