前言: 我们做程序员这一行的哈,说白了就是用机器能识别的语言跟机器打交道,但是经常会出现的一种情况是,我们所写的代码只有自己和机器能看得懂,其他人看懂有一定的困难性,甚至有时候过了一段时间连自己都看不懂了。。。那么此时你应该感叹自己当时的水平高超,还是该好好的反思一下了?
一,重构是什么
OK,闲言碎语不多讲,啥叫重构呢?随手打开百度,输入“重构”,点开百度百科:重构(Refactoring)就是通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。但是这个解释缺少了一个前提,完整的说法应该是:在不改变软件可观察行为的前提下,通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。
当我第一次读到这句话时,感觉重构这件事情跟我这个满头秀发的初级程序员没有任何关系,还什么设计模式,还什么架构,好高深的东西。。但是随着发型趋于成熟和结合踩过的越来越多的坑,逐渐改变了这个想法。
前提:不能改变软件原来有的功能,我做了点什么事呢:改了改代码,然后呢:机器跑这个软件轻松了或其他同事更容易理解这段代码了(此处立个Flag,或许下次再读这句话时会重构它)。
举个例子,当你穿的衣衫不整的就去参加亲戚或朋友的婚礼了,而且表情愤怒,言语不羁,认识你的人知道你是来参加婚礼,不知道的以为你是新娘的前男友去砸场子呢,容易被门口保安小哥按住。如何避免这种尴尬呢?很简单,重构一下啊:把头发梳成大人模样,穿上一身帅气西装,手里拿着红包,然后满脸微笑,见了人多打招呼。这样的话别人一看就知道你是来干啥滴。你还是你,该做的事情做得更好了,保安小哥也不会拦你了,说不定门口的礼仪小姐姐还会和你打招呼,而且逼格也提升了好几个level。设身处地的想一想,我们的仪表着装言谈举止不就是我们个人社交时的“对外接口”么。
OK,回到软件代码的重构,结合刚才那个例子,其实把一个很low函数名改成一个容易让人理解的名字,也属于重构。这样不管是自己还是其他人一看函数名字就大体知道这个函数是要干啥滴!
二,为什么要重构
关于为什么要重构,查阅相关资料或翻阅相关书籍大多都会提到以下几句话:1、持续偏纠和改进软件设计 。2、使代码更被其他人所理解。 3、帮助发现隐藏的代码缺陷 。4、从长远来看,有助于提高编程效率,增加项目进度。
其实将什么是重构时已稍微带入了,再次强调一下,为了持续改进我们的软件设计使得机器跑我们的软件更轻松,为了让同事更容易理解我们写的代码,为了出更少的Bug,为了更快的开发! 简单明了而且就连百度百科上也解释的很清楚,此处不多赘述。
三,怎么重构
哲学上有世界观和方法论,同样此时我们已经对重构有了一定的了解,那么我们通过什么方法进行重构呢?
OK,接下来要分不同的情况进行讨论了。
1,提炼函数
具体做法:①拆分过长的函数。②提炼在多个函数中使用的相同代码
这样做的好处是:①函数被复用的机会大。②如果所提炼的函数命名足够好,那么高层函数的功能更加一目了然
比如以下函数(代码是C/C++,但是 C#, Java的同学都可以看得懂):
int nArrays[5] = {1,2,3,2,1};
void PrintResult()
{
int nPrintNum ;
// Print split line;
printf("****************************");
printf("******** *******");
printf("****************************");
// Algorithm;
for (int i = 0;i<5;i++)
{
nPrintNum += nArrays[i] * 5;
}
// Result:
printf("Result: %d" , nPrintNum);
}
其实这个函数已经很简单了,我这里举一个简单的例子简单说明一下(代码纯手敲,并没有做过运行)
分析一下哈,首先这个函数名是叫"PrintResult",个人感觉里面最好不要包含算法,所以将算法应该提取出来,当然上面Pring splist line也可以提取出来:
void PrintSplitLine()
{
printf("****************************");
printf("******** *******");
printf("****************************");
}
int CalcResult()
{
int nPrintNum = 0;
for (int i = 0;i<5;i++)
{
nPrintNum += nArrays[i] * 5;
}
return nPrintNum;
}
void PrintResult()
{
PrintSplitLine();
int Result = CalcResult();
printf("Result: %d" , Result );
}
这样的话PrintResult()这个函数甚至连注释都不用加大家就都能看懂,而且如果其他地方需要打印分割线或者使用这个算法时候,这两个函数都可以复用了。
2,以查询取代临时变量
比如上一段代码定义了一个为Result申请了一个临时变量,当然也无可厚非,提倡的做法是直接查询就可以了:
void PrintResult()
{
PrintSplitLine();
printf("Result: %d" , CalcResult());
}
3.引入解释性变量
大家看下下面的代码,虽然加了注释,但是看起来依然不爽,揍啥嘞界是!
double GetPrice()
{
//price is base price - quantity discount + shipping
return m_nQuantity * m_nItemPrice - max(0,m_nQuantity-500) * n_ItemPrice * 0.05 + min(m_nQuantity * m_ItemPrice * 01, 100.0 );
}
这个表达式太复杂,我们应该将表达式拆分,拆分的结果应该放到临时变量里,以变量名称来解释表达式的用途:
double GetPrice()
{
double dBasePrice = m_nQuantity * m_nItemPrice;
double dQuantityDiscount = max(0, m_nQuantity - 500) * m_nItemPrice * 0.05;
double dShipping = min( dBasePrice * 0.1, 100.0);
return dBasePrice - dQuantityDiscount + dShipping;
}
4,分解临时变量
临时变量通常用于保存临时运算结果,这种临时变量应该只被赋值一次。如果它被赋值超过一次意味着它承担了一个以上的责任,当然这也无可厚非,但是这使得其他人会产生疑惑,这个值到底是干嘛用的?
比如下面这个函数,本来就已经够复杂的了,但是大家会发现dAcc赋值了两次,而这两次是属于不同的加速度,都用这个变量表示容易让人产生误解。
double GetDistanceTravelled(int nTime)
{
double nResult = 0;
// 加速度
double dAcc = m_dPrimaryForce / m_dMass;
int nPrimaryTime = min(nTime, m_dDelay);
nResult = 0.5 * dAcc * nPrimaryTime * nPrimaryTime;
int nSecondaryTime = nTime - m_dDelay;
if (nSecondaryTime > 0)
{
double nPrimaryVel = dAcc * m_dDelay;
dAcc = (m_dPrimaryForce + m_dSecondaryForce) / m_dMass;
nResult += nPrimaryVel * nSecondaryTime + 0.5 * dAcc * nSecondaryTime * nSecondaryTime;
}
return nResult;
}
不同含义的变量应该用两个不同的临时变量:
double GetDistanceTravelled(int nTime)
{
double nResult = 0;
// 加速度
const double dPrimaryAcc = m_dPrimaryForce / m_dMass;
int nPrimaryTime = min(nTime, m_dDelay);
nResult = 0.5 * dAcc * nPrimaryTime * nPrimaryTime;
int nSecondaryTime = nTime - m_dDelay;
if (nSecondaryTime > 0)
{
double nPrimaryVel = dPrimaryAcc * m_dDelay;
const double dSecondaryAcc = (m_dPrimaryForce + m_dSecondaryForce) / m_dMass;
nResult += nPrimaryVel * nSecondaryTime + 0.5 * dSecondaryAcc * nSecondaryTime * nSecondaryTime;
}
return nResult;
}
5,移除对参数的赋值
首先观察下面的函数的某个片段:
void Method( Object obj)
{
int nResult = obj.GetValue(); // 1
Object anotherObject = new Object();// 2
obj = anotherObject; // 3
// ...
}
解释一下,首先obj是一个参数,在函数内改变它的值是一个不好的习惯,其次obj是当做一个对象传进来的,也没办法改变它的值。如果在Method函数里面只是想获取某些obj里面的值,下面这种写法是提倡的:
void Method( const Object& obj)
{
int nResult = obj.GetValue();
Object anotherObject = new Object();
// ...
}
首先我不改变传入的值所以加了个const,其次传引用只需要传递四个字节。这里需要注意的是Object类中的GetValue()应该也加上const:GetValue() const{ ... },因为const对象不能引用非const成员函数。另外函数加上了const,在函数内就不能改变成员变量了。
再举个简单的例子:
int Discount (int nInputVal, int nQuantity, int nYearToData)
{
if (nInputVal > 50) nInputVal -= 2;
if (nQuantity > 100) nInputVal -= 1;
if (nYearToData > 10000) nInputVal -= 4;
return nInputVal;
}
这种直接这种传入值,转了一圈又当做返回值出去了。。。不要修改传入值,否则容易让人混淆。提倡做法:
int Discount (int nInputVal, int nQuantity, int nYearToData)
{
int nResult = nInputVal;
if (nInputVal > 50) nResult -= 2;
if (nQuantity > 100) nResult -= 1;
if (nYearToData > 10000) nResult -= 4;
return nResult;
}
6,以函数对象取代函数
如果有一个大型函数,局部变量过多,可以将这个函数放到一个单独的对象中,如此一来,局部变量就变成了对象内的字段。然后可以在同一个对象中将这个大型的函数分解成多个小函数。另外,局部变量过多时,首先考虑“以查询取代临时变量”,有时候你会发现根本无法拆解一个需要拆解的函数,在这种情况下考虑使用函数对象。
class Order
{
// ...
double Price()
{
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
//long computation;
// ...
}
}
可参考以下类图:
7,替换算法
把某个算法替换为更清晰的算法:
下面这段代码是Java的代码,懂得C++或C#的童鞋应该能直接看懂。两段代码要实现的功能一致,至于你认为哪一种好用,就用哪一种呗(个人比较推崇第二段实现方式)。
String foundPerson( String[] people ){
for ( int i=0; i < people.length; i++ ) {
if ( people[i].equals("Don") ) {
return "Don";
}
if ( people[i].equals("John") ) {
return "John";
}
if ( people[i].equals("Kent") ) {
return "Kent";
}
}
return "";
}
String foundPerson( String[] people ){
List candidates = Arrays.asList(new String[]{"Don","John","Kent"});
for ( int i=0; i < people.length; i++ ) {
if ( candidates.contains(people[i) ) {
return people[i];
}
}
return "";
}
8,内联函数
在C++中,以inline修饰的函数叫做内联函数,编译时C++编译器会调用内联函数的地方展开,没有函数压栈开销,内联函数提升程序运行的效率。
内联函数用法大家自行百度就可以了,此处不再啰嗦。
OK,暂时先给大家分享到这里。欢迎批评指正。