綜合演練重構 ——以Passalert爲案例


2010612

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一致。大部分還是中規中矩。

問題在於:但是爲什麼那麼多的中間變量,並且變量的生命週期,看起來有些複雜,到底那裏改了,需要看引用,凡是需要看引用才能知道生命週期的,都是需要考慮如何避免的。

直觀的想法就是,代碼應該改成這樣樣子:

 

        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;

就應該各就各位,而不是都堆在最上面了。比如tempStra,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存在往往意味着可能一個構造塊(函數,類等)需要抽離出來。如果抽離後還是感覺函數內要標註的時候,很可能說明還需要分拆。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章