打造可維護軟體C#版 第四章 不撰寫重複的程式碼
第四章針對重複的程式碼,作者介紹兩個重構技巧「提取方法」與「提取父類別」,《Clean Code》的作者Bob大叔提到過大部分的軟體技術都是用來消除重複的程式碼。第四章也可看出「繼承」這兩個字容易誤導初學者。
指導方針:
禁止程式碼重複。
撰寫可以重利用的程式碼,或是使用既有的程式碼。
如果程式碼重複,有可能會在多的地方修正同樣的錯誤。
書中提出一個重複程式碼的惡例
public class CheckingAccount
{
private int transferLimit = 100;
public Transfer MakeTransfer(String counterAccount, Money amount)
{
// 1. Check withdrawal limit:
if (amount.GreaterThan(this.transferLimit))
{
throw new BusinessException("Limit exceeded!");
}
// 2. Assuming result is 9-digit bank account number, validate 11-test:
int sum = 0;
for (int i = 0; i < counterAccount.Length; i++)
{
sum = sum + (9 - i) * (int)Char.GetNumericValue(
counterAccount[i]);
}
if (sum % 11 == 0)
{
// 3. Look up counter account and make transfer object:
CheckingAccount acct = Accounts.FindAcctByNumber(counterAccount);
Transfer result = new Transfer(this, acct, amount);
return result;
}
else
{
throw new BusinessException("Invalid account number!");
}
}
}
public class SavingsAccount
{
public CheckingAccount RegisteredCounterAccount { get; set; }
public Transfer makeTransfer(string counterAccount, Money amount)
{
// 1. Assuming result is 9-digit bank account number, validate 11-test:
int sum = 0; // <1>
for (int i = 0; i < counterAccount.Length; i++)
{
sum = sum + (9 - i) * (int)Char.GetNumericValue(
counterAccount[i]);
}
if (sum % 11 == 0)
{
// 2. Look up counter account and make transfer object:
CheckingAccount acct = Accounts.FindAcctByNumber(counterAccount);
Transfer result = new Transfer(this, acct, amount); // <2>
// 3. Check whether withdrawal is to registered counter account:
if (result.CounterAccount.Equals(this.RegisteredCounterAccount))
{
return result;
}
else
{
throw new BusinessException("Counter-account not registered!");
}
}
else
{
throw new BusinessException("Invalid account number!!");
}
}
}
惡例是複製再上的寫法,若是有貼上N次,當這段程式碼錯誤的時候,就要修改N次。
重複程式碼的類型
重複程式碼的定義是長度至少6行的一段文字相同程式碼,另外一種語法相同的重複本章暫不討論。
4.1 動機
重複程式碼比較難分析
重複程式碼的根本問題是不知其他地方是否存在相同的程式碼、多少份複本、以及位在哪些地方。
重複程式碼比較難修改
4.2 如何運用本指導方針?
「提取方法」的重構技巧正是解決重複程式碼的主要手段。
範例程式添加一個新的靜態方法IsValid
public static bool IsValid(string number)
{
int sum = 0;
for(int i = 0; i < number.Length; i++)
{
sum = sum + (9 - i)*(int)Char.GetNumbericValue(number[i]);
}
return sum%11 == 0;
}
這個方法在CheckingAccount類別中被調用:
public class CheckingAccount
{
private int transferLimit = 100;
public Transfer MakeTransfer(string counterAccount, Money amount)
{
// 1. Check withdrawal limit:
if (amount.GreaterThan(this.transferLimit))
{
throw new BusinessException("Limit exceeded!");
}
if (Accounts.IsValid(counterAccount))
{ // <1>
// 2. Look up counter account and make transfer object:
CheckingAccount acct = Accounts.FindAcctByNumber(counterAccount);
Transfer result = new Transfer(this, acct, amount); // <2>
return result;
}
else
{
throw new BusinessException("Invalid account number!");
}
}
}
SavingsAccount也是:
public class SavingsAccount
{
public CheckingAccount RegisteredCounterAccount { get; set; }
public Transfer MakeTransfer(string counterAccount, Money amount)
{
// 1. Assuming result is 9-digit bank account number, validate 11-test:
if (Accounts.IsValid(counterAccount))
{ // <1>
// 2. Look up counter account and make transfer object:
CheckingAccount acct = Accounts.FindAcctByNumber(counterAccount);
Transfer result = new Transfer(this, acct, amount); // <2>
if (result.CounterAccount.Equals(this.RegisteredCounterAccount))
{
return result;
}
else
{
throw new BusinessException("Counter-account not registered!");
}
}
else
{
throw new BusinessException("Invalid account number!!");
}
}
}
修改之後重複的程式碼小於六行,但仍存在以下問題:
兩個類別存在重複的邏輯。
類別之間會產生緊密耦合,CheckingAccount與SavingsAccount會依賴Accounts。
提取父類別的重構技巧
「提取父類別」將程式碼片段提取成方法,而是提取到原有類別的新建父類別中。運用這項技巧可建立新的Account類別,如下所示:
public class Account
{
public virtual Transfer MakeTransfer(string counterAccount, Money amount)
{
// 1. Assuming result is 9-digit bank account number, validate 11-test:
int sum = 0; // <1>
for (int i = 0; i < counterAccount.Length; i++)
{
sum = sum + (9 - i) * (int)Char.
GetNumericValue(counterAccount[i]);
}
if (sum % 11 == 0)
{
// 2. Look up counter account and make transfer object:
CheckingAccount acct = Accounts.FindAcctByNumber(counterAccount);
Transfer result = new Transfer(this, acct, amount); // <2>
return result;
}
else
{
throw new BusinessException("Invalid account number!");
}
}
}
CheckingAccount 繼承 Account
public class CheckingAccount : Account
{
private int transferLimit = 100;
public override Transfer MakeTransfer(string counterAccount, Money amount)
{
if (amount.GreaterThan(this.transferLimit))
{
throw new BusinessException("Limit exceeded!");
}
return base.MakeTransfer(counterAccount, amount);
}
}
SavingsAccount 繼承 Account
public class SavingsAccount : Account
{
public CheckingAccount RegisteredCounterAccount { get; set; }
public override Transfer MakeTransfer(string counterAccount, Money amount)
{
Transfer result = base.MakeTransfer(counterAccount, amount);
if (result.CounterAccount.Equals(this.RegisteredCounterAccount))
{
return result;
}
else
{
throw new BusinessException("Counter-account not registered!");
}
}
}
Account類別可再改寫為如下:
public class Account
{
public Transfer MakeTransfer(string counterAccount, Money amount)
{
if (IsValid(counterAccount))
{
CheckingAccount acct = Accounts.FindAcctByNumber(counterAccount);
return new Transfer(this, acct, amount);
}
else
{
throw new BusinessException("Invalid account number!");
}
}
public static bool IsValid(string number)
{
int sum = 0;
for (int i = 0; i < number.Length; i++)
{
sum = sum + (9 - i) * (int)Char.GetNumericValue(number[i]);
}
return sum % 11 == 0;
}
}
4.3 常見的反對意見
反對意見:應該允許從其他程式碼基礎複製程式碼
來源程式碼有可能還在維護中,不要直接複製,而是匯入import需要的功能性。
來源程式碼基礎停止維護,而自己正在重建這個程式碼基礎。這種情形更要避免直接複製貼上。
反對意見:細微的變異無可避免,因而導致程式碼重複
還是要想辦法異中求同,找出程式相同的部分,然後將他們移到共用的父類別。
反對意見:這段程式碼永遠不會改變
「諸行無常」每段程式碼都有可能發生變化
系統的功能性需求可能因為使用者、使用行為,或組織業務的改變而發生變更。
組織的所有權、職責、開發方法、開發流程,或法律需求可能發生改變。
技術可能發生變動(通常在系統所處的環境),例如作業系統、第三方程式庫、框架,或者與其他應用程式之間的介面。
程式碼本身可能因為臭蟲、重構,甚至介面外觀的改進而發生變更。
反對意見:應該允許複製完整檔案作為備份
可用SVN或Git版本控管軟體。
反對意見:單元測試會幫我發現問題
單元測試只能進行測試,重複的程式碼是問題的根源。
反對意見:字串實字的重複是不可避免且無害的
程式碼中經常會出現冗長的SQL查詢、XML或HTML。
可用下列方法解決:
提取方法,運用字串串接並搭配參數,來處理當中的差異性。
使用模板引擎(?),利用保存在獨立檔案中、較小的、非重複片段來生成HTML內容。
4.4 參考
留言
張貼留言