有些系统虽然刚开发不久,但你工作起来还是有各种不爽,比如:
如果以上问题你都自信满满,那我就要拿出杀手锏了。
你的代码有测试吗?你平时开发新需求时会写测试吗?你在修改 bug 时会补测试吗?经过这样的灵魂三问,你还有自信坚持说自己的系统不是遗留系统吗?
《修改代码的艺术》一书的作者 Michael Feathers 说过,“没有测试的代码都是遗留代码”。在我们越来越强调软件系统质量内建的今天,仍然有很多系统甚至很多刚刚开发的新系统,由于各种原因不写测试。有的说工期太紧,没时间写;有的说系统原来就没测试,我新加的这么几行代码没必要写。其实每种借口都禁不起推敲,都是在为自己不会写测试来打马虎眼。
软件系统本身就是一个不断熵增的过程,代码逐渐从有序变得无序。如果没有测试的严防死守,熵增的过程就会慢慢加快,代码很快就会变得混乱不堪。
不知道你是否跟曾经的我一样,身处一个遗留系统的漩涡之中,每天为毫无头绪的代码和混乱不堪的架构发愁。一个新的需求来了,都不知道从哪儿开始改起,即便看似简单的需求都要很久才能上线。
我想我们是不是得先明确一下,到底什么样的系统才能称之为遗留系统呢?它存在哪些问题,复杂在哪里?
关于遗留系统的误区
请你先思考这样一个问题:假如一个系统七八年了,它是不是个遗留系统?
系统的时间长等同于就是遗留系统,这是很多人的一个误区。虽然大多数遗留系统确实是存在的时间很长,但并不等于时间长的都是遗留系统。
遗留系统的特点
下图是一家银行的核心应用系统模块之间的交互图,我想没有一个人愿意工作在这样的系统上吧?
综合来看,代码和架构的质量差会导致遗留系统的维护成本相当高昂。这里的维护就包括:新需求的添加、线上 Bug 的修改,以及为了维护系统运行所需投入的软硬件和人力等。
说了这么多,我们似乎已经有了一个很具体的关于遗留系统的画像了,参考如下:
那是不是可以进一步抽象一下概念了呢?
在计算机领域,遗留系统是一种使用旧的方法和技术的、过时的,却仍旧在使用的计算机系统。
原因有很多。首先,可能是成本太高了,企业不愿意投入资源去改进;也可能是因为积重难返,根本改不动。而遗留系统往往都是企业的核心业务系统,支撑着整个企业的业务运营,这样的系统就算问题再多,也是不可替代的。
其次,遗留系统蕴含了大量的数据资产。遗留系统中的数据虽然很难与其他系统进行集成,但这部分数据的价值又是巨大的。企业的新系统常常不得不在这些数据的基础之上去构建,其他系统要想获得遗留系统中的数据,就必须对遗留系统进行修改,所以很多团队为了避免修改代码就会去寻求数据库层面的复制和同步,这也是一个选择。
另外,遗留系统中还藏匿着丰富的业务知识。由于业务人员长期使用并且养成了习惯,很多软件系统已经与业务融为一体,很难区分哪些是真正的业务,哪些是系统的设计。而由于系统历时太久,已经失去了能够正确描述系统现状的文档,所以到最后只有遗留系统的代码才能够准确表达系统的行为,以及与之对应的业务知识。
系统改造,有可为有可不为,而对于遗留系统来说,结合其现代化价值,看上去更像是一种不得不为。所谓现代化,其实就是从代码、架构、DevOps 和团队结构这四个方面来对遗留系统进行治理。
既然不能对遗留系统听之任之,我们就要下决心迎难而上,掌握主动权,否则当问题真正出现时就为时已晚了。
那遗留系统现代化的正确方向到底是什么呢?结合上节课的分析,遗留系统在代码、架构、测试、DevOps 方面存在诸多问题,我们在此基础上,将代码和测试合并(因为它们说的都是代码的质量),并引入开发团队这个维度,就得到了遗留系统现代化的四个方向:代码现代化、架构现代化、DevOps 现代化和团队结构现代化。
代码现代化顾名思义,就是把遗留系统中丑陋的“祖传”代码重构成职责清晰、结构良好的优质代码。
之所以说遗留系统中的代码是“祖传”的,是因为它和其他祖传的东西类似,都是历史悠久、且不敢轻举妄动的。而之所以不敢轻举妄动,就是因为缺乏测试,无法快速验证修改的正确性。而大多数情况下,之所以没有测试,又是因为代码写得不可测。可测试的代码和代码的测试是相互依存的,其中一个做到了,另一个也很容易做到,而如果其中一个没有做到,另一个也必然无法做到。
因此代码现代化的首要任务,就是对遗留系统的代码进行安全的可测试化重构。
在正常情况下,重构应该是在充分的自动化测试的保护下进行的。但对于没有测试的代码,我们只能“硬着头皮”去做一些相对来说比较安全的重构,将代码重构成可以写测试的程度,然后再补上大量的测试,进而在有充分测试覆盖的情况下,进行更广泛更深入的重构。
遗留系统现代化的第二个方向是架构现代化。看到“架构现代化”这几个字,有些同学很自然地就想到了微服务架构或云原生架构。然而我们前面说过,新不代表正确。在团队的开发能力、DevOps 能力和运维能力不足的时候,引入微服务,反而会将团队推向更痛苦的深渊。
有时候我们常常把软件系统比作一个城市,把系统架构和城市建设做类比。随着城市的发展和扩张,以前处于城市边缘的农村,反而会被周围新建的高楼大厦包裹成为一个城中村。治理这些城中村,就叫“改造老城区”。
有时候老城区的设计和规划会暴露出一些问题,不足以满足城市的发展。比如市政府通过一些集中的招商引资后,很多企业都要来这里建厂,但老城区显然没有足够的空间。这时候很多城市都会新建一个城区,有些地方叫开发区,有些地方干脆直接就叫新区。我们将这称之为“建设新城区”。
同样,遗留系统的架构现代化,我们也可以分成“改造老城区”和“建设新城区”两类模式。
代码和架构现代化了,DevOps 的现代化也不能落后。它对项目的重要性不言而喻,如果没有现代化的 DevOps 平台,代码和架构现代化所带来的优势,就无法淋漓尽致地体现出来。
假如在代码和架构优化后,需求的开发时间缩短了一倍,那么大家对于新需求上线的时间点自然也有新的期待。然而落后的 DevOps 水平反而会让这个时间变得更长,因为单体架构变成微服务了,DevOps 的难度增加了。
DevOps 的历史虽然只有短短十几年,但最近几年的发展势头却很足。大大小小的公司都开始了 DevOps 转型,很多项目都声称自己建立了持续集成流水线,但实际上很多都是只见其形不见其神,只学其表不学其里。
如果说代码、架构和 DevOps 的现代化还好理解的话,那这个团队结构现代化是个什么东西?其实很多时候,一个开发团队的结构是否合理,决定了这个团队的交付效率、产品质量,甚至项目成败,而很多人还没有对此产生足够的重视。
近年来有一本新书,叫做 Team Topologies,中文直译就是团队拓扑。一上市便引起了不小的轰动。它将团队放到了软件开发的第一位,提出了四种团队拓扑结构和三种团队交互模式。四种团队拓扑包括业务流团队、复杂子系统团队、平台团队和赋能团队。三种团队交互模式包括协作、服务和促进。我们在进行开发团队的组织结构规划时,应该参考这四种团队拓扑。去年这本书的中文版——《高效能团队模式》也已经上市了。
我们对于团队结构的现代化,基本上是围绕这本书的内容展开的。因为我发现,遗留系统中团队的问题,有时比遗留系统本身更大。比如很多遗留系统可能只有一两个人在维护,在他们遇到困难的时候根本得不到团队的支持;再比如一些遗留系统的“老人”对系统比较熟悉,因此任何新启动的专项治理小组都会邀请他们加入,导致这些人的变动十分频繁,上下文切换的成本极其高昂。
团队拓扑不仅对遗留系统至关重要,对一个新系统如何组建开发团队、团队之间如何沟通协作也是至关重要的。
第一种策略是 Encapsulate,也就是将遗留系统中的数据或者功能封装成 API,供外部调用。
场景一
我们在上面提到过,遗留系统中蕴含着丰富的数据资产,但是因为技术和工具落后,导致它难以与新系统集成,这些数据被封印在遗漏系统中,成了数据孤岛。比如早期的银行或民航软件,很多都是部署在大型机上的。企业非常希望开发手机 App,这样才能更好地为客户服务,但却很难访问到主机上的这些数据。
同样地,遗留系统中还有一些功能十分重要,其他外部系统需要这些能力来构建业务。比如一些公文流转的工作流,可能构建在基于 Lotus Notes 的办公系统中,但如果企业想要开发移动办公 App,并在 App 中复用这套工作流,也是困难重重。
问题虽然棘手,但事到临头,工程师们总要想办法应对。结合刚才说的情况,我们可以封装这些数据和功能,形成 API,供这些移动 App 或其他外部系统使用。如果遗留系统本身就是基于 Web 的,可以在 Web 系统上直接构建 API;如果不是,可以选择构建一个全新的 Web API 来部署并提供服务。
这样做的好处是,以较低的成本和风险,尽可能满足外部系统的需求。你无需对遗留系统做较大的修改,只是增加一些 API 而已。遗留系统本身不会被优化,但它可以通过这些 API 对外提供能力。
场景二
还有一种情况,我也建议你使用封装的策略,那就是当你有一个第三方系统,希望扩展它的功能,但只能访问它的数据库,却无法修改代码的时候。
这时有些团队采用的方式就是直接连它的数据库,并在已有的系统中基于这些数据构建新的功能。我不建议你这么做,直接连数据库固然简单,但由于你可以访问它所有的表和列,距离混乱也就剩一步之遥了。
我建议你基于这个第三方系统的数据库构建一个 Web API,来向其他的系统提供你想提供的数据和功能,而不是暴露全部的数据。
我这里也稍微剧透下,封装的策略落到具体应用的时候,衍生出了很多相关模式,比如数据 API 模式、功能 API 模式等等。
第二种策略是 Replatform,也就是替换运行时平台。这种策略不需要对代码大动干戈,只需要改动很小一部分。到了新的平台后,软件的功能和特性仍然保持不变。
比如,很多银行或民航软件还是基于 COBOL 的主机系统,把它们从大型机上迁移到 Linux 或 Windows 环境,就会甩掉昂贵的主机成本。
在使用 Replatform 时,你只需要对代码做少量更改,以适配新的平台。这样,只通过较小的成本就可以降低基础设施的成本,并提高性能。
还有一种迁移我认为也可以看做是 Replatform,虽然它并不是替换运行时平台。那就是迁移代码版本管理工具。比如你把代码从 SVN 迁移到 Git 中,不需要修改任何功能代码,但却可以享受新的代码管理平台带来的好处。
第三种策略是 Rehost,也就是将应用程序或组件部署到其他基础设施中,如虚拟主机、容器或云。这种策略完全不需要修改代码,而只需要迁移部署的环境,甚至都不需要重新编译,因此这种迁移方法也有个很形象的别名,叫做“lift and shift”,就是原封不动地拎起来,转移到别的地方去。
Rehost 可以让你在完全不修改已有系统的情况下,快速上云,体验云环境带来的弹性、安全性和高性能,并且迁移过程也能做到很平滑。然而由于没有任何适配,也就无法充分利用云原生的优势,因此还需要对系统内部的代码和架构做进一步调整,比如将单体架构拆分为可以独立运维的微服务。
第四种策略是 Refactor 和 Rearchitect,它们是指在不改变系统外部行为的前提下,对代码或架构进行调整、优化,以偿还拖欠已久的技术债务、改善非功能需求、提升系统健康度。
第五种策略是 Rebuild 和 Replace,都是指对遗留系统进行替换。它们两个替换的范围和程度不同。
下图是对上面五种策略的一个总结,你可以从中看出它们的收益、风险和成本(用面积表示):
遗留系统现代化的几种策略,不同的策略有不同的适用场景,我把它们总结到了一张表中。
你要记住的是,一定要根据项目的情况来选择不同的策略组合。不要上来就大张旗鼓地重构或重写,一定要弄清楚想要的是什么。除了重构和重写,你其实还有很多选择。
有效的成果验证既能鼓舞团队士气,又能让领导和业务方感受到改造的价值,还能为你后续的改造安排打下良好的试点基础和团队实力。
遗留系统现代化长路漫漫,千万不要因为这些比较容易做却没有做的小事儿,而半途而废啊。