打造可維護軟體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  參考


 


 


留言

這個網誌中的熱門文章

異世界NTR web版第三章 觀後感 喧賓奪主 ,反派實力過強

泛而不精的我被逐出了勇者隊伍 web第三章 觀後感 菲莉真能打; 露娜超爽der

持有縮小技能的D級冒險者,與聖女結婚並加入勇者團隊 漫畫 01-04 觀後感 大我與小我