Day962.如何更好地重构和组织后端代码 -遗留系统现代化实战

news2024/12/25 9:18:03

如何更好地重构和组织后端代码

Hi,我是阿昌,今天学习记录是关于如何更好地重构和组织后端代码的内容。

如果说在气泡上下文中开发新的需求,类似于老城区旁边建设一个新城区,那么在遗留系统中开发新的需求,就类似于在老城区内部开发新的楼盘。

这就必然要涉及到拆迁的问题。拆迁终归是一个声势浩大的工程,居民要先搬到别的地方,再拆除旧的建筑,盖起新的楼宇,一番折腾之后,老居民才能搬进新家。

不过软件的好处就在于它是“软”的,不需要这么费劲儿。

可以很容易地复制、删除和添加新的代码,轻松地实现一个架构的变迁。


一、修缮者模式

绞杀植物模式适合于用新的系统和服务,替换旧的系统或旧系统中的一个模块。

在旧系统内部,也可以使用类似的思想来替换一个模块,只不过这个模块仍然位于旧系统中,而不是外部。把这种方式叫做 修缮者模式

在这里插入图片描述

在修缮时,通过开关隔离旧系统待修缮的部分,并采用新的方式修改。

在修缮的过程中,模块仍然能通过开关对外提供完整功能。

这就好比是在老城区中修路,如果断路施工对交通的影响就太大了。

更常见的做法是修缮其中的半条路,留另外半条来维持交通。不过,这必然会造成一定的拥堵。但在软件中就好办多了,可以将道路(待修缮的模块)“复制”出来一份,以保障通行正常。等原道路修缮好之后,再删除掉复制出来的道路即可。

用修缮者模式去修复过一个性能问题。一个 API 的请求特别慢,在本地修好后,在生产环境改观不大。

推测这应该是数据分布导致的问题,本地环境的数据分布无法准确模拟生产环境。但当时的安全策略不允许访问生产数据库。

于是,接下来做调优时,并没有直接修改这个 API,而是将 API 复制了一份出来,一个用来维持老的功能,一个用来性能调优。

同时添加了一个针对这个 API 的 Filter,根据开关来决定要调用哪个 API。通过收集调优 API 中的日志,不断地优化,直到解决性能问题。

这时再清理掉旧 API、Filter 和开关。这样做的好处是,由于你无法预测修缮过程中会产生哪些问题,这种通过开关保留回退余地的方法,显然是更灵活的。

如何实现前端的增量演进和随时回退,其实也是这种修缮者模式的思想。

将所有要修改的页面复制出来一份,然后再加入开关,就可以放心地重构页面了。

在没有单元测试的情况下,通过修缮者的方式来重构的。把代码复制出来,重构完之后,通过开关在调用端切换,以完成 A/B 测试,从而实现安全地重构。

// 旧方法
public List<int[]> getThem() {
 List<int[]> list1 = new ArrayList<int[]>();
 for (int[] x : theList)
   if (x[0] == 4)
    list1.add(x);
 return list1;
}
// 新方法
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();
  // 其他代码
}

二、抽象分支

这种优雅的方式就是,把要重构的方法重构成一个方法对象,然后提取出一个接口,待重构的方法是接口的一个实现,重构后的方法是另一个实现。按这种方式重构之后的代码如下所示:

public interface CellsProvider {
  List<int[]> getCells();
}

public class OldCellsProvider implements CellsProvider {
  @Override
  public List<int[]> getCells() {
    List<int[]> list1 = new ArrayList<int[]>();
    for (int[] x : theList)
      if (x[0] == 4)
        list1.add(x);
    return list1;
  }
}
public class NewCellsProvider implements CellsProvider {
  @Override
  public List<int[]> getCells() {
    return gameBoard.stream().filter(c -> c.isFlagged()).map(c -> c.getArray()).collect(toList());
  }
}

在调用端,只需要通过工厂模式,来根据开关得到 CellIndexesProvider 的不同实现,其余的代码都保持不变。在通过 A/B 测试之后,再删除旧的实现和开关。

