查看原文
其他

技术债治理的四条原则

杨政权 Thoughtworks洞见 2023-01-28

“技术债”是 Ward Cunningham 在1992年提出的,它主要用来描述理想中的解决方案和当前解决方案中间的差距所隐含的潜在成本。这种隐喻和金融债务非常类似,这也是这个隐喻的高明之处:为了解决短期的资金压力,获得短期收益,个人或企业向银行或他人借款,从而产生债务,这种债务需要付出的额外代价是利息。

如果短期商业的投资所带来的收益大于利息,这也许是一种明智的做法,但如果入不敷出,收益不及债务产生的利息就会导致资产受损。虽然长期来看这种投资仍然有可能扭亏转盈,但是整个过程风险很大,随时会导致个人或企业破产。

如果把技术债的产生也看做一种投资,那么获得的短期收益可能是快速上线带来的商业利益,比如新的功能吸引了更多的付费用户,解决了短期之内的资金缺口问题;赶在竞争对手之前上线了杀手级应用,并快速地抢占了市场。

不可否认,技术债的存在的确有很多积极的意义,但是我们经常会过度关注积极的因素,而忽略了技术债长期存在所导致的“利息”问题。


技术债全景图

卡内基-梅龙大学软件工程研究所(SEI)的Robert Nord在《The Future of Managing Technical Debt》提出了“技术债务全景图”(Tech Debt Landscape)的概念,我们可以借助于这个模型定性或者定量分析技术债务所产生的“利息”:

(图1:来自 Robert Nord 的《The Future of Managing Technical Debt》)

这张全景图主要从两个方向来分析技术债对于软件的影响:可维护性(Maintainability)、可演进性(Evolvability),同时结合问题的可见性(Visibility)分析技术债对于软件开发过程的影响。

这里的可维护性(Maintainability)主要指的是狭义上的代码问题,即代码本身可读性如何、是否容易被他人所理解、是否有明显的代码坏味道、是否容易扩展和增强。

其中可演进性(Evolvability)指的是系统适应变化的能力。 在生物学中它指的是种群产生适应性的遗传多样性,从而通过自然选择进化的能力。对软件系统来说,可演进性(Evolvability)本质上一种架构的元特征(Meta-Characteristic),描述的是软件架构趋于目标演进的能力,演进目标并不仅局限于支撑功能快速迭代(Iteration)的灵活性(Flexibility),也可以是其他的架构属性(Quality Attribute),比如高可用性、可扩展性。

针对可见性的分析可以依赖于外部视角:对于最终用户来说,软件功能、设计和用户体验等方面的缺陷,导致用户无法顺利完成既定的业务流程,那么对于用户不可见的代码问题就升级为了可见的质量问题;对于需求提供方来说,臃肿的技术架构、散落各处的业务逻辑导致产品无法快速响应需求变化,导致交付延期,那么对于无技术背景的业务人员来说,难以理解的、不可见的架构问题就升级为了可见的软件交付风险。


技术债治理的困境

技术债全景图的分类方法可以帮助我们更全面地了解技术债导致的问题,在可演进性和可维护性这两个维度,我们都可以提取出一些指标来量化“利息”,但是这些指标和业务功能相比终究显得太过苍白无力,并不足以说服主要的业务端干系人并获得对于技术改进的支持。

Mike Cohn(《Scrum敏捷软件开发》作者)曾经使用下面这张图来阐述这样一种情况:在第一个迭代需要花费大量的时间和精力来进行架构的设计,在后续的迭代中对于架构方面的投资不断降低,期望可以一直延续之前的架构设计并从中持续受益。

(图2:来自 Brent Barton 的《Managing Software Debt: Building for Inevitable Change》)

很多技术管理者或多或少地都会遇到上图中描述的困境:项目开始进入正常的开发节奏之后,技术方面的投资逐渐降低,尤其是在面临交付压力的时候,面对技术债更心有余而力不足,自己的意见总是被忽视,长期积压的技术问题迟迟得不到解决。我相信很多团队在治理技术债时都遇到过同样的阻力。

我们也曾经在团队中多次大刀阔斧地进行技术债治理的变革,团队以头脑风暴(Brainstorm)的方式收集技术债,添加到敏捷项目管理工具中统一管理,然后对于这些技术问题进行全局的优先级排序。

在业务端我们也积极地进行了技术债相关理论知识的导入,陈述技术债的产生原因和危害。业务端负责人非常认同技术债治理的意义,甚至主动提出要把技术债放到每个迭代中治理、追踪。虽然在接下来的几个迭代中情况得到了一些好转,但是在1~2个月之后,排到迭代计划中的技术改进比例又恢复了原状,能够真正得到治理的少之又少。

