Day950.遗留系统的四化建设 -遗留系统现代化实战

遗留系统的四化建设

Hi,我是阿昌,今天学习记录的是关于遗留系统的四化建设的内容。

对于老旧、过时,但又十分重要、不可替代的遗留系统,是遗留系统。听之任之只会埋下隐患,真正出现问题就为时已晚了。在动手改造遗留系统之前,先要找准方向。其实相比遗留系统“治理”、“改造”,更强调的是“现代化(Modernization)”,也就是把遗留系统变为现代化的系统。这也是国际上更通用的提法。用“Legacy System Modernization”这个关键词,在 Google 上能搜到 1380 万条结果。很多团队在对遗留系统进行“改造”或者“现代化”的时候,往往会陷入一个误区,就是盲目引入各种时髦的新技术,仿佛“新”就代表着“好”,就代表着方向正确。

比如耳熟能详、近年来愈发流行的微服务架构,有些团队也不管自己的项目适不适合,上来就把一个“大泥球”式的遗留系统肢解成了几十个微服务。更有甚者,一个遗留系统拆成了几百个微服务,有些甚至一张表的“增、删、查、改”居然被拆成了四个服务。架构似乎“现代化”了,运维人员却“哭”了。

那遗留系统现代化的正确方向到底是什么呢?

遗留系统在代码、架构、测试、DevOps 方面存在诸多问题,在此基础上,将代码和测试合并(因为它们说的都是代码的质量),并引入开发团队这个维度,就得到了遗留系统现代化的四个方向:

  • 代码现代化
  • 架构现代化
  • DevOps 现代化
  • 团队结构现代化。

一、代码现代化

代码现代化顾名思义,就是把遗留系统中丑陋的“祖传”代码重构成职责清晰、结构良好的优质代码。

遗留系统中的代码是“祖传”的,是因为它和其他祖传的东西类似,都是历史悠久、且不敢轻举妄动的。而之所以不敢轻举妄动,就是因为缺乏测试,无法快速验证修改的正确性。而大多数情况下,之所以没有测试,又是因为代码写得不可测。可测试的代码和代码的测试是相互依存的,其中一个做到了,另一个也很容易做到,而如果其中一个没有做到,另一个也必然无法做到。因此代码现代化的首要任务,就是对遗留系统的代码进行安全的可测试化重构。

在正常情况下,重构应该是在充分的自动化测试的保护下进行的。但对于没有测试的代码,我们只能“硬着头皮”去做一些相对来说比较安全的重构,将代码重构成可以写测试的程度,然后再补上大量的测试,进而在有充分测试覆盖的情况下,进行更广泛更深入的重构。

下面的代码,想测试 if 的逻辑,当 Dao 的方法返回一个 null 时,这段代码会抛出一个异常。


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 的依赖是固定的,无法解耦的。

要知道在单元测试中,是不可能直接访问真实的数据库的,因此要想测试这样的方法,只能先对它进行可测试化重构,也就是先将它重构为可测试的代码。什么样的代码叫可测试的呢?比如下面这样:

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 这个方法就不会直接访问数据库,它就变成可测试的了。

比如传入这样的一个子类:

public class InMemoryEmployeeDao extends EmployeeDao {
  @Override
  public Employee getEmployeeById(long employeeId) {
    return null;
  }
}

这样,想测试原方法中 if 的代码逻辑就非常方便了。这里使用的重构手法叫做提取接缝(Extract Seam),至于什么是接缝,以及还有哪些可测试化重构的手法先按下不表,当代码可测了,就可以为它们添加足够的测试,提供质量保障。

然后,在测试的保障下进行安全的重构。接下来要做的就是将“祖传”代码重构得让人耳目一新。当代码结构良好了,再实现下一个代码现代化的目标,也就是良好的分层结构。


二、架构现代化

遗留系统现代化的第二个方向是架构现代化。看到“架构现代化”这几个字,很自然地就想到了微服务架构或云原生架构。然而前面说过,新不代表正确。在团队的开发能力、DevOps 能力和运维能力不足的时候,引入微服务,反而会将团队推向更痛苦的深渊。

有时候常常把软件系统比作一个城市,把系统架构和城市建设做类比。随着城市的发展和扩张,以前处于城市边缘的农村,反而会被周围新建的高楼大厦包裹成为一个城中村。治理这些城中村,就叫“改造老城区”。有时候老城区的设计和规划会暴露出一些问题,不足以满足城市的发展。比如市政府通过一些集中的招商引资后,很多企业都要来这里建厂,但老城区显然没有足够的空间。这时候很多城市都会新建一个城区,有些地方叫开发区,有些地方干脆直接就叫新区。将这称之为“建设新城区”。


同样,遗留系统的架构现代化,也可以分成“改造老城区”和“建设新城区”两类模式。

改造老城区模式是指对遗留系统内部的模块进行治理、让模块内部结构合理、模块之间职责清晰的一系列模式。前端方面包括单页应用注入、微前端等,后端包括抽象分支、扩张与收缩等,数据库端包括变更数据所有权、将数据库作为契约等。