这种方法不但可以进行安全地重构,还可以用新的实现替换旧的实现,完成功能或技术的升级。把这种模式叫做抽象分支(Branch by Absctration)。

当进行大的技术改动时,通常需要花费较长的时间。比如用 MyBatis 替换 Hibernate,或用 Kafka 替换 RabbitMQ。

传统的做法是,在当前的产品代码分支上创建一个新的分支,大规模去重写。

这个分支发布之前要经历很长一段时间,直到最后全部修改完成后,才能把分支合并到产品代码分支上。

更糟糕的是,这样做合并时的代码冲突会非常严重,而且架构调整后,首次上线大概率会出问题,交付风险非常高,无法做到增量演进。

为了解决这样的问题,Martin Fowler 提出了抽象分支模式。

可以在不创建真实分支的情况下,通过技术手段,将大的重构项目分解成多个小步骤,每个小步骤都不会破坏功能,都是可以交付的,这样就可以逐步完成架构的调整。

在这里插入图片描述

它的基本步骤是这样的。先为旧实现创建一个抽象层,让旧的模块去实现这个抽象层。

注意,这里的抽象层并不一定是接口,有可能是一系列接口或抽象类。

然后,让部分调用端代码依赖这个抽象层,而不是旧的模块。

同样要注意,这个替换是逐步进行的,不是一次性全部替换掉。

等全部调用端都依赖抽象层后,开始编写新的实现,并让部分模块使用新的实现。

这个过程也是逐步进行的,一方面可以更好地验证新实现,另一方面也可以随时回退。

当全部调用端都使用新的实现后,再删除旧的实现。

有的时候你需要让新旧实现同时存在,对不同的调用端提供不同的实现,这也是很常见的情况。

由于新代码一直可以工作,因此你可以不断提交、不断交付、不断验证。

在实际工作中,抽象分支的运用还是非常广泛的。一个技术改动,在初始化 Redis 的时候,改为从配置文件中读取密码,而不是从数据库中读取密码。

对于这样一个替换,可能直接三下五除二就完成了,但领悟了抽象分支之后,发现可以用更加优雅的方式实现这个替换。一篇博客,可以当做加餐。


三、扩张与收缩模式

有的时候要修改的是接口本身(这里的接口是指方法的参数和返回值),这时候就不太容易通过抽象分支去替换了。

以前返回的是 List,而现在想打破这个接口,返回 List。

因为 List 仍然存在严重的基本类型偏执的坏味道,而且本来已经提取了 Cell 类,又通过 getArray 返回数组,简直是多此一举。

这时可以使用扩张 - 收缩(expand-contract)模式,也叫并行修改(Parallel Change)模式。它一般包含三个步骤,即扩张、迁移和收缩。

这里的扩张是指建立新的接口,它相比原来旧的代码新增了一些东西,因此叫做“扩张”;而收缩是指删除旧的接口,它比之前减少了一些东西,因此叫“收缩”。

一般来说,它会在类的内部新建一些方法,以提供新的接口(即扩张),然后再逐步让调用端使用新的接口(即迁移),当所有调用端都使用新的接口后,就删除旧的接口(即收缩)。

拿刚才这个例子来说,提取完方法对象后的代码如下所示:

public class CellsProvider {
  public List<int[]> getCells() {
    List<int[]> list1 = new ArrayList<int[]>();
    for (int[] x : theList)
      if (x[0] == 4)
        list1.add(x);
    return list1;
  }
}

可以在这个方法对象中进行扩张新增一个方法,以提供不同的接口:

public class CellsProvider {
  public List<int[]> getCells() {
    // 旧方法
  }
  public List<Cell> getFlaggedCells() {
    return theList.stream().filter(c -> c.isFlagged()).collect(toList());
  }
}

然后,让调用端都调用这个新的 getFlaggedCells 方法,而不是旧的 getCells 方法。

