以增量演进为手段
Hi,我是阿昌
,今天学习记录的是关于以增量演进为手段
的内容。
遗留系统现代化中的 HOW,也就是第三个原则,以增量演进为手段。
很多团队在一阵大张旗鼓的遗留系统改造后,终于迎来了最终的“梭哈”时刻。尽管事先可能在各种测试环境测过无数遍了,但上线生产环境仍然如履薄冰。
和遗留系统项目“相爱相杀”十几年,这种一次性交付的大规模遗留系统改造,几乎不可能一上线就成功,必然会有各种或大或小的问题,甚至导致不得不全量回滚,交付日期一拖再拖。哪怕你的“战前准备”历时一年,甚至更久,到头还是一地鸡毛。
对于资源有限的小公司、小项目,还是应该衡量一下改造的难度和运维的能力,以控制风险为主。怎么控制风险呢?答案是增量演进
。
一、什么是增量演进?
什么是增量?什么又是演进呢?这要从演进式架构开始说起。《演进式架构》
这本书中给演进式架构下了精准的定义:支持跨多个维度的引导性增量变更的架构。
这么多的限定词,乍一听挺懵。其中,多维度是指技术、数据、安全、运维等不同的看待架构的视角;引导性是指在适应度函数的引导下,向着正确的方向演进架构;而增量变更是指以小步快跑
的方式,细粒度地构建和部署软件,同时在一定程度上允许新旧两种实现并行运行。
这里说的遗留系统中的增量演进,借鉴了演进式架构中“增量”的概念
。可以把已有的遗留系统作为“存量”,而每一次的优化、改进作为“增量”。
“演进”则要求将这些增量划分成非常小的粒度。这些小的增量也可以随时部署到各种环境来进行验证,每次验证的最小单元都是这些小的增量,而不是整个的改造结果。同时,新改进的实现和老的实现是并存的,一旦在验证时发现问题,可以随时回退到老实现。
因此,增量演进是指,以增量的方式,不断向明确的目标前进的过程。虽然理论上,可演进的架构才更容易实现小的增量变更,但大多数遗留系统的架构显然不是可演进的。
这时候怎么实现相对细粒度的增量交付呢?从代码和架构两个维度为例,具体分析一下。
二、代码的增量演进
在代码现代化方面,主要目标包括三类:
- 修补测试
- 代码重构
- 代码分层。
接下来将以代码重构
为例,向你如何实现增量演进。
下面的代码来自《代码整洁之道》
第 2 章“有意义的命名
”,Bob 大叔举了这样一个例子来吐槽糟糕的命名。
这段代码来自一个扫雷游戏,想实现获取所有被标记过的单元格的目的。
public List<int[]> getThem() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList)
if (x[0] == 4)
list1.add(x);
return list1;
}
不难发现,这段代码的坏味道远不止 getThem、theList 这种晦涩的命名,还包括魔法数字、基本类型偏执等。
面对如此多的坏味道,我相信对代码有洁癖的你,已经摩拳擦掌准备重构了吧?但是请别急,如果你直接改代码,在没有测试的情况下,有信心保证正百分之百正确吗?在遗留系统中,到处充斥着这样的糟糕代码,而且没有测试覆盖。
可以选择先补测试,然后再开始重构。这也是强烈推荐的方式,因为这样的步子迈得更稳、更扎实。但有时代码本身并不可测,还要先完成可测试化改造。
初衷就是单纯地重构这段代码,现在又要可测试化,又要加测试,似乎外延越来越广了,工作量也随之越来越大。
有没有办法不用加测试,也能安全地重构呢,并且完成增量式交付呢?答案是肯定的。这种方法其实很简单,就是先把代码复制出来一份,在复制的代码处进行重构。等重构完毕,再通过某种开关,来控制新旧代码的切换。
在测试时,可以通过开关来做 A/B 测试,从而确保重构的正确性。除了复制代码的方式外,还有一种更巧妙的方法来实现无测试的安全重构,并完成增量交付。
重构完的代码可以像下面这样,只有一行,十分精练:
public List<Cell> getFlaggedCells() {
return gameBoard.stream().filter(c -> c.isFlagged()).collect(toList());
}
这不介绍具体的重构过程了,毕竟的重点是增量交付
。
在原方法的调用端,可以像这样引入开关,来实现这个增量:
List<int[]> cells;
List<Cell> cellsRefactored;
if (toggleOff) {
cells = getThem();
// 其他代码
}
else {
cellsRefactored = getFlaggedCells();
// 其他代码
}
开关的值通常都写到配置文件,或存储在数据库里。可以通过修改这个配置,不断验证新代码的行为是否和旧代码完全一致。
直到经过了充分的测试,有了十足的信心,再来删掉开关,将旧代码完全删除。
《说透中台》专栏的作者王健,曾经把这种重构手法总结为“十六字心法”,非常形象、贴切:
旧的不变,新的创建。一步切换,旧的再见。
- “旧的不变”是指先
不动旧方法
; - “新的创建”是指
创建一个跟原来方法功能相同的新方法,可以通过先复制再重构的方式,来得到这个新方法
,也就是整个系统的一个增量; - “一步切换”是指,在充分测试之后,新的方法可以完全替代旧方法了,就将
开关切换
到新方法上; - “旧的再见”则意味着
删除旧方法以及相应的开关
,一个演进到此也就结束了。
会发现,这十六字心法不光适用于代码重构,也可以推广、复用,用在架构、安全、性能等其他维度,作为增量演进的指导方针。
三、架构的增量演进
如果说代码的重构还可以在短时间内完成并上线,那架构的重新设计就很难一蹴而就了。
这其实就更加需要小步上线,随时验证了。可能会说:“骗人的吧?要是说代码的改动可以小步前进,我还相信,但是架构调整这么大的动作,怎么可能增量演进呢?”
这其实就是一直想要强调的,越是大的改进,越要频繁上线去验证,不要等到最后来个“大惊喜”。
对于架构或系统的替换,Martin Fowler 提出了绞杀植物模式。这源于他一次在澳大利亚旅行时发现的奇观,一棵巨大的古树被榕树的藤蔓缠绕,许多年以后最终被榕树所取代。
“老马”(国内对于 Martin Fowler 的昵称)想到了一种与之类似的系统替换的方式,也就是新建一个系统,让它与旧系统并存且缓慢增长
,直到某一天完全取代旧
的系统。于是,老马就给这种方法起了一个名字,叫绞杀植物模式
。这里稍微说个题外话,这个模式一开始的名字是 Strangler,国内通常的翻译是“绞杀者模式”。
2019 年老马在个人网站上修订了这篇博客,将模式重新命名为 Strangler Fig。原因是这个模式虽然越来越流行,但是名字太血腥太暴力。Strangler Fig 直译成中文是绞杀无花果,听上去有点莫名其妙。其实 Strangler 本身就有绞杀植物的含义,因此倾向于把这个模式翻译为绞杀植物模式。
使用绞杀植物模式最主要的好处,就是降低风险
。作为绞杀植物的新系统可以稳定提供价值,并且频繁发布。你还可以很好地监控它的状态和进度。这种新旧系统或架构同时存在、同时运行、逐渐替换的方式,就是我们的增量演进所追求的目标。
假设有这样一个单体系统,包含员工、财务和薪酬三个模块,其中员工和薪酬模块都会调用通知模块来发送邮件或短信。
上游服务或前端页面通过 HTTP API 来访问不同的模块。
如果希望将薪酬模块迁移到独立的服务中,应该如何使用绞杀植物模式,以增量演进的方式做拆分呢?
可以分四步完成拆分。
第一步,建立开关
。要实现增量演进,开关是必不可少的。
- 一方面可以通过开关来控制 A/B 测试,以验证功能不被破坏
- 另一方面一旦新实现有问题,也能迅速回退到旧实现。可以将这个开关实现在 API 调用薪酬模块的地方,当开关打开的时候,调用新的薪酬服务,当开关关闭的时候,仍然调用已有的薪酬模块。这个开关可以是粗粒度的一个开关,也可以是细粒度的每个功能点一个开关。
建议你把开关尽可能设小一些,在实战中这种方式可以获得更小的增量演进和回滚。
现在的薪酬服务还是一个空壳,没有任何实现。如果打开开关,应该得到一个 501 Not Implemented 错误。
第二步,增量迁移
。按迭代逐步将薪酬模块的功能迁移到薪酬服务中。
假设需要 4 个迭代来完成全部的迁移工作,迭代 0 的工作主要是为开发开关和搭建新服务的脚手架,其余迭代就可以按计划来迁移不同的功能了。
在这一步,建议你从迭代 0 开始,就把薪酬服务部署到生产环境中。
虽然迭代 0 中的薪酬服务还没有任何功能,但这可以让先测试整个部署的过程,以及服务的连通性。
否则你就要在迭代 1 交付的时候既测试部署,又测试功能了。
虽然在迭代 0部署了薪酬服务,但是开关并不会打开,因此并不意味着交付了薪酬服务的功能。
将软件部署和软件交付(或软件发布)的概念做了区分,相信你能体会到它们之间的差别。
从迭代 1 开始,就会有迁移完成的增量发布到薪酬服务中了,可以打开开关来测试这一部分的功能。
第三步,并行运行
。对于有一定规模的架构演进,我强烈建议你将开关和旧代码保留一段时间,让新旧代码并行运行几个迭代。
对于遗留系统来说,这样做好处是利用新旧实现并行的这段时间,让隐藏的坑逐步浮现出来,直到我们对新实现有十足信心。
这里说的“隐藏的坑”意思是指,隐藏在代码和架构深处的,那些任何人都不曾知道的问题。
它们随时可能会暴露出来。多并行一段时间,可以让“子弹飞一会儿”,看看是否能够暴露出这些问题。
第四步,代码清理
。删除旧代码和开关,切记不要忘了这一步。
很多遗留系统的架构演进都没有完成这一步,导致很多无用的代码留在系统中。它们除了给人带来迷惑之外没有任何用处。
完成这四步之后,就实现了架构的增量演进过程。
会发现,架构的增量演进与代码的增量演进一样,也完美契合了“旧的不变,新的创建,一步切换,旧的再见”这十六字心法。
四、总结
为什么历时很久的遗留系统改造会以失败而告终呢?
- 一是因为直到最后一刻才上线,失去了持续验证的机会;
- 二是上线后发现有问题,只能硬着头皮热修复,或者整体回滚,缺乏细粒度的回退机制。而增量演进原则可以有效解决这个问题。
- 它一方面鼓励我们
持续
交付改造的功能或新的实现,不断在生产环境验证; - 另一方面拥有细粒度的开关,也使得回退变得十分
灵活
,一旦发现问题,我们只需要关闭引起问题的那个开关即可。
- 它一方面鼓励我们
在以增量演进为手段这个原则的指导下,对代码和架构的演进步骤做了比较详细的演示。
此外,在软件系统的其他维度,如数据、安全、性能、运维等,也可以用同样的方式完成改进。
增量演进的思想不仅体现在遗留系统现代化之中,平时做设计的时候,也应该遵循增量演进的思想。
一方面给予回退
的可能,小步地上线
另一方面,也可以先上线一个简单的方案
,然后再随着遇到的问题去不断演进
这个方案。
发现很多架构师在设计一个方案时喜欢一步到位,但这其实是错误的。这个世界上根本不存在完美的架构,所有的架构都应该是通过不断演进而浮现出来的,在演进的过程中应该根据当前上下文和约束的改变而不断调整,最终得到一个“差不多的”或者“刚刚好”的架构。而一步到位的思想,轻则导致过度设计,重则完全走错了方向,因为没能尽早上线去收集反馈。
虽然很多一步到位的决策,最后结果是走对了方向,那也不能说明有眼光,只能说明你运气好。
绞杀植物模式、并行运行模式等遗留系统现代化模式。