建设新城区模式是指将遗留系统内部的某个模块拆分到外面,或将新需求实现在遗留系统外部的一系列模式。包括绞杀植物、冒泡上下文等。为了对新建立的新城区予以各种支持,老城区还可以通过提供 API、变动数据捕获、事件拦截等各种模式,与新城区进行集成。

Day950.遗留系统的四化建设 -遗留系统现代化实战_第1张图片

只有“改造老城区”和“建设新城区”齐头并进,遗留系统架构的现代化版图才算完整。


三、DevOps 现代化

代码和架构现代化了,DevOps 的现代化也不能落后。它对项目的重要性不言而喻,如果没有现代化的 DevOps 平台,代码和架构现代化所带来的优势,就无法淋漓尽致地体现出来。假如在代码和架构优化后,需求的开发时间缩短了一倍,那么大家对于新需求上线的时间点自然也有新的期待。然而落后的 DevOps 水平反而会让这个时间变得更长,因为单体架构变成微服务了,DevOps 的难度增加了。

DevOps 的历史虽然只有短短十几年,但最近几年的发展势头却很足。大大小小的公司都开始了 DevOps 转型,很多项目都声称自己建立了持续集成流水线,但实际上很多都是只见其形不见其神,只学其表不学其里。而遗留系统的状况就更惨不忍睹了,它们几乎没有任何的自动化,或仅仅是一两句简单的构建命令。像我在第一节课里举的例子那样,在开发机上打包、靠人工用移动硬盘部署的项目还比比皆是。因此,遗留系统的 DevOps 现代化与其说是一种改进,不如说是从 0 到 1 的建设。

这一部分可以和代码、架构的治理并行,甚至可以更早。先把平台搭起来,再逐步往上添加内容。对于大多数遗留系统来说,有一个可以对代码进行构建、打包的流水线,就已经是极大的进步了。

要从头开始搭建一个 DevOps 平台,包括代码、构建、测试、打包、发布、配置、监控等多个方面。

这其中的代码和测试有一部分是和代码现代化重叠的,代码现代化的课里我会一并说给你听。剩下的几个部分再专门用一节课来详细讲述。


四、团队结构现代化

那这个团队结构现代化是个什么东西?其实很多时候,一个开发团队的结构是否合理,决定了这个团队的交付效率、产品质量,甚至项目成败,而很多人还没有对此产生足够的重视。

近年来有一本新书,叫做 Team Topologies,中文直译就是团队拓扑。一上市便引起了不小的轰动。它将团队放到了软件开发的第一位,提出了四种团队拓扑结构和三种团队交互模式四种团队拓扑包括业务流团队、复杂子系统团队、平台团队和赋能团队。

三种团队交互模式包括协作、服务和促进。我们在进行开发团队的组织结构规划时,应该参考这四种团队拓扑。去年这本书的中文版——《高效能团队模式》也已经上市了。

对于团队结构的现代化,基本上是围绕这本书的内容展开的。因为我发现,遗留系统中团队的问题,有时比遗留系统本身更大。

比如很多遗留系统可能只有一两个人在维护,在他们遇到困难的时候根本得不到团队的支持;再比如一些遗留系统的“老人”对系统比较熟悉,因此任何新启动的专项治理小组都会邀请他们加入,导致这些人的变动十分频繁,上下文切换的成本极其高昂。

团队拓扑不仅对遗留系统至关重要,对一个新系统如何组建开发团队、团队之间如何沟通协作也是至关重要的,后面我专门用一节课为你详细展开。


五、总结

本质上就是将先进的、现代化的软件开发方法应用到遗留系统上,让遗留系统重获新生、保持活力。是的,日光之下并无新事。

遗留系统之所以成为遗留的,就是因为既缺乏现代化的软件开发方法,又没有随着潮流的发展而不断演进。

Day950.遗留系统的四化建设 -遗留系统现代化实战_第2张图片

遗憾的是,这里还应该引入一个“需求现代化”,但是在权衡之后我将它删除了。因为一个企业里的需求方与开发方是不同的部门,要想进行需求的现代化,必然要让需求部门参与进来。然而国内很多企业的需求部门和开发部门,还无法亲密无间地展开合作。甚至有信心对开发部门内部的团队结构进行重组,但却没信心让需求人员改变工作习惯。

无论如何,在做到代码、架构、DevOps、团队结构四个现代化之后,遗留系统的现代化之路就算基本成功了。

不过,在着手对这四个方面进行治理之前,还需要先掌握遗留系统现代化的三个原则。

即:

  1. 以降低认知负载为前提
  2. 以假设驱动为指引
  3. 以增量演进为手段

这是在工作中总结出来的,在遗留系统现代化中的许多举措,都符合这三个原则。

忽视了它们,四个现代化之路很可能背道而驰。


你可能感兴趣的:(业务设计,运维,devops,代码规范,架构,重构)