在替换的过程中,新老方法是同时存在的,这也是为什么这个模式也叫并行修改。

等所有调用端都修改完毕,就可以删掉旧方法了。

在这里插入图片描述

在老城区改造的过程中,这种扩张与收缩模式也是很常见的。城市完成了一次取暖线路改造,从以前的小区锅炉房供暖改成了全市的热力供暖。

施工方并没有将小区内旧的供暖管道直接连到市政热力的管线上,而是在旧的管线旁边新铺了一条管线(即扩张),连接到市政管线。

在供暖期,两条管线是并行运行的,一旦新管线发生问题,可以很快地切回旧的小区供暖。等并行运行一段时间后,判断新管线没问题了,再重新挖沟,拆除旧管线(即收缩)。

有的时候市民不理解为什么天天挖坑,但实际上这么做,都是为了保障供暖的安全性和高可用性啊。


四、再谈接缝

在抽象分支中,我们提取的接口其实是一个接缝

没错,接缝不但可以用来在测试中替换已有的实现,它本身其实也是一个业务变化的方向。

在开发过程中,需要时刻去关注接缝,关注这种可能会产生变化的地方。

比如项目中使用了 RabbitMQ 作为消息中间件,发送和接受消息的代码和 RabbitMQ 的 SDK 紧密耦合,这会带来两方面隐患,一方面当你想替换 MQ 的时候,需要修改全部调用点,另一方面,它也不好写测试。

当意识到它其实是一个接缝的时候,就可以很轻松地通过一系列接口来隔离 SDK。

当需要替换 MQ 的时候,只需要提供一套新的实现类。这时的实现类应该叫做适配器(Adaptor),它其实也起到了防腐层的作用。而在单元测试中,可以通过测试替身构建一组 Fake 的实现类,以提供内存中的 MQ 功能。这样的方案,既优雅又灵活。除了代码中蕴含着很多接缝,架构中也存在接缝。

延续上面 MQ 替换的例子,因为有很多在途的消息还没有处理,这种技术迁移很难做到不停机地丝滑切换。

这时可以利用这个架构接缝,使用事件拦截模式,将发往 RabbitMQ 中的消息也同步发给新的 MQ(比如 Kafaka)。

同时,消费端可以通过幂等 API,来消除重复消费造成的问题。这样一来,系统中就有两个消息中间件同时存在,同时提供消息机制。

当基础设施搭建好之后,就可以实现新老 MQ 的无缝切换了。


五、总结

  • 修缮者模式和绞杀植物类似,可以用来改善单体内的某个模块。
  • 抽象分支模式可以通过一个抽象,优雅地替换旧的实现。
  • 扩张收缩模式主要用于接口无法向后兼容的情况,一张一缩,一个接口就改造完了。
  • 同时,除了代码中的接缝,架构中也存在接缝,可以利用它们来实现架构中的替换。

无论是绞杀植物、修缮者、抽象分支还是扩张收缩,它们在实施的过程中,都允许新旧实现并存,这种思想叫做并行运行(Parallel Run)。这是贯彻增量演进原则的基本思想,希望能牢牢记住。

说的绞杀植物、气泡上下文、修缮者、抽象分支、扩张收缩、并行运行等模式,其实概念上都差不多,之所以叫不同的名字,是因为它们解决的是不同的问题。

比如绞杀植物模式解决的是新老系统的替换,修缮者模式解决的是一个服务内部模块的替换,而气泡上下文专门用于将新需求和老系统隔离开来。

在这里插入图片描述

这就像不同的设计模式虽然叫不同的名字,但构造型模式用来解决不同场景下的对象构造,行为型模式用来处理不同场景下的行为选择。

必须深刻理解这些模式,才能做出正确的选择。

最后,王健对于各种模式的高度抽象,他的十六字心法如余音绕梁,三日不绝。

旧的不变,新的创建,一步切换,旧的,再见。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/493167.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

c++的构造函数与析构函数

