最近看了Martin Fowler的著作《Refactoring: Improving the Design of Existing Code》收获颇大。书上的例子都是Java写的,其实我们同样可以运用在其他语言中,现在做.NET开发的话,用的最多的就是Visual Studio,它自带的重构功能可以很方便的整理代码。

重构的好处不多叙述了,大家都明白。书上论述的非常详细,我挑选几种最常用的重构方法,并用C#语言做示例,帮助大家了解必要的重构手法。另外,我也会附上一些自己平时在编程中常用的重构片段,但未必是100%正确或最好的。

1.Extract Method (提取函数)

样例代码:

public void PrintOwing(double amount)
{
	PrintBanner();
	// print details
	Console.WriteLine("name:" + _name);
	Console.WriteLine("amount:" + _amount);
}

重构为:

public void PrintOwing(double amount)
{
	PrintBanner();
	PrintDetails(amount);
}

public void PrintDetails(double amount)
{
	// print details
	Console.WriteLine("name:" + _name);
	Console.WriteLine("amount:" + _amount);
}

思想:让每个函数只做一件事,抽取能够被组织在一起的代码,并单独抽取为一个函数。如果函数的粒度小,被复用的机会就更大。

我常看到初学ASP.NET的同学喜欢把所有的代码全部写在Page_Load事件中。我们暂且不论分层架构,光看页面本身,如果所有的代码都放在一个函数中,那么可重用性就几乎为0。

打个比方,你的页面上有个GridView,你希望打开网页的时候给它绑定数据,于是你讲这段代码放在了Page_Load中,当然,Page_Load还会做除此以外的很多处理。然而,在你相应页面的其他事件的时候(比如点击Button、处理用户输入等),仍然需要重新绑定GridView的数据。这时候你就非常希望不要重写一遍数据绑定的逻辑(如果重写一个逻辑很多次,那以后要改的话就得改很多处)。于是,我们就应该把数据绑定逻辑提取到一个单独的函数中。比如叫BindData(),然后分别在Page_Load事件,和其他你需要再次绑定数据的事件中调用BindData()。

PS: 有时候需要重新绑定数据,单独写GridView1.DataBind();是不行的。

Visual Studio中做这样的重构很简单:选中你要提取的代码片段,然后点右键->重构->提取方法,键入一个新方法名称即可。VS会自动处理该片段与已有函数的调用关系。

关于函数的命名,我建议大家用动宾短语,阅读的时候会感觉比较自然。

2.Inline Temp(内联临时变量)

样例代码:

double basePrice = anOrder.BasePrice();
return (basePrice > 1000);

重构为:

return (anOrder.BasePrice() > 1000);

注意:重构前要保证这个临时变量只被赋值一次,所以当遇到for等循环语句时,要注意被内联的变量的值是否会在循环中被改变。有时在执行for之前定义固定的临时变量是必要的。尤其是当临时变量取自某对象的属性,而这个对象将在for循环中被更改。

3. Replace Temp with Query(以查询取代临时变量)

样例代码:

double basePrice = _quantity * _itemPrice;
if(basePrice > 1000)
{
	return basePrice * 0.95;
}
else
{
	return basePrice * 0.98;
}

重构为:

if(BasePrice() > 1000)
{
	return BasePrice() * 0.95;
}
else
{
	return BasePrice() * 0.98;
}
...
double BasePrice()
{
	return _quantity * _itemPrice;
}

其实对于这个重构,我还是有些疑惑的。我们发现,BasePrice()会被计算多次,但书上说不用担心性能问题,不过我对此还是保持怀疑。毕竟有些计算开销是很大的,并且执行多次可能产生额外的影响,所以我建议大家使用Replace Temp with Query手法的时候要谨慎,如果给一个临时变量复制的操作开销很大,尤其是需要和数据库交互,我不建议采用该手法重构。

4. Replace Nested Conditional with Guard Clauses (以卫语句嵌套条件表达式)

样例代码:

double GetPayAmount()
{
	double result;
	if(_isDead) result = deadAmount();
	else
	{
		if(_isSeparated)
		{
			result = separatedAmount();
		}
		else
		{
			if(_isRetired)
			{
				result = retiredAmount();
			}
			else
			{
				result = normalPayAmount();
			}
		}
	}
	return result;
}

重构为

double GetPayAmount()
{
	if(_isDead)
	{
		return deadAmount();
	}
	if(_isSeparated)
	{
		return separatedAmount();
	}
	if(_isRetired)
	{
		return retiredAmount();
	}
	return normalPayAmount();
}

这个重构手法也是我用的很多的,在初学编程的时候,很多人都会写出很多if…else嵌套的代码,其实很多时候,else并不是必须的。关于if…else…如何取决,关键看你对各分支的重视程度。

摘录书上的原话:

“根据我的经验,条件表达式通常有两种表现形式。第一种形式是:所有分支都属于正常行为。第二种形式则是:条件表达式提供的答案中只有一种是正常行为,其他都不是常见的情况。这两类表达式有不同的用途,这一点应该通过代码表现出来。如果两条分支都是正常行为,就应该用形如if…else…的条件表达式;如果某个条件及其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为卫语句”

插一句题外话,我平时写程序,遇到不正常情况需要提示,我通常会在if里throw一个exception,这些if是并行的,不是嵌套的,最后在catch里捕获消息并弹窗提示,我不知道这种做法是否好,求高手指点: )

5. Introduce Explaining Variable  (引入解释性变量)

样例代码:

if((platform.ToUpper().IndexOf("MAC") > -1) && (browser.ToUpper().IndexOf("IE") > -1) && wasInitialized() && resize > 0)
{
	// do something
}

重构为:

bool isMacOs = platform.ToUpper().IndexOf("MAC") > -1;
bool isIE = browser.ToUpper().IndexOf("IE") > -1;
bool wasResized = resize > 0;

if(isMacOs && isIE && wasInitialized() && wasResized)
{
	// do something
}

这种重构手法的目的显而易见,不多叙述了。

6. 我自己常用的代码重构片段

1. 关于if和return

// 原始片段
int row = DbHelperSql.ExecuteSql(sql);
if(row) > 1
{
	return true;
}
else
{
	return false;
}

// 这样写可以省略一对括号
int row = DbHelperSql.ExecuteSql(sql);
if(row) > 1
{
	return true;
}
return false;

// 逻辑是这样,直接return一个表达式
int row = DbHelperSql.ExecuteSql(sql);
return (row > 1);

// 应用"Replace Temp with Query"手法
return DbHelperSql.ExecuteSql(sql) > 1;

2. 三目表达式

// 如果碰到非bool类型的返回值可以这样
return Function() == 1 ? "Hehe" : "haha";

“Replace Temp with Query”中的例子也可以写成三目表达式:

return BasePrice() > 1000 ? BasePrice() * 0.95 : BasePrice() * 0.98;