高效重构 C++ 代码(中)
作者:魔术大师 发布时间:[ 2016/9/27 11:39:33 ] 推荐标签:测试开发技术 .NET
重构的顺序
通过之前的例子可以看到稍微复杂一点的重构都是由一系列的基本手法或者原子步骤组成. 在实践中对基本手法或者原子步骤的使用顺序非常重要,如果顺序不当,有时甚至会让整个重构变得很难实施.
关于重构的顺序,基本的一点原则是自底向上! 我们只有先从细节的重构开始着手,才能使得较大的重构轻易完成.
例如对于消除兄弟类型之间的重复表达式,我们只有先运用Extract Method将重复部分和差异部分分离,然后才能将重复代码以Pull Up Method重构手法推入父类中.
对于稍微复杂的重构,当我们确定了重构目标后,接下来可以进行重构顺序编排,顺序编排的具体操作方法是:从目标开始反向推演,为了使当前目标达成的这一步操作能够非常容易、安全和高效地完成,它的上一步状态应该是什么? 如此递归,一直到代码的现状.
下面我们以复杂一点的Extract Method为例,应用上述原则,完成重构顺序的编排.
我们知道Extract Method在函数有较多临时变量的时候,是比较难以实施的.原函数内较多的临时变量可能会导致提取出的函数有大量入参和出参,增加提炼难度,并且降低了该重构的使用效果.
所以为了使得提炼较为容易实施,我们一般需要在原函数中解决临时变量过多的问题.
在这里我们把函数内的临时变量分为两类,一类是累计变量(或者收集变量),其作用是收集状态,这类变量的值在初始化之后在函数内还会被反复修改.累计变量一般包括循环变量或者函数的出参. 除过累计变量外,函数内其它的变量都是查询变量.查询变量的特点是可以在初始化的时候一次赋值,随后在函数内该变量都是只读的,其值不再被修改.
接下来我们逐步反推如何使得Extract Method较为容易实施:
目标状态: 新提取出来的方法替换原方法中的旧代码,并且测试通过!
为了使得1容易完成,我们现在应该已经具有新提炼出来的新方法,该方法命名良好,其返回值和出入参是简单明确的,其内部实现已经调整OK.并且这个新提炼出来的方法是编译通过的.
为了使得2容易完成,我们需要先为其创建一个方法原型,将老代码中要提炼部分copy进去作为其实现,根据copy过去的代码非常容易确定新函数的返回值类型和出入参.
为了使得3容易完成,原有代码中被提炼部分和其余部分应该具有很少的临时变量耦合.
为了满足4,我们需要消除原有函数中的临时变量. 对于所有的查询变量,我们可以将其替换成查询函数(应用重构手法:以查询替代临时变量).
为了使得5容易做到,我们需要区分出函数内的累计变量和查询变量.如果某一查询变量被多次赋值,则将其分解成多个查询变量,保证每个查询变量都只被赋值一次.并且对每个查询变量定义即初始化,而且要定义为const类型.
为了使6方便做到,我们需要容易地观察到变量是否被赋值多次,这时变量应该被定义在离使用近的地方.所以我们应该对变量的定义位置进行调整,让其靠近其使用的位置.(对C语言程序这点尤其重要).
有了上面的分析,我们将其反过来,是对于稍微复杂的Extract Method重构手法每一步的操作顺序:
· 修改原函数内每一个局部变量的定义,让其靠近其使用的地方,并尽量做到定义即初始化;
· 区分查询变量和累计变量.对于查询变量有多次赋值的情况,将其拆分成多个查询变量.保证每个查询变量只被赋值一次.
· 对每个查询变量的类型定义加上const,进行编译.
· 利用”以查询替代临时变量”重构,消除所有查询变量.减少原函数中临时变量的数目.
· 创建新的方法,确定其原型.
· 将原函数中待提炼代码拷贝到新的函数中,调整其实现,保证编译通过.
· 将新的函数替换回原函数,保证测试通过.
下面是一个例子:
bool calcPrice(Product* products,U32 num,double* totalPrice)
{
U32 i;
double basePrice;
double discountFactor;
if(products != NULL)
{
for(i = 0; i < num; i++)
{
basePrice = products[i].price * products[i].quantity;
if(basePrice >= 1000)
{
discountFactor = 0.95;
}
else
{
discountFactor = 0.99;
}
basePrice *= discountFactor;
*totalPrice += basePrice;
}
return true;
}
return false;
}
上面是一段C风格的代码. 函数calcPrice用来计算所有product的总price. 其中入参为一个Product类型的数组,长度为num. 每个product的价格等于其单价乘以总量,然后再乘以一个折扣. 当单价乘以总量大于等于1000的时候,折扣为0.95,否则折扣为0.99. 出参totalPrice为终计算出的所有product的价格之和. 计算成功函数返回true,否则返回false并且不改变totalPrice的值.
Product是一个简单的结构体,定义如下
typedef unsigned int U32;
struct Product
{
double price;
U32 quantity;
};
calcPrice函数的实现显得有点长.我们想使用Extract Method将其分解成几个小函数.一般有注释的地方或者有大的if/for分层次的地方都是提炼函数不错的入手点.但是对于这个函数我们无论是想在第一个if层次内,或者for层次内提炼函数,都遇到不小的挑战,主要是临时变量太多导致函数出入参数过多的问题.
下面是我们借助eclipse的自动重构工具将for内部提取出一个函数的时候给出的提示:
extractExample.jpeg
可以看到它提示新的函数需要有5个参数之多.
对于这个问题,我们采用上面总结出来的Extract Method的合理顺序来解决.
将每一个局部变量定义到靠近其使用的地方,尽量做到定义即初始化;
bool calcPrice(Product* products,U32 num,double* totalPrice)
{
if(products != NULL)
{
for(U32 i = 0; i < num; i++)
{
double basePrice = products[i].price * products[i].quantity;
double discountFactor;
if(basePrice >= 1000)
{
discountFactor = 0.95;
}
else
{
discountFactor = 0.99;
}
basePrice *= discountFactor;
*totalPrice += basePrice;
}
return true;
}
return false;
}
在上面的操作中,我们将变量i,basePrice和dicountFactor的定义位置都挪到了其第一次使用的地方.对于i和basePrice做到了定义即初始化.
对于查询变量有多次赋值的情况,将其拆分成多个查询变量.保证每个查询变量只被赋值一次.
在这里我们辨识出totalPrice和i为累计变量,其它都是查询变量.对于查询变量basePrice存在多次赋值的情况,这里我们把它拆成两个变量(增加actualPrice),保证每个变量只被赋值一次.对于所有查询变量尽量加上const加以标识.
bool calcPrice(const Product* products,const U32 num,double* totalPrice)
{
if(products != NULL)
{
for(U32 i = 0; i < num; i++)
{
const double basePrice = products[i].price * products[i].quantity;
double discountFactor;
if(basePrice >= 1000)
{
discountFactor = 0.95;
}
else
{
discountFactor = 0.99;
}
const double actualPrice = basePrice * discountFactor;
*totalPrice += actualPrice;
}
return true;
}
return false;
}
利用”以查询替代临时变量”重构,消除所有查询变量.减少原函数中临时变量的数目.
在这里先从依赖较小的basePrice开始.
double getBasePrice(const Product* product)
{
return product->price * product->quantity;
}
bool calcPrice(const Product* products,const U32 num,double* totalPrice)
{
if(products != NULL)
{
for(U32 i = 0; i < num; i++)
{
double discountFactor;
if(getBasePrice(&products[i]) >= 1000)
{
discountFactor = 0.95;
}
else
{
discountFactor = 0.99;
}
const double actualPrice = getBasePrice(&products[i]) * discountFactor;
*totalPrice += actualPrice;
}
return true;
}
return false;
}
下来搞定discountFactor
double getBasePrice(const Product* product)
{
return product->price * product->quantity;
}
double getDiscountFactor(const Product* product)
{
return (getBasePrice(product) >= 1000) ? 0.95 : 0.99;
}
bool calcPrice(const Product* products,const U32 num,double* totalPrice)
{
if(products != NULL)
{
for(U32 i = 0; i < num; i++)
{
const double actualPrice = getBasePrice(&products[i]) * getDiscountFactor(&products[i]);
*totalPrice += actualPrice;
}
return true;
}
return false;
}
下来消灭actualPrice:
double getBasePrice(const Product* product)
{
return product->price * product->quantity;
}
double getDiscountFactor(const Product* product)
{
return (getBasePrice(product) >= 1000) ? 0.95 : 0.99;
}
double getPrice(const Product* product)
{
return getBasePrice(product) * getDiscountFactor(product);
}
bool calcPrice(const Product* products,const U32 num,double* totalPrice)
{
if(products != NULL)
{
for(U32 i = 0; i < num; i++)
{
*totalPrice += getPrice(&products[i]);
}
return true;
}
return false;
}
到目前为止,我们初的目标已经达成了.如果你觉得getBasePrice调用过多担心造成性能问题,可以在getDiscountFactor和getPrice函数中使用inline function重构手法将其再内联回去.但是getBasePrice可以继续保留,假如该方法还存在其它客户的话.另外是否性能优化可以等到有性能数据支撑的时候再进行也不迟.
后,可以使用重构手法对calcPrice做进一步的优化:
double getTotalPrice(const Product* products,const U32 num)
{
double result = 0;
for(U32 i = 0; i < num; i++)
{
result += getPrice(&products[i]);
}
return result;
}
bool calcPrice(const Product* products,const U32 num,double* totalPrice)
{
if(products == NULL) return false;
*totalPrice = getTotalPrice(products,num);
return true;
}
针对后是否提取getTotalPrice函数可能会有争议. 个人认为将其提取出来是有好处的,因为大多数情况下只关注正常场景计算的函数是有用的.例如我们可以单独复用该函数完成对计算结果的打印:
Product products[10];
...
printf("The total price of products is %fn",getTotalPrice(product,10));
通过上面的例子可以看到按照合理顺序进行重构的重要性. 当我们实施重构的时候,如果到某一步觉得很难进行,要反思自己的重构顺序到底对不对,首先看看是不是自底向上操作的,再思考一下如果要让当前步骤变得简单,它之前还应该做什么.具有这样的思维后,以后碰到各种场景都能游刃有余了.
总结
本节我们总结了四类基本的重构手法.复杂的重构基本上可以借助基本手法的组合来完成. 更进一步我们提炼了重构的原子步骤,将大家从学习重构的繁琐步骤中解放出来. 只要掌握了两个原子步骤及其要求,可以组合出大多数的重构手法. 而且原子步骤是安全小步的,代码的提交和回滚可以以原子步骤为单位进行. 为了使substitute容易进行,我们讨论了锚点以及其经常使用的场合. 后我们总结了重构操作顺序背后的思想,借助合理的顺序,可以让我们的重构变得轻松有序.
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系SPASVO小编(021-61079698-8054),我们将立即处理,马上删除。
相关推荐
更新发布
功能测试和接口测试的区别
2023/3/23 14:23:39如何写好测试用例文档
2023/3/22 16:17:39常用的选择回归测试的方式有哪些?
2022/6/14 16:14:27测试流程中需要重点把关几个过程?
2021/10/18 15:37:44性能测试的七种方法
2021/9/17 15:19:29全链路压测优化思路
2021/9/14 15:42:25性能测试流程浅谈
2021/5/28 17:25:47常见的APP性能测试指标
2021/5/8 17:01:11热门文章
常见的移动App Bug??崩溃的测试用例设计如何用Jmeter做压力测试QC使用说明APP压力测试入门教程移动app测试中的主要问题jenkins+testng+ant+webdriver持续集成测试使用JMeter进行HTTP负载测试Selenium 2.0 WebDriver 使用指南