上一讲主要介绍了如何有效管理第三方组件的实际案例,目的是让你意识到依赖组件的质量也会影响到软件的质量。前面几个课时谈论的主要内容都是跟软件质量相关,通过各种方式方法提高软件交付的质量。这时就会遇到一个问题,软件质量固然重要,但工作中我们不可能把软件质量做到完美无缺才上线。软件质量不是免费的,更好的质量需要付出更多的成本和时间。那么如何平衡软件的开发速度和软件质量的关系,这就是今天要介绍的内容——技术债务。在软件开发的过程中,技术债务是不可避免的,有技术债务也是正常的。那么,如何合理、有效的管理技术债务,让软件实现健康地发展呢?
什么是技术债务
相信很多软件开发人员都阅读或修改过他人写的代码,特别是企业里遗留下来的老代码。如果要修改其中的一个 Bug,或者在这些系统之上增加新的功能,那将非常痛苦。我听到很多次:“改他人的代码,还不如我自己写一个。”有时候并不是说大家喜欢重复造轮子,而是因为目前这个“轮子”年久失修,已经不具备重复使用的可能性。
目前大多数企业由于交付期限的压力,都存在这样的问题:
& 文档没有写或者根本与当前版本不同步;
& 架构设计也只是满足当时的需求;
& 代码中留下了很多待优化的代码片段;
& 遗留代码缺乏文档和单元测试,无人能改,无人敢改。
上面提到的这些问题你应该都能理解,这些问题大多是技术债务没有有效处理导致的结果。那么,什么是技术债务?技术债务的概念是由敏捷先驱 Ward Cunningham(沃德·柯宁汉)在 1992 年提出的,是指那些现在选择不做,但如果一直不做,就会影响未来软件发展的事情。 下面两个例子可以帮助我们理解。
& 过时的架构设计:支撑 1 万人的架构和支撑 1 千万人的架构肯定是不一样的。在最初用户量不大时,采用单体架构满足业务需求是没问题的,但随着用户量的增长,系统架构不做升级,就会影响到业务的发展。
& 需要重构的代码:在单体架构时,为了加快数据的加载速度,会在内存中将预先加载的数据缓存起来。但当采用分布式系统架构后,这段代码逻辑就要进行相应的重构,采用像 Redis 这种分布式缓存方案,否则就会出现数据不一致的情况。
要注意的是,未实现的功能需求不属于技术债务。
因此,技术债务是在短期内不会产生影响,但从长期来看,经常背负技术债务的团队会面临潜在的严重问题,甚至会危及软件的可持续性。每个技术债务,无论多么小,偿还的成本都会随着时间的推移而不断增加,还会导致额外的技术债务。
技术债务是如何产生的
生命周期
在介绍技术债务产生的原因之前,我们先了解一下技术债务的生命周期,掌握技术债务从产生到消亡的整个过程。如下图所示。技术债务经历了四个阶段:
1.产生阶段, 由于各种原因产生了技术债务,此时并未意识到技术债务的存在。
2.意识阶段, 开发人员意识到技术债务的存在,但并未影响到软件的正常发展。在此之前一直享受技术债务带来的红利。
3.临界点阶段, 技术债务已经阻碍软件的正常发展了,出现负债的情况。
4.补救阶段, 采取措施,偿还技术债务,恢复到正常开发流程。
产生的原因
从上图可以看出,技术债务的产生是源头。要想有效管理技术债务,首先就要了解技术债务产生的原因。下面介绍一下在许多团队中技术债务产生的常见原因,有利于团队能够清楚地认识技术债务,并选择正确的规避方法。这些原因主要与业务、上下文改变、开发流程和团队有关,如下图所示。
第一个原因与业务有关。
在企业里,业务目标、需求、具备的资源以及能够承担的风险都会影响软件产品。业务问题导致技术问题,进而产生技术债务。
& 时间和成本压力
开发团队通常因为项目交付的时间、项目预算问题,而优先处理用户需要的功能。业务人员和客户也只关心如何更快的向用户交付新功能,抢占市场先机。因此,开发团队没有太多时间设计具有低耦合、高内聚、可扩展的服务层,而需要继续在现有的系统中添加这些功能。虽然短期内能暂时满足项目需求,但此时技术债务已经产生。
& 业务目标不匹配
开发团体的设计方案和技术选型不是为了实现业务目标,而是其他方面的原因。比如,企业需要快速开发一个 App。但技术人员考虑到性能因素,采用了原生的 Android 和 iOS 语言开发。由于对语言的掌握有限,导致系统设计差,用户体验不好,最终导致大量返工。
& 需求不足
当需求不明确,就会产生技术债务,特别是对软件的非功能性需求,没有明确的要求(如安全性、性能、可扩展性等)情况下。
第二个原因与上下文改变有关
技术债务是一个与时间有关的概念。即便是在当初做决策时未产生任何技术债务的设计,随着时间的推移也会过时,需要重新设计。这种重新设计是由业务、技术的变化,或者自然进化造成的技术债务的结果。
& 业务上下文变化
系统的所有决策在当初都是合理的,可突如其来的外部事件改变了现有的业务目标。比如企业被收购,收购后系统需要进行融合等。
& 技术变化
软件、硬件和中间件技术的更新迭代,导致现有系统债务重重。比如:十年前使用 JDK5 开发的应用系统,在今天的技术形态下只能推翻重来了。
& 自然进化
系统在演化的过程中会发生变化,如修复问题、增加新功能。这种变化本身也会削弱一个系统,这是自然进化的结果。就跟人类身体的零部件随时年龄慢慢老化一样,如果做过手术,更会元气大伤。
第三个原因与开发流程有关
我们通常将没有按照软件工程实践和开发规范完成的事项也归类为技术债务。这类技术债务要分析哪些是没有遵循流程规范,哪些是流程规范本身不合理导致的。再根据分析结果进行相应的处理。
& 无效的文档
文档,一直都是软件开发过程中的痛。开发人员对编写文档的抵触一直都是存在的。事实上,关键性文档的缺失或过时增加了系统产生技术债务的风险,比如架构设计文档、测试用例文档等。最初的设计人员在交付压力下,并未有效纪录架构设计的细节、约束和遗留的问题,后续的开发人员会犹豫是否要更改他们不确定的代码。
& 测试自动化不足
随着系统功能不断增加,开发人员会着重关注当前开发的新功能是否可用,而对已有功能的关注就相应减少了。这时就需要有一套自动化测试工具,能够对已有的功能进行测试。这样就可以校验当前的变更是否对已有功能产生影响。
& 流程不匹配
为了保障研发过程顺利开展,所有的软件研发团队都有自己的研发流程。团队成员如果偏离该流程,就有可能导致技术债务,比如代码评审,分支策略,导致版本分支混乱。
第四个原因与团队相关。
影响系统发展的一个关键的、经常被忽略的因素是人。需求是人提出的,架构是人设计的,代码是人编写的,系统是人使用的。有很多现实的例子表明,团队成员的业务和技术水平对于技术债务的产生至关重要。
& 经验不足
一般情况下,团队中经验较少的队员在编码时更容易产生技术债务。比如缺乏模块化设计、代码复杂度的理解。由于企业内部对员工级别的限制和团队梯队的合理性,每个团队中都会有一些初级开发人员,因此,对初级开发人员的工作结果审核和技能培训是至关重要的。
& 分布式团队
沟通问题可能会导致对设计决策理解不一致。分布式团队经常会面临着沟通协调的问题。比如广州的团队进行架构设计,北京的团队负责代码实现,在进行任务沟通时,将会是一个很大的挑战。
& 非专属团队
团队成员横跨多个项目,特别是经验丰富的开发人员。多任务并行不仅需要在多个任务间进行切换,而且往往团队和个人会将注意力集中在最紧迫的项目上,而忽略对其他项目的关注。不能高效、专注地完成任务,不可避免的会产生技术债务。
管理技术债务的原则和实践
介绍完技术债务产生的原因,下面就要介绍如何有效地管理技术债务。**管理技术债务不是一项一次性活动,它是软件开发过程中的一部分。**下面按照避免产生、提前发现和尽快修复的顺序和原则介绍如何有效管理技术债务。
避免产生技术债务
团队成员的综合能力和丰富经验可以识别技术债务以及避免产生技术债务。因此,从团队成员来说,避免产生技术债务的方法是:
& 让具有丰富经验和能力的人参加到项目中;
& 让具有丰富经验和能力的人把控项目的进度和质量;
& 建立系统化的业务知识和技能的培训,提升团队的整体实力。
提前发现技术债务
大多数情况下,技术债务都是无意产生的。如果要想尽早发现这些技术债务并不是一件容易的事。目前有大量的免费、开源的工具可以帮助识别代码中的技术债务。
& 单元测试框架
单元测试是开发人员自己编写测试,以验证代码中单个代码单元的正确性。如果单元测试失败,团队成员能够立即发现并修复导致失败的代码。单元测试还可以检验当前变更是否对已有功能有影响。目前,每一种编程语言都有对应的单元测试框架 xUnit。比如 Java 语言的 JUnit,Python 语言的 PyUnit 和 C++ 语言的 CppUnit 等等。
& 静态代码分析
静态代码分析工具可以识别代码的编码规范、漏洞和缺陷,以及重复代码等问题。这些工具可以方便地与流水线进行集成,自动化的执行扫描和生成检查报告。下图是 SonarQube 执行代码检查的结果,可以非常清晰的了解当前代码的内部质量和技术债务情况,点击上面的数字,能够看到产生该问题的具体代码片段,对于解决问题非常有帮助。
& 持续集成
传统软件开发中,一般要到每个功能完成后才会进行集成,此时会有各种各样的问题,如果该问题影响范围较大,甚至将设计方案推翻重来,那付出的代价将会是巨大的。持续集成是一种不同于传统开发的方法,团队每天至少集成一次或者每次提交到代码库时,自动执行集成和验证的过程。通过频繁的集成尽早发现集成中遇到的问题,降低最终集成时的风险。
尽快偿还技术债务
当我们通过代码扫描工具或者在开发过程中发现技术债务后,就要考虑尽快偿还技术债务。根据技术债务的难易、大小、缓急采取不同的偿还策略。
& 立即偿还技术债务
对于能当时处理的就立即偿还掉。这种方法可能是最有效的方法,但也是实施起来难度最大的方法。团队习惯于对当前正在处理的功能进行编码,而是把技术债务留到以后去改进,即便是“以后”永远也不会到来…无论如何,尽快偿还是解决技术债务和使软件更具有可维护性的最有效方法。
& 标记技术债务
当发现技术债务的代码时,如果不能立即处理,就先记录一下,然后在后面的某个时间点进行处理。之前的做法是添加 TODO 或 FIXME 这样的注释作为临时占位符,标记这里是存在技术债务的。但随着时间的推移,这些注释不但效果不好,也不会阻止有问题的代码提交到代码库。更有效的做法是在发现技术债务的代码中添加运行时异常,这样就可以使代码运行时处于中断的状态,阻止有问题的代码提交到代码库。
可以看下面的例子,在代码中灵活配置门限的阈值。代码片段如下:
try {File thresholdConfig = new File("c:/threshold.config");FileInputStream fis = new FileInputStream(thresholdConfig);Properties infoProperties = new Properties();infoProperties.load(fis);String thresholdVal = infoProperties.getProperty("threshold");System.out.println(thresholdVal);} catch (IOException e) {e.printStackTrace();}```这段代码的问题是以硬编码的方式从本地加载配置文件。开发人员也了解这样写是不对的,因为他只是想调试一下代码逻辑的正确性,并且不希望分散目前的注意力。那么可以将运行时异常添加到验证成功代码的后面,这样既能验证代码逻辑的正确性又不会遗忘此处的技术债务。如下所示:
try {
File thresholdConfig = new File(“c:/threshold.config”);
FileInputStream fis = new FileInputStream(thresholdConfig);
Properties infoProperties = new Properties();
infoProperties.load(fis);
String thresholdVal = infoProperties.getProperty(“threshold”);
System.out.println(thresholdVal);
throw new RuntimeException(“不要以硬编码的方式加载配置文件”);
} catch (IOException e) {
e.printStackTrace();
}
运行的结果如下图所示:
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/cb6cb49fad4d4644adae2ed09972a85a.png)
& **对技术债务进行排期**当发现技术债务较大,并不能在短时间内解决时,就需要将技术债务添加到产品列表(Product Backlog)中,产品经理将技术债务和需求一起排列优先级,共享团队的资源和时间,并和需求一样进行跟踪和验收,保证技术债务确实被偿还。**总结**本课时开篇以如何平衡软件开发进度和软件质量之间的关系引出技术债务。并提出在软件开发过程中,技术债务是不可避免的,需要有效的管理和控制技术债务的蔓延,才能保证软件的健康发展。接下来系统化地介绍了什么是技术债务,什么不是技术债务,分析了技术债务产生的原因。根据技术债务的生命周期曲线图,技术债务产生后,在一段时间内会对当前业务有积极作用,只是随着时间的推移,慢慢会成为阻碍企业发展的绊脚石。因此,在管理技术债务部分,先是在产生之前能避免则避免,不能避免的可以借助工具提前发现,对于发现的技术债务尽可能早的偿还掉。有句俗话:“勤借勤还,再借不难”,言简意赅地表明了管理技术债务的策略。针对如何管理技术债务,你是否有其他的方法,欢迎在评论区分享自己的方法。