究其原因,团队在反思之后觉得主要有这几点:

  1. 团队对于技术改进缺少战略思考

    在今年的1月份,客户所处的的行业竞争不断加剧,新进入者对于客户的威胁越来越大,客户的投资重心也对应地发生了转变,把资源更多地分配给了另一个产品。在技术改进方面团队并没有及时调整技术改进的优先级,让改进的方向和业务的战略方向保持一致。

  2. 代码可维护性问题很难说服客户买单

    技术债的影响和收益是难以衡量的,对于这种代码级别的问题更是这样,对于没有技术背景的客户来说,很难用数字量化代码重构的直接收益。况且我们一直给客户承诺的是交付高质量、可工作的软件,除了性能、安全等非功能需求之外,代码质量本身也应该是内建的交付物之一,那代码达到什么水准才能体现出我们在这方面的专业性?

  3. 效果不明显,客户信心不足

    一个典型的例子是应用程序的性能优化,面对一些技术债导致的性能问题时只是隔靴搔痒。虽然加几个索引、调整一下SQL、增加过滤条件或者配置一下延迟加载(Lazy Load)可以使问题得到一些缓解,但是并没有触及本质的问题。随着数据量或者并发用户的增加,之前的问题又再次暴露了出来。也许下次在游刃余地解决问题的同时,也可以多质疑一下:领域模型设计是否合理?为什么一定要把这个一对多的关系放到某个实体上?花更多的时间讨论、探索不同的设计区别是什么,评估它们优劣的标准是什么?

基于这几方面原因并结合团队在技术债治理的实践经验,我们总结出了技术债治理的四条原则,也许可以为缓解这个困境提供一些不同的视角和思路


技术债治理的四条原则

1. 核心领域优于其他子域

识别领域、子域是DDD战略设计的重要步骤,在识别子域之后我们还需要进一步分析哪些是核心域(Core Domain),哪些是支撑子域(Supporting SubDomain)和通用子域(Generic Subdomain)。核心域在业务上至关重要,它提供了区别于行业竞争对手的差异化优势,承载了业务背后最核心的基础理念。《领域驱动设计》的作者、DDD概念的提出者 Eric Evans 是这样描述核心域的:

The Core Domain should deliver about 20% of the total value of the entire system, be about 5% of the code base, and take about 80% of the effort.

我们可以借助于这种战略建模方式,根据解决技术债之后所产生的收益,将其放置于领域图中的不同位置,可以得到类似这样的可视化结果。在建立关联之后,需要遵循“核心域优先、其他子域次之”的原则来选择技术债。也许我们可以把这种评估技术债优先级的方式叫做“Technical Debt Mapping”。

(图3:Technical Debt Mapping - 基于《领域驱动设计》中的 Bounded Context 修改)

2. 可演进性优于可维护性

技术债导致的可演进性问题大多和架构相关,比如服务和服务之间的循环依赖、模块和模块之间的过度耦合、缺少模块化和服务边界的“大泥球”组件等,在添加新的功能时,这些架构的坏味道会给产品功能的迭代造成不少麻烦。比如服务之间如果存在循环依赖的问题,当你对系统进行少量更改时,它可能会对其他模块产生连锁反应,这些模块可能会产生意想不到的错误或者异常。此外,如果两个模块过度耦合、相互依赖,则单个模块的重用也变得极其困难。

可演进性问题可能会直接导致开发速度滞后,功能无法按期交付,使项目出现重大的交付风险。而且问题发生的时候往往已经“积重难返”,引入的技术债务没有在合适的时间得到解决,其产生的影响会像“滚雪球”一样越滚越大。在我所经历过的项目中有一个不太合理的模型设计,由于错过了最佳的纠正时间,为了实现新的业务功能最终不得不做服务拆分时,发现需要修改的调用点竟有1000多处,而且这些修改点很难借助于IDE或者重构工具来一次性解决,不但增加了团队的负担还直接导致了后续功能需求的交付延期。

和可演进性问题相比,高复杂度、霰弹式修改等代码级别问题也很重要,但是相对来说我们更加关注软件适应变化的能力,通过提升软件系统的适应性减少软件最终交付价值的前置时间,快速收集真实用户的反馈,持续不断迭代产品、完善设计。

所以我们在治理技术债时坚持的另外一个原则是 “可演进性优于可维护性”。如果把上文提到的可维护性和可演进性使用不同的颜色来标识的话(红色表示可演进性问题、蓝色表示可维护性问题),我们可以得到这样的结果:

(图4:Classified Technical Debt Mapping - 基于《领域驱动设计》中 Bounded Context 修改)

3. 明确清晰的责任定义优于松散无序的任务分配

如果我们深入分析一下技术债产生的过程,很容易发现“交付压力”是一个频繁被提及的原因,这也许也是技术债这个隐喻本身存在的一些问题,原本应该体现为内建质量的工作被当做可以取舍、可以之后偿还的债务,导致必要的工作被滞后、被遗忘。有时候浮现式设计(Emergent Design)反而成了一种心理安慰的借口:“我知道这里有问题,但是我觉得这个变更需要通过需求来驱动”。

诚然,在开发阶段彻底消除技术债是需要付出额外成本的,在真实的项目中也很难明确定义出这样的边界: 哪些部分应该是用刻意设计(Intentional Design),哪些应该是用浮现式设计(Emergent Design)?在 Bob大叔的新作《架构整洁之道》中也提到这种类似的情形:在一开始的设计阶段,如果要划分出完美的架构边界,让两个组件在编译期和部署期相互独立,既要考虑动态的接口抽象、输入(Input)和输出(Output)数据结构定义,又需要做好组件之间的依赖管理,这些都增加了不少额外的工作量,可以采用一些妥协的做法比如共享组件、策略模式、外观模式(Facade)等。

