打造可維護軟體C#版 第三章 撰寫簡單的程式碼單元
第三章重點在於重構,作者介紹三招針對分支程式碼的重構技巧。
指導方針:
限制每個程式碼單元不超過4個邏輯分支點。
將複雜的程式碼單元拆解成多個簡單的程式碼單元。
邏輯分支點越少,程式碼單元越容易修改及測試。
C#中最常見的分支點是if和switch。
???????
覆蓋程式碼單元所有獨立執行路徑的最小測試數量等於分支點數量加1。
範例程式輸入一個國家,GetFlagColors方法回傳正確的國旗顏色。
public IList<Color> GetFlagColors(Nationality nationality)
{
List<Color> result;
switch(nationality)
{
case Nationality.DUTCH
result = new List<Color> {Color.Red, Color.White, Color.Blue};
break;
…………...
}
return result;
}
有5個國家和1個未分類狀況,需要測試的獨立數量為6。想要測試整個方法,就必須使用6個獨立測試案例。
隨著時間的進展案例越來越多,起初簡單的程式會越來越複雜。
3.1 動機
基於下列兩個原因,請務必讓程式碼保持簡單:
簡單的程式碼單元容易修改
簡單的程式碼單元容易測試
3.2 如何應用這個指導方針?
在C#中,下面陳述式與運算子都算分支點:
if
case
?, ??
&&, ||
while
for, foreach
catch
處理一連串條件語句
第一種方法引進Map資料結構。
將國家對映到特定的Flag物件,這種重構方式減少GetFlagColors方法的複雜度,從McCabe 7 到 McCabe 2。
private static Dictionary<Nationality, IList<Color>> FLAGS = new Dictionary<Nationality, IList<Color>> ();
static FlagFactoryWithMap()
{
FLAGS[Nationality.DUTCH] = new List<Color>{Color.Red, Color.White, Color.Blue};
FLAGS[Nationality.GERMAN] = new List<Color>{Color.Black, Color.Red, Color.Yellow};
…………..
}
public IList<Color> GetFlagColors(Nationality nationality)
{
IList<Color> colors = FLAGS[nationality];
return colors ?? new List<Color>{Color.Gray};
}
第二種方法透過多型取代條件式。
讓每個國旗有自己的型別,並且實作通用的介面。C#語言的多型機制確保執行時期環境能夠調用正確的功能性。
為了這個重構,我們首先定義通用的IFlag介面:
public interface IFlag
{
IList<Color> Colors { get; }
}
針對不同國家定義不同的國旗型別,例如荷蘭:
public class DutchFlag:IFlag
{
public IList<Color> Colors
{
get
{
return new List<Color>{Color.Red, Color.White, Color.Blue};
}
}
}
以及義大利:
public class ItalianFlag:IFlag
{
public IList<Color> Colors
{
get
{
return new List<Color>{Color.Green, Color.White, Color.Red};
}
}
}
GetFlagColors方法現在變得簡潔,比較不容易出錯:
private static readonly Dictionary<Nationality, IFlag> FLAGS = new Dictionary<Nationality, IFlag> ();
static FlagFactory()
{
FLAGS[Nationality.DUTCH] = new DutchFlag();
FLAGS[Nationality.GERMAN] = new GermanFlag();
……..
}
public IList<Color> GetFlagColors(Nationality nationality)
{
IFlag flag = FLAGS[nationality];
flag = flag ?? new DefaultFlag();
return flag.Colors;
}
這項重構技巧提供最靈活的實作方式,例如只需實作新的國旗型別,並且獨立測試它。
缺點是會導致更多的程式碼散布在更多的類別中。?
開發者必須在擴展性與簡潔性之間作取捨。
處理嵌套語句
範例給定二元搜尋樹的根節點與整數,CalculateDepth方法會判斷這個整數在樹狀結構的位置,如果有找到,就回傳這個整數在樹狀結構中的深度,否則丟出TreeException例外。
第一版程式碼出現兩層if/else語句。
public static int CalculateDepth(BinaryTreeNode<int> t, int n)
{
int depth = 0;
if(t.Value == n)
{
return depth;
}
else
{
if(n < t.Value)
{
BinaryTreeNode<int> left = t.Left;
if(left == null)
{
throw new TreeExcepyion(“Value not found in tree!”);
}
else
{
return 1 + CalculateDepth(left, n);
}
}
else
{
BinaryTreeNode<int> right = t.Right;;
if(right == null)
{
throw new TreeExcepyion(“Value not found in tree!”);
}
else
{
return 1 + CalculateDepth(right, n);
}
}
}
}
第二版程式碼Replace Nested Conditional with Guard Clauses,可透過各種不同情況並且插入return 語句。重構結果如下:
public static int CalculateDepth(BinaryTreeNode<int> t, int n)
{
int depth = 0;
if(t.Value == n)
{
return depth;
}
if((n < t.Value) && (t.Left != null))
{
return 1 + CalculateDepth(t.Left, n);
}
if((n > t.Value) && (t.Right != null))
{
return 1 + CalculateDepth(t.Right, n);
}
throw new TreeExcepyion(“Value not found in tree!”);
}
原本兩層if/else語句改為一層if/else,程式碼變得容易理解,但複雜度並未降低。
我認為第二版本的方法等同是狀態圖化簡的原理,第一版本是直接思考土法煉鋼,第二版本有化簡狀態。
第三版程式為了降低複雜度,將if/else語句提取到獨立的方法,重構結果如下:
public static int CalculateDepth(BinaryTreeNode<int> t, int n)
{
int depth = 0;
if(t.Value == n)
{
return depth;
}
else
{
return TraverseByValue(t, n);
}
}
private static int TraverseByValue(BinaryTreeNode<int> t, int n)
{
BinaryTreeNode<int> childNode = GetChildNode(t, n);
if(childNode == null)
{
throw new TreeExcepyion(“Value not found in tree!”);
}
else
{
return 1 + CalculateDepth(childNode, n);
}
}
private static BinaryTreeNode<int> GetChildNode(BinaryTreeNode<int> t, int n)
{
if(n < t.Value)
{
return t.Left;
}
else
{
return t.Right;
}
}
3.3 常見的反對意見
反對意見:高複雜度無可避免
現實問題複雜必定會讓程式碼複雜,程式設計師的責任要將複雜的程式碼化繁為簡。
反對意見:拆分方法不會降低複雜度
一個方法拆分成幾個新方法,並不會降低McCabe複雜度,卻可以讓程式更容易測試、閱讀與維護。
3.4 參考
留言
張貼留言