2010年6月12日
9:29
我超愛這個函數 ,它真是講解重構的極品。這段代碼意圖是比較容易看得到的,因爲它就寫在alertword變量內——檢查密碼如果符合以下其情況,就返回提示文字
1. 密碼爲空
2. 密碼爲連續字符
1. 密碼總共只用了不超過2個符號
2. 密碼位數不到6位;
我一直在尋找各種各樣的案例,然後看到這個案例後,我覺得好像找到了“終極案例”——可以
用很多的重構手法用於此處。比如:
1. 變量就近聲明
1. 去除重複
1. 函數抽取
2. 對象內聚
1. 封裝簡單類型
這段代碼還可以演示從一個函數,到一組函數,在到類的過程。對於建立OO思維是很好的案例。
不如讀者們自己先試試看這些優化的方法,這樣看下面的文字才更有效果。
private string PasswordAlert(string pwd)
{
bool IsSequence = true, Is2Char = true, IsShort = true, IsEmpty = true;
string tempStr = string.Empty, a = string.Empty, b = string.Empty;
string alertWord = "提醒,你的密碼有下列情況之一:\n\n1、密碼爲空;\n2、密碼爲連續字符;\n3、密碼總共只用了不超過2個符號;\n4、密碼位數不到6位;\n\n爲了密碼安全,請修改你的密碼。";
IsEmpty = pwd.Length == 0;
if (pwd.Length <= 1)
return IsEmpty || IsSequence || Is2Char || IsShort ? alertWord : string.Empty;
for (int i = 0; i < pwd.Length - 1; i++)
{
if ((int)pwd[i] + 1 == (int)pwd[i + 1] || (int)pwd[i] - 1 == (int)pwd[i + 1])
continue;
IsSequence = false;
break;
}
for (int i = 0; i < pwd.Length; i++)
{
tempStr = pwd.Substring(i, 1);
if (String.IsNullOrEmpty(a))
a = tempStr;
if (!String.IsNullOrEmpty(a) && String.IsNullOrEmpty(b) && a != tempStr)
b = tempStr;
if (tempStr == a || tempStr == b || a == string.Empty || b == string.Empty)
continue;
Is2Char = false;
break;
}
IsShort = pwd.Length < 6;
return IsEmpty || IsSequence || Is2Char || IsShort ? alertWord : string.Empty;
}
。
代碼存在的問題
問題1在於:它的意圖和代碼本身並不自然的匹配,反而存在些不自然的寫法。比如第一個if判斷的情況和以上4條並不直接相關,第一個for就是檢查密碼字符是否連續,和2匹配,第二個for檢查和規則3匹配,最後一個 IsShort = pwd.Length < 6;和規則4一致。大部分還是中規中矩。
問題2 在於:但是爲什麼那麼多的中間變量,並且變量的生命週期,看起來有些複雜,到底那裏改了,需要看引用,凡是需要看引用才能知道生命週期的,都是需要考慮如何避免的。
直觀的想法就是,代碼應該改成這樣樣子:
private string PasswordAlert(string pwd)
{
string alertWord = "XXX...";
return IsEmpty() || IsSequence() || Is2Char() || IsShort()? alertWord : string.Empty;
}
每個IsXXX函數都直接和語義相配合,從而語義上更加直接呢?
好,我們把他當成目標,來看我們如何把現在的代碼變成我們的目標代碼!這就是不改變代碼的外部行爲,但是通過各種方法讓代碼的內部質量得以提升的過程就是重構。
重構第一步:去掉死代碼
比如:
if (pwd.Length <= 1)
return IsEmpty || IsSequence || Is2Char || IsShort ? alertWord : string.Empty;
代碼執行到這裏,其實 IsSequence || Is2Char || IsShort 必然都是true。也就是說代碼和
if (pwd.Length <= 1)
return alertWord ;
完全等效。
既然如此 IsEmpty || IsSequence || Is2Char || IsShort ? alertWord : string.Empty;就可以說大部分都是死代碼,放在這裏對閱讀者就是一種干擾——儘管我明白,它的意思是當pwd長度爲1的時候,符合3種情況。
重構第二步:變量就近聲明
不但函數有職責,函數內的代碼塊也同樣需要按職責來組織。同樣職責的代碼,儘可能的放到一起,從而更加容易理解。從這個角度分析,最上面的變量聲明塊
bool IsSequence = true, Is2Char = true, IsShort = true, IsEmpty = true;
string tempStr = string.Empty, a = string.Empty, b = string.Empty;
就應該各就各位,而不是都堆在最上面了。比如tempStr,a,b都僅僅被第二個for的代碼塊引用,因此應該把這些變量移動下來,放到第二for之上。這就是“變量就近聲明”的重構手法。
手工改當然是可以的,但是不夠安全——非常幸運的是有工具可以幫助我們來做這個事情。Coderush express有一個功能叫做
重構第三步:抽取函數
鑑於抽取函數太過簡單,太過常用,因此這裏就不講了。沒有什麼講頭。
一旦完成,在同一個類內,就會有多個新的函數出現,並且和老的PasswordAlert並列。
形如:
private string PasswordAlert(string pwd) {… }
private static bool IsShort_(string pwd){… }
private static bool IsEmpty_(string pwd){… }
private static bool Is2Char(string pwd){… }
private static bool IsSequence(string pwd){… }
重構第四步:合併函數爲類
顯然,函數IsShort,IsEmpty,Is2Char,IsSequence 應該是成爲一組的。這一組功能和其他代碼關係不大,而他們之間則是非常內聚的——都是爲了對password進行驗證而存在的。因此,從耦合關係上看,把這些方法放到一個構造塊內是必要的。不僅從語義上分析應該內聚,從這些個函數的參數內也可以看出端倪。他們有共同的參數就是password字符串。因此引入的新類可以就是Password,這個類內聚有IsShort,IsEmpty,Is2Char,IsSequence方法,並且一旦這樣做了後,也不必在一個個的給每個函數傳遞password參數,而是通過構造函數一次傳遞進來,其他的函數都可以通過成員變量pwd來共享這個外部參數。
這樣完成後,代碼形如
Class Password
{
string pwd;
private string Password(string pwd) {this.pwd=pwd; }
private static bool IsShort_(){… }
private static bool IsEmpty_(){… }
private static bool Is2Char(){… }
private static bool IsSequence(){… }
}
刪除多餘的參數是很爽的事情!
總結
演示了從大函數到小函數,從多個小函數變成類的過程。這個方法對我而言已經是一個很好的套路——多次使用,屢試不爽。比如在很久以前的一個項目中,我從大類內的大型函數到若干函數,形成類,把類分離出來。類之間採用委託來耦合,很成功的把一個職責不清的大類分拆成爲若干個職責清晰、功能靈活組合的小類。
在去年的一個項目中,也通過類似的手法,把一個大類變成30幾個小類,每個類不過200行。
可以用自動化的工具來幫助安全的重構。Eric gamma說,重構是有風險的,風險在於變化中的很多細枝末節,一個不小心就可能改錯。他根據和kent beck的合作經驗,認爲 small step once time(一次一小步) 的方式可以更好的規避這個風險。
在這個案例裏面,也說明通過重構工具可以更加安全的重構。以這個案例爲例,作爲c#的代碼,可以通過devexpress coderush xpress來幫助重構。步驟二(變量就近聲明),步驟三(函數抽取)都是可以用工具來做的,步驟一和四無法用工具,但是步驟四內的”刪除參數“是可以用工具的。
工具協助下做的重構都是必然是安全的。它遵守的是等價變換的原則。因此用工具可以讓整個過程更加平順。儘可能用工具,修改後,連測試都是不需要的 。實際上我就是這樣去做重構的。
對於存在標註函數體,通常是小函數的存在的信號,應該分而治之。另外一個信號是region的存在。region存在往往意味着可能一個構造塊(函數,類等)需要抽離出來。如果抽離後還是感覺函數內要標註的時候,很可能說明還需要分拆。