那对应不同的组件、模块或者服务,谁可以来决定采用哪一种设计方法?两个模块之间应该采用完全隔离还是部分隔离?如果采用部分隔离应该采用哪一种方式?团队在针对技术债的治理过程中也经过一段很长时间的混乱期,团队中每个小组并不是一个独立的作战单元,而是一个个特性工厂(Feature Factory), 每个小组没有清晰的业务职责边界,分配功能特性的时候由各个小组根据兴趣、意愿等主观因素选择功能开发;针对于线上的问题分配也比较随机,更多是基于团队当时的忙碌程度和带宽,这些都导致了业务和技术上下文的割裂,原本在开发初期的架构设计原则逐渐退化,刻意选择要在未来偿还的技术债务在各个小组切换功能的过程中也逐渐被遗忘。

理想的情况下每个小组应该是一个价值趋向(Outcome Oridented)的团队,负责一个或者多个业务能力,原则上每个业务能力应该有且仅有一个团队负责。然而这种理想的情况在实际项目中又很难落地,即使业务能力和团队对齐,但客户对于不同业务能力的投资并不是均等固定的,如果客户一开始希望把百分之五十的预算花在某核心业务能力的构建上,而且要在3个月之内交付,之后的投资重心又转向了其他方面,在这种情况下要如何保持这种稳定的结构?不过这种业务能力和团队之间映射关系还是必要的,在多个小组合作开发时,可以由所负责的小组驱动必要的合作对话和相关的技术治理活动,触发关于业务上下文和技术上下文的跨团队分享,制定并推广代码规范、架构设计原则同时监督各个团队实施的效果。

但有时候业务能力和技术模块是无法一一对应的,现存的单体应用可能是横跨了多个上下文、提供了多种业务能力,在分配特性、技术债务或者线上问题的时候,高度抽象的业务能力无法提供有效的指导。所以我们采用了一种比较折中的方案 - 服务责任人制度(Service Owner), 一个小组负责一个或者多个微服务,每个微服务只由一个小组负责,在分配特性功能、技术债务和线上问题时,需要把服务责任人制度作为首要遵循的原则。由于业务知识和技术上下文的相对集中,在解决具体的软件缺陷时不再浮于表面,团队成员可以更加深入地从需求、技术方案、软件架构等方面着手解决根本问题;针对线上问题通过清晰、明确的责任制度,倒逼团队在开发阶段主动关注软件的内建质量,谨慎判断是否引入技术债。

(图5:服务责任人制度 - 横纵坐标分别为小组名称和服务数量,Yes / No表示服务活跃状态)

4. 主动预防优于被动响应

这个原则本质上是缩短反馈周期,提前发现潜在问题,除了必要的代码审查流程(Code Review)、提升团队能力之外还可以借助于自动化工具来提前发现问题。

对于代码可维护性方面,很多比较成熟的静态代码扫描工具都可以自动识别这类问题,比如SonarQubecheckstyle 等,但是仅仅在持续集成上(Continuous Integration)运行还不够,需要和团队一起自定义扫描规则,并把检查代码扫描报告作为代码审查的一部分,逐步形成一种正向的反馈机制。

那我们应该如何提前发现不可见的可演进性问题哪 ?在Neal Ford、 Rebecca Parsons 等合著的《Building Evolutionary Architecture》中提出了“架构适应度函数”(Architecture Fitness Function)的概念,可以给我们发现潜在的架构问题提供一些思路。

“适应度函数”这个概念来源于遗传算法(Genetic Algorithm),用计算机模拟仿自然界生物进化机制,适应度函数用于评价个体的优劣程度,适应度越大个体越好,反之适应度越小则个体越差。在软件系统的不断增量迭代过程中,我们可以基于架构的演进目标,定义出软件架构的适应度函数,来衡量增量的代码是否会导致架构偏离这个目标。

在工具方面使用方面,我们可以借助于 ArchUnit 和 NDepend帮助我们定义自己项目中的“适应度”规则,这是一个借助于ndepend自动识别组件循环依赖的例子:

(图6:来自 NDepend 官方文档中的依赖矩阵图)

在技术债治理的过程中,实践可以剪裁,甚至原则也可以妥协,因为比这几条原则更重要的是获得关键干系人的支持。作为技术人员或者技术领导者,不仅要有前瞻性的技术洞察力、锐意变革的魄力,还需要以“旁观者”视角,置身事外地观察自己所处的环境,思考技术改进究竟对于自己、他人、团队、公司和客户究竟产生了什么价值。


- 相关阅读 -

不就是个短信登录API嘛,有这么复杂吗? 

不就是个短信验证嘛,还真挺复杂的

点击【阅读原文】可至洞见网站查看原文&绿色字体部分的相关链接。

本文版权属ThoughtWorks公司所有,如需转载请在后台留言联系。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存