构造函数是一种特殊的成员函数&#xff0c;用于在对象创建时初始化对象的成员变量。它的名称与类名相同&#xff0c;没有返回类型&#xff0c;可以有参数。当创建对象时&#xff0c;构造函数会自动调用&#xff0c;以初始化对象的成员变量。如果没有定义构造函数&#xff0c;编…

华为OD机试真题-24点运算【2023】【JAVA】

一、题目描述 计算24点是一种扑克牌益智游戏&#xff0c;随机抽出4张扑克牌&#xff0c;通过加()&#xff0c;减(-)&#xff0c;乘(*), 除(/)四种运算法则计算得到整数24&#xff0c;本问题中&#xff0c;扑克牌通过如下字符或者字符串表示&#xff0c;其中&#xff0c;小写jo…

PCL1.12.0+Vtk7.1.1安装

1. qt4&#xff1a;Ubuntu 20.04 LTS 安装qt4 library_ubuntu20.04安装qt4 2.本文下载过程可参考1&#xff1a;ubuntu20.04下安装pcl_ubuntu安装pcl_Yuannau_jk的博客-CSDN博客 参考2&#xff1a;Ubuntu 20.04.05安装PCL-1.12.0_no package metslib found_zhiTjun的博客-CSDN…

解决 IDEA中的Tomcat服务器控制台乱码

解决 IDEA中的Tomcat服务器控制台乱码 问题描述&#xff1a;当我们使用idea编辑器部署web程序到tomcat服务器上&#xff0c;当我们运行tomcat的时候控制台出现服务器输出内容乱码的情况&#xff0c;这个问题可能是由于编码不一致引起的。在IDEA中&#xff0c;如果项目的编码方…

HttpServletRequest在Spring中的获取和注入 @Autowired注入Request

问题描述&#xff1a; 在最近一次团队review代码时&#xff0c;团队成员发现有将HttpServletRequest 直接通过Autowired注入的情况&#xff0c;于是大家产生了一个疑问&#xff0c;HttpServletRequest并非Spring中的类&#xff0c;且在没有手动通过Bean的方式注入&#xff0c;…

Oracle数据库、实例、用户、表空间、表之间的关系

数据库&#xff1a; Oracle数据库是数据的物理存储。这就包括&#xff08;数据文件ORA或者DBF、控制文件、联机日志、参数文件&#xff09;。其实Oracle数据库的概念和其它数据库不一样&#xff0c;这里的数据库是一个操作系统只有一个库。可以看作是Oracle就只有一个大数据库。…

Vue核心 绑定样式 条件渲染

1.11.绑定样式 class样式&#xff1a; 写法&#xff1a;:class“xxx”&#xff0c;xxx 可以是字符串、数组、对象:style“[a,b]” 其中a、b是样式对象**:style“{fontSize: xxx}”**其中 xxx 是动态值 字符串写法适用于&#xff1a;类名不确定&#xff0c;要动态获取数组写法…

HTB靶机07-Cronos-WP

cronos IP&#xff1a;10.10.10.13 scan ┌──(xavier㉿kali)-[~] └─$ sudo nmap -sSV -T4 10.10.10.13 Starting Nmap 7.93 ( https://nmap.org ) at 2023-04-06 23:19 CST Nmap scan report for 10.10.10.13 Host is up (0.23s latency). Not shown: 997 closed tcp por…

SpringCloud全面学习笔记之进阶篇

目录 前言微服务保护初识Sentinel雪崩问题及解决方案雪崩问题超时处理仓壁模式熔断降级流量控制总结 服务保护技术对比Sentinel介绍和安装微服务整合Sentinel 流量控制快速入门流控模式关联模式链路模式小结 流控效果warm up排队等待 热点参数限流全局参数限流热点参数限流案例…

算法记录 | Day52 动态规划

300.最长递增子序列 思路&#xff1a; 1.dp[i]的定义:以 nums[i] 结尾的最长递增子序列长度。 2.状态转移方程:位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 1 的最大值。 if (nums[i] > nums[j]) dp[i] max(dp[i], dp[j] 1); 注意这里不是要dp[i] …

