gitbook/遗留系统现代化实战/docs/506570.md
2022-09-03 22:05:03 +08:00

152 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 02 | 遗留系统现代化:遗留系统的四化建设
你好,我是姚琪琳。
上节课,我们学习了什么是遗留系统,对于老旧、过时,但又十分重要、不可替代的遗留系统,听之任之只会埋下隐患,真正出现问题就为时已晚了。
不过在动手改造遗留系统之前我们先要找准方向。其实相比遗留系统“治理”、“改造”我们更强调的是“现代化Modernization也就是把遗留系统变为现代化的系统。这也是国际上更通用的提法。用“Legacy System Modernization”这个关键词在Google上能搜到1380万条结果。
很多团队在对遗留系统进行“改造”或者“现代化”的时候,往往会陷入一个误区,就是盲目引入各种时髦的新技术,仿佛“新”就代表着“好”,就代表着方向正确。
比如我们耳熟能详、近年来愈发流行的微服务架构,有些团队也不管自己的项目适不适合,上来就把一个“大泥球”式的遗留系统肢解成了几十个微服务。更有甚者,一个遗留系统拆成了几百个微服务,有些甚至一张表的“增、删、查、改”居然被拆成了四个服务。架构似乎“现代化”了,运维人员却“哭”了。
那遗留系统现代化的正确方向到底是什么呢结合上节课的分析遗留系统在代码、架构、测试、DevOps方面存在诸多问题我们在此基础上将代码和测试合并因为它们说的都是代码的质量并引入开发团队这个维度就得到了遗留系统现代化的四个方向**代码现代化、架构现代化、DevOps现代化和团队结构现代化**。
## 代码现代化
代码现代化顾名思义,就是把遗留系统中丑陋的“祖传”代码重构成职责清晰、结构良好的优质代码。
之所以说遗留系统中的代码是“祖传”的,是因为它和其他祖传的东西类似,都是历史悠久、且不敢轻举妄动的。而之所以不敢轻举妄动,就是因为缺乏测试,无法快速验证修改的正确性。而大多数情况下,之所以没有测试,又是因为代码写得不可测。可测试的代码和代码的测试是相互依存的,其中一个做到了,另一个也很容易做到,而如果其中一个没有做到,另一个也必然无法做到。
**因此代码现代化的首要任务,就是对遗留系统的代码进行安全的可测试化重构**
在正常情况下,重构应该是在充分的自动化测试的保护下进行的。但对于没有测试的代码,我们只能“硬着头皮”去做一些相对来说比较安全的重构,将代码重构成可以写测试的程度,然后再补上大量的测试,进而在有充分测试覆盖的情况下,进行更广泛更深入的重构。
后面的课程我还会详细讲解如何进行可测试化重构但在这里我想先举一个小例子来让你有个感性认识。比如下面的代码我想测试if的逻辑当Dao的方法返回一个null时这段代码会抛出一个异常。
```java
public class EmployeeService {
public EmployeeDto getEmployeeDto(long employeeId) {
EmployeeDao employeeDao = new EmployeeDao();
// 访问数据库获取一个Employee
Employee employee = employeeDao.getEmployeeById(employeeId);
if (employee == null) {
throw new EmployeeNotFoundException(employeeId);
}
return convertToEmployeeDto(employee);
}
}
```
看到这样的代码你可能会说这质量还行啊可读性不错职责也比较清晰。的确是这样但这样的代码却是不可测的。因为EmployeeDao内部会访问数据库从中读取出一个Employee对象。而这个EmployeeDao是在方法内通过new的方式直接构造的就意味着这个方法对EmployeeDao的依赖是固定的无法解耦的。
要知道在单元测试中,我们是不可能直接访问真实的数据库的,因此要想测试这样的方法,只能先对它进行可测试化重构,也就是先将它重构为可测试的代码。
什么样的代码叫可测试的呢?比如下面这样:
```java
public class EmployeeService {
private EmployeeDao employeeDao;
public EmployeeService(EmployeeDao employeeDao) {
this.employeeDao = employeeDao;
}
public EmployeeDto getEmployeeDto(long employeeId) {
Employee employee = employeeDao.getEmployeeById(employeeId);
if (employee == null) {
throw new EmployeeNotFoundException(employeeId);
}
return convertToEmployeeDto(employee);
}
}
```
通过这次重构我们把会访问数据库的EmployeeDao提取成类的私有字段通过构造函数传入到EmployeeService中来在getEmployeeDto方法中就可以直接使用这个EmployeeDao实例不用再去构造了。由于传入的EmployeeDao并不是EmployeeService构造的所以后者对前者的依赖就不是固定的是可以解耦的。
如果我们传入EmployeeService的是一个new出来的EmployeeDao那和原来的方法一样仍然会去访问数据库如果传入的是一个EmployeeDao的子类而这个子类不会去访问数据库那么getEmployeeDto这个方法就不会直接访问数据库它就变成可测试的了。比如我们传入这样的一个子类
```java
public class InMemoryEmployeeDao extends EmployeeDao {
@Override
public Employee getEmployeeById(long employeeId) {
return null;
}
}
```
这样想测试原方法中if的代码逻辑就非常方便了。
这里我们使用的重构手法叫做**提取接缝Extract Seem**,至于什么是接缝,以及还有哪些可测试化重构的手法先按下不表,后面课里我会细说,你先有个初步印象就好。
当代码可测了,我们就可以为它们添加足够的测试,提供质量保障。然后,在测试的保障下进行安全的重构。接下来要做的就是将“祖传”代码重构得让人耳目一新。当代码结构良好了,再实现下一个代码现代化的目标,也就是良好的分层结构。
## 架构现代化
遗留系统现代化的第二个方向是架构现代化。看到“架构现代化”这几个字有些同学很自然地就想到了微服务架构或云原生架构。然而我们前面说过新不代表正确。在团队的开发能力、DevOps能力和运维能力不足的时候引入微服务反而会将团队推向更痛苦的深渊。
有时候我们常常把软件系统比作一个城市,把系统架构和城市建设做类比。随着城市的发展和扩张,以前处于城市边缘的农村,反而会被周围新建的高楼大厦包裹成为一个城中村。治理这些城中村,就叫“改造老城区”。
有时候老城区的设计和规划会暴露出一些问题,不足以满足城市的发展。比如市政府通过一些集中的招商引资后,很多企业都要来这里建厂,但老城区显然没有足够的空间。这时候很多城市都会新建一个城区,有些地方叫开发区,有些地方干脆直接就叫新区。我们将这称之为“建设新城区”。
同样,遗留系统的架构现代化,我们也可以分成“改造老城区”和“建设新城区”两类模式。
**改造老城区模式是指对遗留系统内部的模块进行治理、让模块内部结构合理、模块之间职责清晰的一系列模式。**前端方面包括单页应用注入、微前端等,后端包括抽象分支、扩张与收缩等,数据库端包括变更数据所有权、将数据库作为契约等。
![](https://static001.geekbang.org/resource/image/03/41/038a1616b382744261c92ed7945e0c41.jpg?wh=3840x1859)
**建设新城区模式是指将遗留系统内部的某个模块拆分到外面,或将新需求实现在遗留系统外部的一系列模式。**包括绞杀植物、冒泡上下文等。为了对新建立的新城区予以各种支持老城区还可以通过提供API、变动数据捕获、事件拦截等各种模式与新城区进行集成。
![图片](https://static001.geekbang.org/resource/image/63/21/63015b42d785dd8328ef1a618624d521.jpg?wh=1920x992)
看到这么多专业名词,你可能应接不暇,别担心,后面课里这些内容都会详细展开。总之,只有“改造老城区”和“建设新城区”齐头并进,遗留系统架构的现代化版图才算完整。
## DevOps现代化
代码和架构现代化了DevOps的现代化也不能落后。它对项目的重要性不言而喻如果没有现代化的DevOps平台代码和架构现代化所带来的优势就无法淋漓尽致地体现出来。
假如在代码和架构优化后需求的开发时间缩短了一倍那么大家对于新需求上线的时间点自然也有新的期待。然而落后的DevOps水平反而会让这个时间变得更长因为单体架构变成微服务了DevOps的难度增加了。
DevOps的历史虽然只有短短十几年但最近几年的发展势头却很足。大大小小的公司都开始了DevOps转型很多项目都声称自己建立了持续集成流水线但实际上很多都是只见其形不见其神只学其表不学其里。
而遗留系统的状况就更惨不忍睹了它们几乎没有任何的自动化或仅仅是一两句简单的构建命令。像我在第一节课里举的例子那样在开发机上打包、靠人工用移动硬盘部署的项目还比比皆是。因此遗留系统的DevOps现代化与其说是一种改进不如说是从0到1的建设。这一部分可以和代码、架构的治理并行甚至可以更早。先把平台搭起来再逐步往上添加内容。对于大多数遗留系统来说有一个可以对代码进行构建、打包的流水线就已经是极大的进步了。
要从头开始搭建一个DevOps平台包括代码、构建、测试、打包、发布、配置、监控等多个方面。这其中的代码和测试有一部分是和代码现代化重叠的代码现代化的课里我会一并说给你听。剩下的几个部分再专门用一节课来详细讲述。
## 团队结构现代化
如果说代码、架构和DevOps的现代化还好理解的话那这个团队结构现代化是个什么东西其实很多时候一个开发团队的结构是否合理决定了这个团队的交付效率、产品质量甚至项目成败而很多人还没有对此产生足够的重视。
近年来有一本新书叫做Team Topologies中文直译就是团队拓扑。一上市便引起了不小的轰动。它将团队放到了软件开发的第一位提出了四种团队拓扑结构和三种团队交互模式。四种团队拓扑包括业务流团队、复杂子系统团队、平台团队和赋能团队。三种团队交互模式包括协作、服务和促进。我们在进行开发团队的组织结构规划时应该参考这四种团队拓扑。去年这本书的中文版——《高效能团队模式》也已经上市了。
我们对于团队结构的现代化,基本上是围绕这本书的内容展开的。因为我发现,遗留系统中团队的问题,有时比遗留系统本身更大。比如很多遗留系统可能只有一两个人在维护,在他们遇到困难的时候根本得不到团队的支持;再比如一些遗留系统的“老人”对系统比较熟悉,因此任何新启动的专项治理小组都会邀请他们加入,导致这些人的变动十分频繁,上下文切换的成本极其高昂。
团队拓扑不仅对遗留系统至关重要,对一个新系统如何组建开发团队、团队之间如何沟通协作也是至关重要的,后面我专门用一节课为你详细展开。
## 小结
今天我们学习了遗留系统的四个现代化。
也许你已经发现了,这样做本质上就是**将先进的、现代化的软件开发方法应用到遗留系统上**,让遗留系统重获新生、保持活力。是的,日光之下并无新事。遗留系统之所以成为遗留的,就是因为既缺乏现代化的软件开发方法,又没有随着潮流的发展而不断演进。
![](https://static001.geekbang.org/resource/image/c2/e4/c2c9b09b10109abec6189df04a8b5ee4.jpg?wh=9470x7437 "遗留系统的四个现代化")
遗憾的是,这里还应该引入一个“需求现代化”,但是在权衡之后我将它删除了。因为一个企业里的需求方与开发方是不同的部门,要想进行需求的现代化,必然要让需求部门参与进来。然而国内很多企业的需求部门和开发部门,还无法亲密无间地展开合作。我们甚至有信心对开发部门内部的团队结构进行重组,但却没信心让需求人员改变工作习惯。
无论如何在做到代码、架构、DevOps、团队结构四个现代化之后遗留系统的现代化之路就算基本成功了。不过在着手对这四个方面进行治理之前我们还需要先掌握遗留系统现代化的三个原则。即
1.以降低认知负载为前提
2.以假设驱动为指引
3.以增量演进为手段
这是我在工作中总结出来的,我们在遗留系统现代化中的许多举措,都符合这三个原则。忽视了它们,四个现代化之路很可能背道而驰。[下节课](https://time.geekbang.org/column/article/507513)我们就从修改需求的场景出发,聊一聊为啥要遵循“以降低认知负载为前提”这个原则。
## 思考题
感谢你学完了这节课的内容,今天的思考题是这样的:
你所在的遗留系统架构是什么样的?如果是单体架构,是否打算将其拆分为微服务?打算怎么拆?如果是分布式架构(不一定是微服务),是如何运维的?
欢迎你在留言区留下你的思考,我们一起交流讨论。