破解遗留系统重构问题的 6 步心法
你好,我是 Thoughtworks 团队的技术教练黄俊彬,目前主要在智能硬件、通讯互联网、金融等领域企业提供敏捷转型、系统架构改造以及大型遗留系统重构等服务。今天给你分享的话题是 MV 模式的重构演进。
我会从以下四个部分来分享,第一部分给大家分享遗留系统典型特征,介绍这类系统的特征以及对团队的影响。第二部分是MV*模式重构的策略,针对这类系统里面的一些特征,我们有哪些重构的策略及方法来进行改造跟优化。第三部分是 MV模式的重构示例。第四部分是对整次的分享的小结。
我在工作中遇到过很多遗留系统,总的来说有两个很明显的特征。第一,从整个工程上来看它是呈大泥球的架构,通常代码是百万行起而且随意依赖。
大泥球架构
基于这样的循环,雪球就越滚越大,导致系统越腐化越严重。如此一来严重影响团队的业务和研发效率。通常情况下,一开始可能会采取不断垒人的形式去解决问题,但是随着规模越来越大,复杂度越来越高,单纯通过人海战术已经没办法解决系统的问题。
基于此,针对遗留系统里的 ALL In Class 上帝类,我们通过 MV* 的模式进行重构,解决整个代码的抽象设计和复杂度以及没有任何单元测试和分层设计的问题。
MV* 常见的有三种模式。第一种是 MVC,它主要分为三个部分,View、Controller 和 Model。用户通过 View 操作会触发 Controller 的逻辑,Controller 会调用模型做一些数据,然后通过模型触发 View 数据的变化,这是 MVC 的模式。
MVC
MVC第二种是 MVP 模式,MVP 模式跟 MVC 模式的主要的区别是 MVP 模式中 View 跟 Model 没有直接交互,它们是通过中间的Presenter以及一些接口的设计来达到双边的交互。
MVP
第三种是 MVVM 模式,我们可以跟 MVP 模式对比,它们主要的区别是 MVVM 中 Model 跟 View 这层通过 Data Binding 的机制让它能够自动做绑定从而简化通讯接口的设计。
MVVM
谈到重构大家应该非常熟悉一本书叫《重构改善既有代码的设计》,书中提到重构的定义,认为重构是对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下提高可理解性并降低修改成本。
重构
但是实际在这个项目里落地重构时,我们通常会遇到很多挑战。第一个挑战是没有时间重构,开发同学经常会提到这个问题。第二个挑战是重构以后背锅怎么办?这个问题使得很多同事不愿做重构。第三个挑战是重构以后又变成了一个遗留系统,这也是我们经常遇到的情况。
针对这三个挑战,我给大家介绍一些之前项目落地的经验。
首先第一个问题,没有时间进行重构。其实我们更应该把重构按不同的修改范围去分类,在这里我们将重构分为了三种类型:小型重构、中型重构和大型重构。
重构时机
小型重构的修改范围主要针对的是对单个类内部的重构优化,比如一些非常基础的重命名、提取变量、提取函数等操作,我建议这种重构随时进行。
中型重构主要修改的是多个类间的重构优化,比如提取接口、超类、委托等操作。此刻分享的 ALL In Class 重构到 MVP 的模式就属于中型重构。我建议这类型重构的时机是在开发新需求或者修复 Bug 的时候,预留时间做重构优化。
大型重构是对系统组件架构进行重构优化,比如一些单体应用,或者微服务的架构等,这一类型的重构涉及范围非常大,通常会通过项目上立专项来落地。
其实这里隐含了两个问题,第一个问题是重构的安全性,第二个问题是我们怎么更高效地做这个事情。下面介绍四种非常有用的实践。
结对重构
①结对重构
这种形式非常好地避免了修改错误的问题。我们可以找相对熟悉这块业务的开发同事一起来做结对。
完全自动化
②实践尽可能完全自动化
通过 IDE 安全重构的方式减少人工操作代码或者改代码的错误,这种方式能够非常好地保证安全性以及提高效率。
保护测试
③保护测试在重构的过程中频繁通过自动化测试做守护,当然,对遗留系统来说编写单元测试是非常难的,所以我们可以通过编写一些中大型验收的首步测试来做一层基本的防护。
小步前进
④小步前进一次解耦尽可能做一次提交,小步前进方便代码的回滚。
做整个重构时应该确定一些相关度量,例如针对 ALL In Class 重构到 MV* 的模式,其度量可能有几个维度,比如重构完以后架构模式是否符合新的 MV* 模式,这一点非常重要,不能做了重构以后变样了。
其次我们也可以通过其他的度量维度衡量重构的效果。比如代码质量、圈复杂度、代码的重复率等,以及通过重构以后是不是会得到有效的收敛。最后是一些有效的自动化测试,通过重构以后这些用例数、覆盖率是不是得到有效的提升。让我们以始为终,不要重构完以后达不到我们最初设想的预期的效果。
整个重构流程的方法针对 MV* 模式我们主要分为三层,总共有六个步骤。
重构流程方法
首先需要去梳理原有的业务逻辑,这一点非常重要,因为很多遗留系统的业务和逻辑都是有一些缺失的,所以第一步要去梳理业务逻辑。第二个是分析原有的代码设计,从中识别一些代码设计的问题。第三个,开始动手做安全重构时,建议先补充守护测试,做一层简单的防护网。
第四个是针对接下来要重构的模式要做一些简单的设计,比如ALL In Class要做MV*的模式,我们应该针对MV*模式的架构的分层特点做一些对应的简单设计。第五个是小步安全重构,结合IDE充分进行安全重构。第六个是做完以后我们要做集成验收。以上就是针对MV*模式整个重构流程的六个步骤。
下面通过一个实际案例演示如何通过这六个步骤将 ALL In Class 的代码坏味道安全重构至 MV* 的模式。
这个示例很简单,作为一个云存储的用户,我希望有一个专门的文件管理页面以便我能便捷浏览及操作文件,简单来说就是一个文件模块上面能够显示文件列表。但是刚刚也提到第一步是非常重要的,梳理原有的业务逻辑听起来简单,但是在过程中可能隐含很多业务细节和业务逻辑我们是不清楚的,所以第一步我们要去梳理原有的业务逻辑。
下面推荐三种方式。第一种是找人,我们可以找产品经理、设计以及测试人员进行流程上的确认和答疑。第二种,我们可以通过文档原有的需求设计文档测试用例、设计稿等,帮助我们理解原有的业务。第三种,通过分析原有代码中的逻辑边界梳理业务。下图就是刚刚我们进一步梳理细化出来的逻辑。
可以看到进入页面时,是从网络加载文件列表的。刚刚的图里面我们只看到了正常文件列表的显示,但这个流程图里面有很多异常条件。
比如说数据是否加载成功?如果加载成功就显示文件列表,也显示文件名跟文件大小。但是如果网络异常就会显示 NetworkErrorException,当用户点击这个提示的时候,它会重新加载这个文件列表,重新触发数据是否加载成功的判断。如果数据为空,它会显示 empty data 的提示。同样,当点击提示的时候会重新触发网络加载。
通过梳理业务逻辑我们可以更好地挖掘边界条件或者异常条件,避免做重构时遗漏原有逻辑。
接下来展示这个页面的代码片段,实际所有的逻辑都在一个 ALL In Class 的大类里。第一个部分是获取文件列表。
获取文件列表
我们可以来细看代码,通过 getFileList() 的方法,里面利用了 Thread() 的线程,通过 fileController 远程去拿文件数据,拿到以后,通过 Handler 里的一些判断去发送消息。
数据处理
第二部分是数据的处理,Handler 收到消息以后会做信息类型的判断,做数据的解析和格式化,最后通过 View 把它显示到网格或者列表上,这个消息是异常的话,会显示异常的提醒。
数据展示
最后是数据展示,通过标识和数据的判断控制视图的显示。以上是文件列表原有代码的片段,我们从这个代码片段中可以分析原有代码里的一些设计问题。
主要有以下五个问题:
主要的获取文件、异常逻辑判断、界面刷新控制都是在一个类里面,不利于后续的扩张及修改维护
存在粗暴的 new Thread 进行管理
Handler存在内存泄露风险
存在规范问题,例如 empty data 字符串没有使用 xml 进行管理、代码中有无效的导包等
代码中没有任何守护测试
测试的整体的策略是将用户核心的业务操作自动化,我们在做重构时,修改完代码可以频繁地通过测试做验证,避免我们修改出新的问题。针对前面的业务我们梳理出总共存在三种场景。
当用户进入文件页面,正常请求到数据,显示文件列表
当用户进入文件页面,但网络异常,显示异常提示
当用户进入文件页面,但数据为空,显示空提示
针对前面这种场景,我们编写了下面三个用例。当涉及到一些异步网络请求,采用 Mock 的形式将它 Mock 掉来模拟实际的业务场景。
正常显示
正常显示正常显示的情况下,当我们这个页面启动的时候,页面上应该成功显示这个文件的信息。
网络异常
当网络异常的时候,进入文件列表页面的时候应该显示异常的信息提示。
数据为空
当进入页面数据为空的时候,应该显示空数据提示。
守护测试执行结果
每一次进行重构我们都可以频繁验证运行这个测试,如果测试通过,就证明本次重构没有影响主流程的业务。如果出现异常,就要查看是不是这次重构对原有的业务逻辑造了影响。
有了这层守护测试以后,在开始进行编码工作之前,我们要对重构模式做简单的设计。
MVP 架构模式
上图是 MVP 架构的一种模式,分为 View、Model 和 Presenter。View 和 Presenter 中间主要通过 interface 接口形式来做交互,这种架构能够让业务逻辑跟视图分离,Presenter 和 View 之间通过接口交互。
结合 MVP 模式设计接口
结合这种模式,我们把基本的交互接口定义出来。上图中我们定义了一个 FileListContract,里面定义了 View 的接口,主要包含 ShowFileList(ListfileList) 显示文件的列表,showNetWorkException(String errorMessage) 显示网络异常信息,showEmptyData() 显示空数据以及 Presenter 获取文件列表的接口,最好有一个 FileDataSource 仓储数据的接口。通过简单设计我们结合新的模式把接口定义出来。
接下来进行小步安全重构。在这个过程中,需要我们结合整个 IDE 的安全重构进行重构。
重构流程主要有三个步骤:
抽取 FileFragment 的业务逻辑到 FilePresenter
FileFragment 提取 View 接口
抽取 FileDataSource 作为数据源管理
整个过程涉及到的重构方法:提取接口、移动方法、移动类、抽取方法、内联、提取变量等
操作详细的代码操作演示我为大家专门录制了一个代码演示视频,如果你感兴趣,可以观看整个代码演示的过程:https://time.geekbang.org/qconplus/detail/100110438
重构后 View 层代码变化
当我们重构完以后,我们可以看到重构之后的代码和重构之前的代码对比。首先是重构以后 View 层的代码变化,之前是 Fragment 作为上帝类,所有的逻辑都融合在一起,当我们重构完以后,它只继承对应 View 的接口,只是单纯作为 View 数据的展示判断,并不糅合其他的业务逻辑,网络操作的逻辑。重构完以后业务层的代码抽取到独立的 Presenter 里面,也通过 RxJava 解决之前 new Thread 线程管理,将独立的业务逻辑存放在 Presenter 类里。
当我们做完重构以后,接下来要做整体集成的验收。
验收包括三部分:
组件及集成编译构建通过
验收及架构守护自动化测试通过
运行检查测试通过
以上是完整示例,通过六个步骤演示了整个 MVP 模式的安全重构。
本文主要讲了四个部分,第一部分分享了遗留系统典型特征,主要是大泥球架构、ALL In Class,还分享了遗留系统对整个团队及业务的影响。第二部分主要讲了 MV* 模式重构的策略,这里面主要包含了重构的时机,包含小型、中型、大型的重构,也给大家分享了如何在重构过程中更好地做保障。
第三个部分通过一些度量反馈,让我们以始为终更好地达到重构的目的。最后梳理了整个 MV* 模式重构的流程方法,通过一个完整的示例给大家演示了这六个步骤。
最后,给你分享一句话,重构是日常开发的一部分而不是最后的补救措施。以上是本文的全部内容,如果有任何的问题欢迎在文章下方留言和我一起讨论,如果你有相关的技术经验也欢迎通过留言分享出来我们一起精进。
黄俊彬
Thoughtworks 中国区 DTO 团队敏捷技术教练、9 年移动端开发经验,某头部⽹盘 Android 客户端开发与性能优化、基础组件研发及 SDK 设计,某手机厂商 OS 解耦及敏捷转型等项⽬。在移动开发领域的应⽤性能优化、自动化测试、架构设计及组件化等⽅向有丰富的经验。目前主要在智能硬件、通信、互联网、金融等领军企业提供敏捷转型、性能优化、系统架构改造、大型遗留系统重构等服务。
遗留系统一直是技术领域的“重灾区”,想要深入剖析遗留系统的特点和问题,推荐你学习这门专栏,它将从代码、架构、DevOps 和团队现代化四大方向,解决遗留系统治理的疑难杂症,帮你和所在团队走出遗留系统的泥潭。使用口令现仅 7 折入手,等你来品鉴!
微信扫码关注该文公众号作者