背景

最近项目新上线的版本中,出现了很多涉及下载相关的bug;经过艰难的代码调试排查,可还是没有准确定位问题。主要还是由于代码年久,经过多代人的修修补补,到处都是“坏味道”的代码。跟项目经理沟通过后,同意重构这部分的代码。下面就简单记录一下重构的过程。

项目现状

项目最早可追溯至2014~2015年,分为配置管理网站(Server),WPF桌面软件(Client)。桌面软件又分为:用户操作界面WPF,后台服务 Windows Services 程序。
在后端Services中,维护一个本地的小型Firebird数据库,为WPF提供数据和用户状态保持;实现了从远程服务器同步数据到本地,访问远程接口下载管理数据上报等功能。而我这次要做的就是,重写下载管理。下面是关于旧代码的分析:

  1. 三种不同的下载触发机制:软件启动触发用户触发数据同步到本地触发
  2. 八种不同的资源下载,分别在不同的表中管理;
  3. 有五套复制粘贴的代码:软件启动(两套)、用户触发(两套)、数据同步(一套);
  4. 大量无意义尝试,比如下载失败404后,短时间内没有必要再次尝试;
  5. 状态管理不统一,有的下载完成状态改为 ReadyForUse,有的下载完成状态改为 DownloadSuccess;
  6. 执行方法语义重叠,比如 DownloadPause 和 DownloadStop,DownloadFinished 和 DownloadSuccess;
  7. 大段注释代码,这些代码基本永远不会再使用的;

重构设计

鉴于项目是在运行阶段,一方面要考虑时间成本,另一方面要保证原有的运行方式。在当前软件的框架下,把重写的代码限定在下载模块内部,对外的接口不做改变。
重构代码的抽象设计,也是依据当前项目的实际情况,尽可能的通用和易扩展。设计的UML类图如下:

重构设计的UML

下面的三个虚类,是下载管理流程的核心,所有的控制逻辑都包含在其中。

  1. DownloadResourceAbstract 虚类 资源文件的基本信息: DatabaseIDResourceID为了兼容不同数据库表的设计;DownloadPriority 预设优先级;DownloadImmediatelyPriority 用户触发的下载,优先级最高,立即执行;其它字段涵义可见字段名称;
  2. DownloadWorkItemAbstract 虚类 资源下载操作类,包含下载状态,开始,停止,MD5检查,状态更改的回调方法;与一对一关系;
  3. DownloadControlerAbstract 虚类 控制同时下载的数量,根据优先级控制下载的顺序;与一对多关系

下面的类,是具体下载方法的实现;当前的项目只有HTTP下载,类图中也只实现了HTTP的下载的管理功能:

  • 可扩展下载方法设计:FtpClientHttpClient继承自DownloadClientAbstract
  • 关联 HTTP 下载,分别实现的子类 HttpDownloadWorkItemHttpDownloadControler

对不同类型的资源文件,组合不同的下载方式:

  • AutoHttpDownloadDealer: 自动下载的资源 + HTTP 下载;
  • UserOperHttpDownloadDealer: 用户手动操作的资源 + HTTP 下载;
  • 由数据同步到本地的触发下载资源 + HTTP下载;(未在UML图中画出)

发现bug的真实原因

在重构测试阶段,意外发现了bug的原因:前段时间更换了文件服务器,而新的文件服务器,不支持断点续传,HTTP Status 返回的不是预期的Partial Content(206),而是OK(200);下载文件不存在时,HTTP Status 返回的也不是 404,而返回自定义的 Response Text 。。。

由于这两个不合理的地方,导致软件的下载功能,出了好多莫名其妙的bug,再叠加到处复制 + 粘贴“坏味道”的代码,修复起来异常繁琐,于是就有了重构这件事情。

个人感受

在当前的项目中,也做了几次小范围的重构,要么代码影响范围小,要么只是在原代码基础上套个框架。而这次是彻底干掉原代码,重新设计,重新写。下面是代码重写完成后 git merge 的 log:

Showing 1107 revision(s), from revision 6092a531 to revision f336c077 - 1 revision(s) selected, 166 file(s) selected; line: 5226(+) 5251(-) files: modified = 118 added = 28 deleted = 20 replaced = 0

从上面的log汇总信息,好像也不足以展示修改工作量。这些修改大约花费了我一周的时间,自愿工作日加班,自愿周末加班(有时候代码写上头了,还得强迫自己下班休息去)。开始的前三天的时间,边读旧代码,边搭建新的框架,尝试把旧代码融入到新设计中;后期转换思路,理解代码逻辑,把代码逻辑融入到新设计中

这样就可以放开手脚的干起来了,写代码的时候,感觉时间过的好快,如有神助般各种代码中的细节,也会不假思索的写出来。在下阶段单元测试中,非常顺滑,基本没有发现什么bug,真的是酣畅淋漓。

最最让我寒心的是,好不容易重构测试完代码了,压抑着无比激动的心情,希望组里的同事给个积极肯定的反馈,可连一个“卧槽”也没有,就行往常平淡的工作日。。。项目经理也是很淡然的说,要尽快拿给测试。

尾声

即使我的努力和心血没有得到,预期中的肯定,我也要把这个过程留存记录下来:把重构的代码剔除项目信息,再提取合并通用的部分,重新梳理成一个类库Github 地址;写一篇文章来记录一下。

其实,后面我细细回想了一下。如果最开始项目设计的下载功能是,把所有下载资源放在同一张数据库表中管理,是不是就那不就更简单了?如果最最早期的设计做好了,后期可以省掉多少功能扩展、修复bug、重构。