重构系列谈(三)遗留代码重构的杀器–微调手法(完)


软件系统一定不是一成不变的,一个相对完善的软件系统,能够很好应对已知的需求变化方向的变化。但在新增需求的不断冲击下,大部分情况下的不当应对会导致软件系统不停的腐化,这就需要对系统不断进行重构,促使软件架构的方向同需求变化方向一致。在这种长期的马拉松式的重构中,如何控制重构的复杂度、如何保持系统重构中实时可用、如何保持开发人员重构的激情等是难点,本文中老乐尝试给大家分享自己的一点经验。

重构是一种在不改变软件系统外在行为的前提下,优化代码内部质量的方法,使得代码易于复用、易于理解、减少冗余,提升软件内在质量,从而使软件实现的复杂度逼近于软件要解决问题的复杂度,降低理解和维护成本。

既然重构不能改变系统外部行为,那就需要自动化测试用例的保护,于是重构前首先要补充自动化测试用例。但是由于遗留代码依赖较深,错综复杂,很多代码都是一个个大泥球,且过大类、过大函数、发散式变换、散弹式修改比比皆是,很多场景混杂揉合,用例很难构造,甚至过大耦合导致很多类都不能实例化出来,必须依赖上下文才能构造,这种情况很难补充用例,补充用例越难,越是不敢重构;要解决设计测试用例问题,必须先把代码进行重构,把功能拆分、迁移、规整、内聚,整理出一个个职责单一、功能内聚小功能代码模块,这么大改动又必须需要自动化测试用例的保护。这就陷入了一个死循环,补充用例需要先重构,重构又需要先补充用例,掉入鸡生蛋还是蛋生鸡的陷阱。

为了解决这个困境,这里推荐一个折中的方法,采用金鱼缸

的方式,先在尽可能有把握保证功能安全的情况下,对代码进行微调,使之逐步适于编写测试用例,或进行一定的解耦分割,使内容变得单一、内聚,从而适于编写用例。

下面,老乐着重介绍下几种常见的很难编写测试用例的遗留代码和微调手法。

一、硬代码依赖

这种情况指的是对第三方模块或非本功能内模块的引用采用直接依赖的方式。

比如,一个从键盘读入数据并输出到打印机的程序:

其中,RdKbd()函数是键盘驱动函数,这种情况下如果不对代码做微调,想做自动化测试的话,只能以下做法:

1、做系统测试,提供完整的键盘硬件、键盘驱动等,并搭建真实测试环境。问题是:

a) 测试环境准备代价大(如果是稀缺资源会很排队使用,多套测试环境要准备多套硬件);

b) 其次测试环境不稳定问题,比如硬件质量问题,接口松动,连接不稳定、网络风暴甚至病毒干扰等;

c) 单次用例执行相对较长

d) 测试用例不过时,需要分别定位是业务代码问题还是硬件问题、驱动问题,增加额外工作量。

2、做UT,使用mock工具

使用mock工具可以不修改代码构造成用例。但是,问题是此时用例要清楚知道mock的位置,也就是说要知道代码的细节实现逻辑,这就造成测试用例和代码细节实现逻辑的深度耦合,用例变得非常晦涩难懂,且细节实现逻辑稍微变化后,就会造成用例不稳定,造成用例维护工作量过大。

微调手法一、依赖隔离

如下:

这样微调后,从正确性和性能角度看,风险是可控的。即可以认为copy1=copy2,测试用例就可以针对copy3开发,这样copy的业务逻辑就和键盘和键盘驱动硬件隔离开了,测试用例设计的简单性、测试环境稳定性、测试问题定位的易识别性、测试性能都会提升。

如果代码是面向对象的,依赖隔离手法另一种常见方式是使用子类进行隔离依赖。比如:

class A{

public:

void processPORequest(PORequest *request){

 log.logEvent(request);

 …

}

}

////////////////////////修改为

class B:A{

public:

void processPORequest(PORequest *request){

 logEvent(request);

 …

}

virtual void logEvent(PORequest *request) {

 log.logEvent(request);

}

}

这样,对processPORequest函数进行单元测试时,就可以通过类C继承类B,然后覆盖掉B中的logEvent函数即可隔离对第三方log的依赖。

二、新增功能

微调手法二、发芽

新增功能时,新增部分如果粒度相对较独立时,这时候微调的手法是发芽,就像在一棵老树上长出一棵新芽一样。

具体做法建议独立成一个接口或类,把新增功能独立出来变成单独函数或类C,这样对这个独立出来的C做单元测试,而不是把新功能代码散落到老代码中,当然,这里也要实现依赖隔离。

三、大泥球

微调手法三、分离关注点

当接受一坨大泥球式的代码时,特别是很难读懂来龙去脉的代码是,更是感觉有点碰到一只刺猬,

哪里都是刺,无从下口。这时候微调的手法就是分离关注点,这种方法是一种相对比较粗放的方式,适合对大泥球进行粗分,帮助我们更好的理解原代码,并为下一步精细化重构做好铺垫。

以发散式变换的过大类A为例,具体手法如下:

1、首先尝试选取部分关联紧密的成员变量,将这些成员变量移植到一个新的类C中,并且设置成public封装类型,并给这个新类一个合适的名字。

2、A中通过C.变量的形式引用这些成员变量,编译通过。

3、A中和C.变量关联紧密的代码移植到C中,并变成C的成员函数。

4、A中引用C的新的成员函数

5、C中的成员变量变成private。

这样A就被分割成A、C两个类,然后再选取A中其他成员变量,重复上述5个步骤,渐渐的,A就被拆分为一系列的功能内聚、职责单一的小类,从而就可以补充测试用例并进行下一步精细化重构。

对于非面向对象的代码,比如过长函数,则可以选择其中的作用域大的局部变量或全局变量为起点,抽取到struct中开始做起。

对于不易快速读懂的老代码,就可以通过分离关注点这种手法来理解代码逻辑,并且拆分代码功能,从而更好的进行重构。

本文作为重构系列谈的一个结尾,总算填完了一个坑,老乐舒了一口气,希望能给大家带来启发。

声明:来自丁辉的软件架构说,仅代表创作者观点。链接:https://eyangzhen.com/7852.html

丁辉的软件架构说的头像丁辉的软件架构说

相关推荐

添加微信
添加微信
Ai学习群
返回顶部