基于AT89C52单片机的电子秒表设计与仿真

点击链接获取Keil源码与Project Backups仿真图&#xff1a; https://download.csdn.net/download/qq_64505944/87755619?spm1001.2014.3001.5503 源码获取 主要内容&#xff1a; 本设计以AT89C52单片机为核心&#xff0c;采用常用电子器件设计&#xff0c;包括电源开关、按键…

网络安全 等级保护 网络设备、安全设备知识点汇总

网络设备、安全设备知识点汇总 1、防火墙&#xff08;Firewall) 定义:相信大家都知道防火墙是干什么用的&#xff0c; 我觉得需要特别提醒一下&#xff0c;防火墙抵御的是外部的攻击&#xff0c;并不能对内部的病毒 ( 如ARP病毒 ) 或攻击没什么太大作用。 功能:防火墙的功能…

coturn中turnutils_peer和turnutils_uclient使用说明

coturn的作用有两个&#xff1a;寻找反射地址以及流转发&#xff0c;本人写过webrtc janus服务器部署在公网&#xff0c;coturn转发媒体流 coturn下面的工具turnutils_stunclient用于查找反射地址。 而turnutils_peer和turnutils_uclient用于测试转发功能&#xff0c;再次给以…

STL中priority_queue自定义类型使用和源码简单分析

priority_queue使用 这里说一下优先级队列的其他的用法,这里我们先看默认的究竟是建立大堆还是小堆? #include <iostream> #include <queue>int main() {int arr[] { 10, 2, 1, 3, 5, 4, 0 };std::priority_queue<int> q;for (size_t i 0; i < sizeo…

基于springboot的私人健身与教练预约管理系统

摘 要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#xff0c;各行各业相继进入信息管理时代…

计算机网络学习笔记-概述

目录 信息时代 互联网 因特网发展的三个阶段 制定互联网的正式标准阶段 互联网组成&#xff1a;边缘部分核心部分 边缘部分 核心部分 计算机网络 体系结构 OSI 七层参考模型&#xff1a;物理层 数据链路层 网络层 运输层 会话层 表示层 应用层 TCP/IP 4层参考模型&a…

【K8S系列】深入解析k8s网络

序言 你只管努力&#xff0c;其他交给时间&#xff0c;时间会证明一切。 文章标记颜色说明&#xff1a; 黄色&#xff1a;重要标题红色&#xff1a;用来标记结论绿色&#xff1a;用来标记一级论点蓝色&#xff1a;用来标记二级论点 Kubernetes (k8s) 是一个容器编排平台&#x…

(8) 支持向量机分类器SVC案例:预测明天是否会下雨

文章目录 案例介绍1 导库导数据&#xff0c;探索特征2 分集&#xff0c;优先探索标签3 探索特征&#xff0c;开始处理特征矩阵3.1 描述性统计与异常值3.2 处理困难特征&#xff1a;日期3.3 处理困难特征&#xff1a;地点3.4 处理分类型变量&#xff1a;缺失值3.5 处理分类型变量…

Typora + PicGo + Gitee 搭建免费图床

搭建准备 本次搭建过程需要以下介质&#xff1a;Typora PicGo Gitee/GitHub &#xff0c;「免费&#xff01;」 Typora Typora 是一款 markdown 编辑器&#xff0c;支持几乎所有的 markdown 格式&#xff0c;神器&#xff01; 支持 macOS、Windows、Linux 三种操作系统&am…

Composition API 的优势、新的组件(Fragment,Teleport,Suspense)【Vue3】

四、Composition API 的优势 1. Options API 存在的问题 使用传统OptionsAPI中&#xff0c;新增或者修改一个需求&#xff0c;就需要分别在data&#xff0c;methods&#xff0c;computed里修改。 2. Composition API 的优势 我们可以更加优雅的组织我们的代码&#xff0c…