焦油坑
遗留系统重构是个老生常谈的问题,因为在一个腐化甚至大泥球的架构上又脏又快的开发(这里的快是指代码写得快,而不是指更高质量端到端交付的快)是一件熟悉的机械的工作,处于一个稳定的逐步下滑的状态,特别在新增功能和故障双爆发double-killing的时候,项目从上到下都会陷入死亡行军中。犹如掉入一个焦油坑,越是挣扎滑落越深。一般没有外力推动的情况下,根本无法自拔。
麻木
更加关键的是,死亡行军中的人虽然很痛苦,但是并没有太多的心里的不安,认知让他们觉得软件项目就该如此。就像一个驼背,没有治好之前,不会感受到直着身子呼吸的畅爽。即使痛苦,也是麻木很久了。毕竟每天都能看到功能交付的进展,痛就痛吧。
欲速则不达
而对架构腐化的修复的收益是长效的,人普遍不能接受延迟满足的等待,因为延迟满足就是个反人性的过程,在巨大交付压力面前,如果没有老司机引路,大家一般都会选择看起来速效的又脏又快的解决方案,结果往往是越是抛开架构的裸奔,越是会使你的交付变慢。
上图中架构相当于地基、车轮和动力构成的一个系统,车上压的货物越多,车轮陷得越深,动力越是不足。所谓欲速则不达,货物运送的越慢取决于整个系统,而不是看着拉得多,其实单趟时间加长,整体效率反倒是降低的。
重构误区
因此,对于架构的重构和维护就变得非常重要。对于重构,往往会陷入两个误区:
– 过于保守
害怕风险,不愿意重构;或者总是想找到一个合适时机,重构一拖再拖。
– 过于激进
重构采用休克疗法蛮干,整个系统全部推到重写,由于缺乏对老系统的有效继承,新老系统功能对齐工作量巨大,新系统迟迟不能上线;而且往往缺乏防护网守护,风险巨大。
架构重构的命门
重构一定要有正确方法论的指导。否则很可能要么达不到重构目标,要么又很快再次陷入焦油坑,等于没重构。
遗留架构重构中靠谱的重构路径非常重要。
一旦要路径,就必须有现状和目标。同时沿着路径从现状到目标的过程中,为了防止出现偏差,相应的标尺就变得十分重要。
这样大型遗留系统重构路径的命门就全了:
标尺-》现状-》路径-》目标
标尺
重构的过程就像一个砌墙,砌墙一般是铅锤来标定和度量,不然墙很容易歪掉。这里铅锤就是一个标尺。
重构也一样,没有规矩不成方圆,只有设定好标尺,才能做好重构。
架构度量
好的架构,核心特征之一就是高内聚、低耦合。
展开架构度量之前,我们先看和耦合相关的几个概念。
(~)波浪线表示高能,高能部分可以跳过不影响阅读。
~~~~~~~~~~~~~~~~~begin~~~~~~~~~~~~~~
不稳定度
计算公式:代码原子出向依赖数量 / (入向依赖数量 + 出向依赖数量)
要求依赖必须要指向更稳定的方向。这里的不稳定度指的是组件/模块的变更成本,和它变更的频繁度没有直接的关联
连通度
计算公式:100*n/(V(V-1)),计算方法见下面例子。
这个指标用来描述组件内原子间的依赖程度,指标值越大,分数越低。
指标值在2%和5%算正常,高于5%的需要优先改进
公式中的n是系统依赖图中从a可到达b的对(a,b)的数目(依赖图中传递闭包中的边数)
比如下面这个由ABCD四个原子组成的组件的连通对为(B,A),(C,A),(B,D),(C,D),(A,D),所以n=5,
连通度=100*5/(4*3)=41.67
平均依赖
计算公式:E/V
该指标主要用来描述组件内平均每个原子的依赖数,指标值越大,分数越低,建议指标值在5以下。
(下图中的V)间的依赖关系(下图中的E)
循环依赖占比
计算公式:100 * n / (V*(V-1)/2)
度量系统依赖图中强连接的系统对的百分比。如果a可从b到达并且b可从a到达,则a和b是强连接。
耦合计算为100*n/(V*(V-1)/2),其中n是强连接对的数量。指标值越大,分数越低,建议指标值为0。
下图右上角内容即为反向依赖
以上指标如果条件允许,可以通过架构度量工具(caa、lattix、s101等)获取。
~~~~~~~~~~~~~end~~~~~~~~~~~~~~
架构重构的方向很重要的一个方面就是解耦,解耦的效果可以选取以上耦合度指标作为架构架构度量标尺,可视化架构重构中解耦的效果,可以有效防止重构过程中架构的腐化。
重要
架构优秀和架构度量指标之间是一个充分不必要的关系(就像体检一样,身体好体检指标会正常,体检指标正常身体可不一定好),既架构优秀,架构度量指标一定好,但是指标好,架构却不一定优秀。
优秀架构一定是高内聚低耦合的,架构度量基本都是反映架构耦合程度的,虽然降低耦合可以提升内聚,但不是完全对应的,所以架构耦合度只能作为架构优秀的部分条件。
防护网
架构重构要想方设法充分利用已有系统的自动化ST、FT作为防护网,并进一步补充这些已有防护网,同时架构重构过程中要抽取组件/模块,过程中要采用TDD的方式开发,补充尽量端到端的黑盒UT用例,完善测试金字塔,提升自动化测试的防护的效率。
重构的过程本身就是解耦的过程,其中防护网建设更是重中之重的工作,必须投重兵充分开展。
实际案例
我们通过一个实际案例来展开现状、目标和路径这些命门的应用。一个实际的遗留系统:
遗留系统情况
代码历史接近 10年
开发人员超过 30+
c/c++代码 200W+
全量构建 30分钟以上
核心痛点
系统耦合大
重复代码多
问题
项目经理说:一些简单的功能发布都要一个月以上
用服人员说:全面的熬夜升级值守都在这个项目,而且熬夜熬的提心吊胆。
开发人员说:这个代码,读不太懂不敢改 …
测试人员说:质量太差,故障泄露防不胜防…
现状
通过架构度量指标给出现状,架构质量非常差。
耦合度指标
重复度指标
目标
重构过程中,如果缺乏重构目标,很容易掉入以下几个陷阱:
1、重视新老功能对齐、轻视架构目标,重构完成后,架构依旧不优秀,重构效果打折扣。
a、完全抛开老系统,重写一套新的架构,以对齐老功能为目标,然后不停追赶重构过程中的新功能和故障单。由于交付压力巨大,陷入功能风暴和故障风暴中双杀,架构迅速腐化,再次掉入焦油坑。
2、架构目标不明确
a、重构没有目标约束,做到哪里算哪里,甚至连自身团队的潜能都没能发挥出来,重构意义不大。
这里给出本次架构重构抽象目标
– 解耦、消重
– 建立标尺(架构度量+防护网)
– 保持随时发布的能力
具体目标设定我们结合例子详细展开。
路径
路径是指从现状到目标之间的通道,只有路径清晰,才能做到有条不紊,才能给项目关键干系人(领导、项目成员)信心,才能推动架构重构开展和获得持续支持。
现状
目标
路径
大型遗留系统重构路径
对于大型遗留架构来说,考虑版本随时发布和老架构继承利旧,架构重构路径建议为:
1、新老并存,通过脚手架的方式实现新老功能路由和新老系统互通,这样可以保证目前交付同时最大限度继承老系统的已有实现。
2、考虑重构的优先级,从现状到目标给出明确方向和该方向上的重构组件/模块。
3、老系统逐步萎缩,新系统逐渐壮大,功能全部移植完成后,拆除脚手架,此间版本随时可以发布。
案例重构过程
重构过程如下:
分析现有架构问题,梳理出现状
关键实践:
通过架构依赖分析工具分析目前架构耦合情况
设计期望架构,给出目标
分层规则
1、上下分层
2、层内分布
架构 Hierarchy:层->组件/模块->包/文件夹->类/文件
边界规则
现有的类/文件,包/文件夹,组件/模块出现在期望的位置。
依赖规则
1、下层(中的类)不能依赖上层(中的类)
2、同一层内的模块(中的类/文件)之间没有循环依赖,最好没有直接依赖
3、各层都不能直接依赖infrastructure(基础设施)层,必须通过抽象层依赖。
目标架构
关键实践:DDD
用遗留代码重构思路,明确路径
1、利用绞杀者+脚手架模式进行解耦重构,复用老系统FT/ST,补充解耦后模块/组件自动化测试用例(最好是端到端黑盒UT),完善防护网
关键实践:解耦设计、重构、TDD
2、增加架构守护、“增量”解耦出更多模块/组件
关键实践:架构度量
3、单个模块/组件解耦思路
补充
重构小组构成
重构的过程中要有组织的支持,重构小组必须有架构师、骨干员工(对原系统深入了解并经验丰富)和的防火队员(对原系统深入了解并经验丰富)组成。
持续守护
架构重构过程中或重构完成后,都需要持续的架构度量和架构review进行架构守护,防止架构再次腐化。
总结
大型遗留系统架构重构首先要摸清现状,其次是树立目标,并建立标杆,然后设定路径,中间辅助数字化展示纠偏。只有有了清晰和可达的目标和路径,才能为整个团队指明重构方向,树立信心,并获得项目关键干系人的支持,重构才能获得成功。
祝大家重构顺利。
阅读原文