前言
前兩天寫了一篇程序猿也愛學英語(上),有圖有真相的文章,寫作那篇文章只是自己一時興起,或者說是自己的興趣使然。文中的觀點只是自己的學習心得和體會,屬一家之言且鑑於本人不是學英語出身,所以也肯定有不正確的地方,也歡迎大家積極討論並給我留言,再次感謝大家的熱烈支持。關於大家詢問下篇的發佈問題,我想我會盡力在週末完成。
這幾天由於剛發佈完項目,所以有比較充裕的時間整理自己的知識庫,發現三年多以前學習並記錄了31天重構系列筆記,至今仍回味無窮,索性重新閱讀、糾正錯誤並重新排版整理出來,希望再次和大家一起分享。
對31天重構系列文章最早接觸是在2009年10月份,由於當時沒有訂閱Sean Chambers的 blog,所以是在國外的社區上閒逛的時候鏈接過去的。基於文章中的重構點都比較常用,所以一口氣看完了整個系列,同時由於這些重構Tips基本上項目都在使用,只是我們沒有專門把它標示和整理出來,所以當時也沒有引起多大的重視。
但就在三年前,當時我們在做一個WPF的重構項目且鑑於團隊成員技術和經驗參差不齊,非常必要專門整理一個重構的綱要,所以就收集和整理了很多的資料(31天重構也在其中)。當然這個系列除了用語重構Tips之外,也非常適合做新系統的代碼規範參考。總而言之:只要有代碼的地方,這個重構規範就很有價值。同時鑑於當時自己剛到新加坡這個美麗的城市,沒有親戚或者朋友,週末也不想出去閒逛,所以才靜下心來用了足足兩天時間學習並寫完了這個系列筆記。
31天重構這個系列和《代碼大全》、《重構:改善既有代碼的設計》比較起來最大的特點就是比較簡單且淺顯易懂。我這系列文章也都是學習並概括Sean Chambers的31天重構的知識要領,所以如果大家對這個筆記有任何的問題或者異議也可以指出,或者大家可以直接去看原文(即可掌握了技術,又可以學習英語!):
http://www.lostechies.com/blogs/sean_chambers/archive/2009/07/31/31-days-of-refactoring.aspx。
代碼下載地址:http://github.com/schambers/days-of-refactoring
目錄
1. 封裝集合
概念:本文所講的封裝集合就是把集合進行封裝,只提供調用端需要的接口。
正文:在很多時候,我們都不希望把一些不必要的操作暴露給調用端,只需要給它所需要的操作或數據就行,那麼做法就是封裝。這個重構在微軟的代碼庫也經常遇到。比如最經典的屬性對字段的封裝就是一個很好的例子,那麼下面我們將看到對集合的封裝,如下代碼所示,調用端只需要一個集合的信息,而我們則提供了一個IList的集合,大家都知道IList具有對集合的所有操作,所以這會帶來很多隱患,最好的做法就是對它進行重構。
using System.Collections.Generic; namespace LosTechies.DaysOfRefactoring.EncapsulateCollection.Before {public class Order{private List<OrderLine> _orderLines;private double _orderTotal;public IList<OrderLine> OrderLines {get { return _orderLines; } }public void AddOrderLine(OrderLine orderLine) { _orderTotal += orderLine.Total; _orderLines.Add(orderLine); }public void RemoveOrderLine(OrderLine orderLine) { orderLine = _orderLines.Find(o => o == orderLine);if (orderLine == null)return; _orderTotal -= orderLine.Total; _orderLines.Remove(orderLine); } }public class OrderLine{public double Total { get; private set; } } }
那麼重構之後,我們把IList換成了IEnumerable,大家都知道只包括一個返回值爲IEnumerator的GetEnumerator()方法,所以這樣只能遍歷取出它的值,而不能對這個集合做出改變,這正是我們所需要的結果,具體代碼如下:
using System.Collections.Generic; namespace LosTechies.DaysOfRefactoring.EncapsulateCollection.After {public class Order{private List<OrderLine> _orderLines;private double _orderTotal;public IEnumerable<OrderLine> OrderLines {get { return _orderLines; } }public void AddOrderLine(OrderLine orderLine) { _orderTotal += orderLine.Total; _orderLines.Add(orderLine); }public void RemoveOrderLine(OrderLine orderLine) { orderLine = _orderLines.Find(o => o == orderLine);if (orderLine == null)return; _orderTotal -= orderLine.Total; _orderLines.Remove(orderLine); } }public class OrderLine{public double Total { get; private set; } } }
總結:這個例子很容易讓我們想到以前系統間耦合常喜歡用數據庫。每個系統都會操作數據庫,並且有些系統還會對數據庫的表結構或字段進行修改,那麼這很容易就會造成維護的地獄,很明智的一個做法就是使用SOA來隔開這些耦合,讓一些只需要數據展示的系統得到自己需要的數據即可。
2. 移動方法
概念:本文所講的移動方法就是方法放在合適的位置(通常指放在合適的類中)。
正文:移動方法是一個很簡單也很常見的重構,只要是系統就會存在很多類,那麼類裏面包括很多方法,如果一個方法經常被另外一個類使用(比本身的類使用還多)或者這個方法本身就不應該放在這個類裏面,那麼這個適合應該考慮把它移到合適的類中。代碼如下:
namespace LosTechies.DaysOfRefactoring.MoveMethod.Before{public class BankAccount{public BankAccount(int accountAge, int creditScore, AccountInterest accountInterest) { AccountAge = accountAge; CreditScore = creditScore; AccountInterest = accountInterest; }public int AccountAge { get; private set; }public int CreditScore { get; private set; }public AccountInterest AccountInterest { get; private set; }public double CalculateInterestRate() {if (CreditScore > 800)return 0.02;if (AccountAge > 10)return 0.03;return 0.05; } }public class AccountInterest{public BankAccount Account { get; private set; }public AccountInterest(BankAccount account) { Account = account; }public double InterestRate {get { return Account.CalculateInterestRate(); } }public bool IntroductoryRate {get { return Account.CalculateInterestRate() < 0.05; } } }}
移動以後大家可以看到BankAccount類的職責也單一,同時CalculateInterestRate也放到了經常使用且適合它的類中了,所以此重構是一個比較好的重構,能讓整個代碼變得更加合理。
namespace LosTechies.DaysOfRefactoring.MoveMethod.After {public class AccountInterest{public BankAccount Account { get; private set; }public AccountInterest(BankAccount account) { Account = account; }public double InterestRate {get { return CalculateInterestRate(); } }public bool IntroductoryRate {get { return CalculateInterestRate() < 0.05; } }public double CalculateInterestRate() {if (Account.CreditScore > 800)return 0.02;if (Account.AccountAge > 10)return 0.03;return 0.05; } } } namespace LosTechies.DaysOfRefactoring.MoveMethod.After {public class BankAccount{public BankAccount(int accountAge, int creditScore, AccountInterest accountInterest) { AccountAge = accountAge; CreditScore = creditScore; AccountInterest = accountInterest; }public int AccountAge { get; private set; }public int CreditScore { get; private set; }public AccountInterest AccountInterest { get; private set; } } }
總結:這個重構法則在很多時候能讓我們把代碼組織的結構調整得更合理,同時也能給以後的維護帶來方便。
3. 提升方法
概念:提升方法是指將一個很多繼承類都要用到的方法提升到基類中。
正文:提升方法是指將一個很多繼承類都要用到的方法提升到基類中,這樣就能減少代碼量,同時讓類的結構更清晰。如下代碼所示,Turn方法在子類Car和Motorcycle 都會用到,因爲Vehicle都會有這個方法,所以我們就會想到把它提到基類中。
namespace LosTechies.DaysOfRefactoring.PullUpMethod.Before {public abstract class Vehicle{// other methods}public class Car : Vehicle{public void Turn(Direction direction) {// code here} }public class Motorcycle : Vehicle{ }public enum Direction{ Left, Right } }
重構後的代碼如下,那麼現在Car 和Motorcycle都具有Turn這個方法,如果這個方法修改也只需要修改基類即可,所以給維護和以後的重構帶來了方便。
namespace LosTechies.DaysOfRefactoring.PullUpMethod.After {public abstract class Vehicle{public void Turn(Direction direction) {// code here} }public class Car : Vehicle{ }public class Motorcycle : Vehicle{ }public enum Direction{ Left, Right } }
總結:這個重構要根據具體情況使用,如果不是每個子類都有這個方法的話,可以考慮使用接口或者其他方式。
4. 降低方法
概念:本文中的降低方法和前篇的提升方法整好相反,也就是把個別子類使用到的方法從基類移到子類裏面去。
正文:如下代碼所示,Animal 類中的方法Bark只有在其子類Dog中使用,所以最好的方案就是把這個方法移到子類Dog 中。
namespace LosTechies.DaysOfRefactoring.PushDownMethod.Before {public abstract class Animal{public void Bark() {// code to bark} }public class Dog : Animal{ }public class Cat : Animal{ } }
重構後的代碼如下,同時如果在父類Animal 中如果沒有其他的字段或者公用方法的話,可以考慮把Bark方法做成一個接口,從而去掉Animal類。
namespace LosTechies.DaysOfRefactoring.PushDownMethod.After {public abstract class Animal{ }public class Dog : Animal{public void Bark() {// code to bark} }public class Cat : Animal{ } }
總結:面向對象三大特徵(繼承、封裝、多態)很多時候可以幫助我們,但同時也可能會造成使用過度或者使用不當,所以如何把握好設計,這個就變得至關重要。在什麼時候使用繼承的方式,在什麼時候使用組合和聚合,接口和繼承類的選擇等久成了我們的重點。
5. 提升字段
概念:本文中的提升字段和前面的提升方法頗爲相似,就是把子類公用的字段提升到基類中,從而達到公用的目的。
正文:如下代碼所示, Account 的兩個子類CheckingAccount和SavingsAccount 都有minimumCheckingBalance 字段,所以可以考慮把這個字段提到基類中。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace LosTechies.DaysOfRefactoring.PullUpField.Before {public abstract class Account{ }public class CheckingAccount : Account{private decimal _minimumCheckingBalance = 5m; }public class SavingsAccount : Account{private decimal _minimumSavingsBalance = 5m; } }
重構後的代碼如下,這樣提的前提是這些子類有一個基類或者有很多相似的字段和方法,不然爲了一個字段而單獨建立一個抽象類是不可取的,所以這個就需要具體權衡。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace LosTechies.DaysOfRefactoring.PullUpField.After {public abstract class Account{protected decimal _minimumBalance = 5m; }public class CheckingAccount : Account{ }public class SavingsAccount : Account{ } }
總結:這個重構的策略比較簡單,同時也是比較常用的一些做法,最主要就是要注意權衡是否真的有這個必要,看這樣做究竟有沒有什麼好處(比如只需要改一個地方,維護簡便了,同時代碼量也更少了等)。
6. 降低字段
概念:本文中的降低字段和前篇的提升字段正好相反,就是把基類中只有某些少數類用到的字段降低到使用它們的子類中。
正文:如下代碼所示,基類Task 類中的_resolution字段只會在子類BugTask中用到,所以就考慮把它放到BugTask 類中。
namespace LosTechies.DaysOfRefactoring.PushDownField.Before {public abstract class Task{protected string _resolution; }public class BugTask : Task{ }public class FeatureTask : Task{ } }
重構後的代碼如下所示,這樣做的好處可以簡化基類,同時讓其他沒有使用它的子類也變得更加簡單,如果這樣的字段比較多的話,使用此重構也能節約一部分內存。
namespace LosTechies.DaysOfRefactoring.PushDownField.After {public abstract class Task{ }public class BugTask : Task{private string _resolution; }public class FeatureTask : Task{ } }
總結:此重構也是一個非常簡單的重構,在很多時候我們都會不自覺的使用它。
7. 重命名(方法,類,參數)
概念:本文中的改名(方法,類,參數)是指在寫代碼的時候對類、方法、參數、委託、事件等等元素取一個有意義的名稱。
正文:如下代碼所示,加入一個公司建立一個員工的類,類中有一個員工名字的字段和一個按照小時計算員工收入的方法,那麼下面代碼的取名就顯得很難理解了,所以我們會重構名稱。
namespace LosTechies.DaysOfRefactoring.Rename.Before {public class Person{public string FN { get; set; }public decimal ClcHrlyPR() {// code to calculate hourly payratereturn 0m; } } }
重構後代碼如下所示,這樣看起來就非常清晰,如果有新進項目組的成員,也會變得很樂意看這個代碼。
namespace LosTechies.DaysOfRefactoring.Rename.After {// Changed the class name to Employeepublic class Employee{public string FirstName { get; set; }public decimal CalculateHourlyPay() {// code to calculate hourly payratereturn 0m; } } }
總結:此重構經常被廣大程序員所忽視,但是帶來的隱患是不可估量的,也許老闆要修改功能,那我們來看這段沒有重構的代碼(就算是自己寫的,但由於時間和項目多等關係,我們也很難理解了),然後就會變得焦頭爛額。相反重構後的代碼就會覺得一目瞭然、賞心悅目。
8. 使用委派代替繼承
概念:本文中的“使用委派代替繼承”是指在根本沒有父子關係的類中使用繼承是不合理的,可以用委派的方式來代替。
如下代碼所示,Child 和Sanitation(公共設施)是沒有邏輯上的父子關係,因爲小孩不可能是一個公共設施吧!所以我們爲了完成這個功能可以考慮使用委派的方式。
namespace LosTechies.DaysOfRefactoring.ReplaceInheritance.Before {public class Sanitation{public string WashHands() {return "Cleaned!"; } }public class Child : Sanitation{ } }
重構後的代碼如下,把Sanitation 委派到Child類中,從而可以使用WashHands這個方法,這種方式我們經常會用到,其實IOC也使用到了這個原理,可以通過構造注入和方法注入等。
namespace LosTechies.DaysOfRefactoring.ReplaceInheritance.After {public class Sanitation{public string WashHands() {return "Cleaned!"; } }public class Child{private Sanitation Sanitation { get; set; }public Child() { Sanitation = new Sanitation(); }public string WashHands() {return Sanitation.WashHands(); } } }
總結:這個重構是一個很好的重構,在很大程度上解決了濫用繼承的情況,很多設計模式也用到了這種思想(比如橋接模式、適配器模式、策略模式等)。
9. 提取接口
概念:本文中的“提取接口” 是指超過一個的類要使用某一個類中部分方法時,我們應該解開它們之間的依賴,讓調用者使用接口,這很容易實現也可以降低代碼的耦合性。
正文:如下代碼所示,RegistrationProcessor 類只使用到了Cla***egistration 類中的Create方法和Total 字段,所以可以考慮把他們做成接口給RegistrationProcessor調用。
namespace LosTechies.DaysOfRefactoring.ExtractInterface.Before {public class Cla***egistration{public void Create() {// create registration code}public void Transfer() {// class transfer code}public decimal Total { get; private set; } }public class RegistrationProcessor{public decimal Proce***egistration(Cla***egistration registration) { registration.Create();return registration.Total; } } }
重構後的代碼如下,我們提取了一個ICla***egistration 接口,同時讓Cla***egistration繼承此接口,然後調用端RegistrationProcessor 就可以直接通過ICla***egistration接口進行調用。
namespace LosTechies.DaysOfRefactoring.ExtractInterface.After {public interface ICla***egistration{void Create();decimal Total { get; } }public class Cla***egistration : ICla***egistration{public void Create() {// create registration code}public void Transfer() {// class transfer code}public decimal Total { get; private set; } }public class RegistrationProcessor{public decimal Proce***egistration(ICla***egistration registration) { registration.Create();return registration.Total; } } }
總結:這個重構策略也是一個常見的運用,很多設計模式也會在其中運用此思想(如簡單工程、抽象工廠等都會通過接口來解開依賴)。
10. 提取方法
概念:本文中的把某些計算複雜的過程按照功能提取成各個小方法,這樣就可以使代碼的可讀性、維護性得到提高。
正文:如下代碼所示,CalculateGrandTotal方法裏面包含了多個邏輯,第一計算subTotal 的總和,第二subTotal 要循環減去discount,也就是計算Discounts,第三就是計算Tax。所以我們可以根據功能把他們拆分成三個小方法。
using System.Collections.Generic; namespace LosTechies.DaysOfRefactoring.ExtractMethod.Before {public class Receipt{private IList<decimal> Discounts { get; set; }private IList<decimal> ItemTotals { get; set; }public decimal CalculateGrandTotal() {decimal subTotal = 0m;foreach (decimal itemTotal in ItemTotals) subTotal += itemTotal;if (Discounts.Count > 0) {foreach (decimal discount in Discounts) subTotal -= discount; }decimal tax = subTotal * 0.065m; subTotal += tax;return subTotal; } } }
重構後的代碼如下,然後CalculateGrandTotal方法就直接調用CalculateSubTotal、CalculateDiscounts、CalculateTax,從而是整個邏輯看起來更加清晰,並且可讀性和維護性也得到了大大提高。
using System.Collections.Generic; namespace LosTechies.DaysOfRefactoring.ExtractMethod.After {public class Receipt{private IList<decimal> Discounts { get; set; }private IList<decimal> ItemTotals { get; set; }public decimal CalculateGrandTotal() {decimal subTotal = CalculateSubTotal(); subTotal = CalculateDiscounts(subTotal); subTotal = CalculateTax(subTotal);return subTotal; }private decimal CalculateTax(decimal subTotal) {decimal tax = subTotal * 0.065m; subTotal += tax;return subTotal; }private decimal CalculateDiscounts(decimal subTotal) {if (Discounts.Count > 0) {foreach (decimal discount in Discounts) subTotal -= discount; }return subTotal; }private decimal CalculateSubTotal() {decimal subTotal = 0m;foreach (decimal itemTotal in ItemTotals) subTotal += itemTotal;return subTotal; } } }
總結:這個重構在很多公司都有一些的代碼規範作爲參考,比如一個類不能超過多少行代碼,一個方法裏面不能超過多少行代碼,這在一定程度上也能使程序員把這些複雜的邏輯剝離成意義很清楚的小方法。
11. 使用策略類
概念:本文中的“使用策略類” 是指用設計模式中的策略模式來替換原來的switch case和if else語句,這樣可以解開耦合,同時也使維護性和系統的可擴展性大大增強。
正文:如下面代碼所示,ClientCode 類會更加枚舉State的值來調用ShippingInfo的不同方法,但是這樣就會產生很多的判斷語句,如果代碼量加大,類變得很大了的話,維護中改動也會變得很大,每次改動一個地方,都要對整個結構進行編譯(假如是多個工程),所以我們想到了對它進行重構,剝開耦合。
namespace LosTechies.DaysOfRefactoring.SwitchToStrategy.Before {public class ClientCode{public decimal CalculateShipping() {ShippingInfo shippingInfo = new ShippingInfo();return shippingInfo.CalculateShippingAmount(State.Alaska); } }public enum State{ Alaska, NewYork, Florida }public class ShippingInfo{public decimal CalculateShippingAmount(State shipToState) {switch (shipToState) {case State.Alaska:return GetAlaskaShippingAmount();case State.NewYork:return GetNewYorkShippingAmount();case State.Florida:return GetFloridaShippingAmount();default:return 0m; } }private decimal GetAlaskaShippingAmount() {return 15m; }private decimal GetNewYorkShippingAmount() {return 10m; }private decimal GetFloridaShippingAmount() {return 3m; } } }
重構後的代碼如下所示,抽象出一個IShippingCalculation 接口,然後把ShippingInfo類裏面的GetAlaskaShippingAmount、GetNewYorkShippingAmount、GetFloridaShippingAmount三個方法分別提煉成三個類,然後繼承自IShippingCalculation接口,這樣在調用的時候就可以通過IEnumerable<IShippingCalculation> 來解除之前的switch case語句,這和IOC的做法頗爲相似。
using System; using System.Collections.Generic; using System.Linq; namespace LosTechies.DaysOfRefactoring.SwitchToStrategy.After_WithIoC {public interface IShippingInfo{decimal CalculateShippingAmount(State state); }public class ClientCode{ [Inject]public IShippingInfo ShippingInfo { get; set; }public decimal CalculateShipping() {return ShippingInfo.CalculateShippingAmount(State.Alaska); } }public enum State{ Alaska, NewYork, Florida }public class ShippingInfo : IShippingInfo{private IDictionary<State, IShippingCalculation> ShippingCalculations { get; set; }public ShippingInfo(IEnumerable<IShippingCalculation> shippingCalculations) { ShippingCalculations = shippingCalculations.ToDictionary(calc => calc.State); }public decimal CalculateShippingAmount(State shipToState) {return ShippingCalculations[shipToState].Calculate(); } }public interface IShippingCalculation{State State { get; }decimal Calculate(); }public class AlaskShippingCalculation : IShippingCalculation{public State State { get { return State.Alaska; } }public decimal Calculate() {return 15m; } }public class NewYorkShippingCalculation : IShippingCalculation{public State State { get { return State.NewYork; } }public decimal Calculate() {return 10m; } }public class FloridaShippingCalculation : IShippingCalculation{public State State { get { return State.Florida; } }public decimal Calculate() {return 3m; } } }
總結:這種重構在設計模式當中把它單獨取了一個名字——策略模式,這樣做的好處就是可以隔開耦合,以注入的形式實現功能,這使增加功能變得更加容易和簡便,同樣也增強了整個系統的穩定性和健壯性。
12. 分解依賴
概念:本文中的“分解依賴” 是指對部分不滿足我們要求的類和方法進行依賴分解,通過裝飾器來達到我們需要的功能。
正文:正如下面代碼所示,如果你要在你的代碼中加入單元測試但有一部分代碼是你不想測試的,那麼你應用使用這個的重構。下面的例子中我們應用靜態類來完成某些工作,但問題是在單元測試時我們無法mock靜態類,所以我們只能引入靜態類的裝飾接口來分解對靜態類的依賴。從而我們使我們的調用類只需要依賴於裝飾接口就能完成這個操作。
namespace LosTechies.DaysOfRefactoring.BreakDependencies.Before {public class AnimalFeedingService{private bool FoodBowlEmpty { get; set; }public void Feed() {if (FoodBowlEmpty)Feeder.ReplenishFood();// more code to feed the animal} }public static class Feeder{public static void ReplenishFood() {// fill up bowl} } }
重構後代碼如下,我們添加一個接口和一個實現類,在實現類中調用靜態類的方法,所以說具體做什麼事情沒有改變,改變的只是形式,但這樣做的一個好處是增加了了代碼的可測試性。在應用了分解依賴模式後,我們就可以在單元測試的時候mock一個IFeederService對象並通過AnimalFeedingService的構造函數傳遞給它。這樣就可以完成我們需要的功能。
namespace LosTechies.DaysOfRefactoring.BreakDependencies.After {public class AnimalFeedingService{public IFeederService FeederService { get; set; }public AnimalFeedingService(IFeederService feederService) { FeederService = feederService; }private bool FoodBowlEmpty { get; set; }public void Feed() {if (FoodBowlEmpty) FeederService.ReplenishFood();// more code to feed the animal} }public interface IFeederService{void ReplenishFood(); }public class FeederService : IFeederService{public void ReplenishFood() {Feeder.ReplenishFood(); } }public static class Feeder{public static void ReplenishFood() {// fill up bowl} } }
總結:這個重構在很多時候和設計模式中的一些思想類似,使用中間的裝飾接口來分解兩個類之間的依賴,對類進行裝飾,然後使它滿足我們所需要的功能。
13. 提取方法對象
概念:本文中的“提取方法對象”是指當你發現一個方法中存在過多的局部變量時,你可以通過使用“提取方法對象”重構來引入一些方法,每個方法完成任務的一個步驟,這樣可以使得程序變得更具有可讀性。
正文:如下代碼所示,Order 類中的Calculate方法要完成很多功能,在之前我們用“提取方法”來進行重構,現在我們採取“提取方法對象”來完成重構。
using System.Collections.Generic; namespace LosTechies.DaysOfRefactoring.ExtractMethodObject.Before {public class OrderLineItem{public decimal Price { get; private set; } }public class Order{private IList<OrderLineItem> OrderLineItems { get; set; }private IList<decimal> Discounts { get; set; }private decimal Tax { get; set; }public decimal Calculate() {decimal subTotal = 0m;// Total up line itemsforeach (OrderLineItem lineItem in OrderLineItems) { subTotal += lineItem.Price; }// Subtract Discountsforeach (decimal discount in Discounts) subTotal -= discount;// Calculate Taxdecimal tax = subTotal * Tax;// Calculate GrandTotaldecimal grandTotal = subTotal + tax;return grandTotal; } } }
正如下代碼所示,我們引入了OrderCalculator類,該類實現了所有的計算方法,Order類將自身傳遞給 OrderCalculator類並調用Calculate方法完成計算過程。
using System.Collections.Generic; namespace LosTechies.DaysOfRefactoring.ExtractMethodObject.After {public class OrderLineItem{public decimal Price { get; private set; } }public class Order{public IEnumerable<OrderLineItem> OrderLineItems { get; private set; }public IEnumerable<decimal> Discounts { get; private set; }public decimal Tax { get; private set; }public decimal Calculate() {return new OrderCalculator(this).Calculate(); } }public class OrderCalculator{private decimal SubTotal { get; set; }private IEnumerable<OrderLineItem> OrderLineItems { get; set; }private IEnumerable<decimal> Discounts { get; set; }private decimal Tax { get; set; }public OrderCalculator(Order order) { OrderLineItems = order.OrderLineItems; Discounts = order.Discounts; Tax = order.Tax; }public decimal Calculate() { CalculateSubTotal(); SubtractDiscounts(); CalculateTax();return SubTotal; }private void CalculateSubTotal() {// Total up line itemsforeach (OrderLineItem lineItem in OrderLineItems) SubTotal += lineItem.Price; }private void SubtractDiscounts() {// Subtract Discountsforeach (decimal discount in Discounts) SubTotal -= discount; }private void CalculateTax() {// Calculate TaxSubTotal += SubTotal * Tax; } } }
總結:本文的重構方法在有的時候還是比較有用,但這樣會造成字段的增加,同時也會帶來一些維護的不便,它和“提取方法”最大的區別就是一個通過方法返回需要的數據,另一個則是通過字段來存儲方法的結果值,所以在很大程度上我們都會選擇“提取方法”。
14. 分離職責
概念:本文中的“分離職責”是指當一個類有許多職責時,將部分職責分離到獨立的類中,這樣也符合面向對象的五大特徵之一的單一職責原則,同時也可以使代碼的結構更加清晰,維護性更高。
正文:如下代碼所示,Video類有兩個職責,一個是處理video rental,另一個是計算每個客戶的總租金。我們可以將這兩個職責分離出來,因爲計算每個客戶的總租金可以在Customer計算,這也比較符合常理。
using System.Collections.Generic;using System.Linq;namespace LosTechies.DaysOfRefactoring.BreakResponsibilities.Before{public class Video{public void PayFee(decimal fee) { }public void RentVideo(Video video, Customer customer) { customer.Videos.Add(video); }public decimal CalculateBalance(Customer customer) { returncustomer.LateFees.Sum(); } }public class Customer{public IList<decimal> LateFees { get; set; }public IList<Video> Videos { get; set; } }}
重構後的代碼如下,這樣Video 的職責就變得很清晰,同時也使代碼維護性更好。
using System.Collections.Generic;using System.Linq;namespace LosTechies.DaysOfRefactoring.BreakResponsibilities.After{public class Video{public void RentVideo(Video video, Customer customer) { customer.Videos.Add(video); } }public class Customer{public IList<decimal> LateFees { get; set; }public IList<Video> Videos { get; set; }public void PayFee(decimal fee) { }public decimal CalculateBalance(Customer customer) {return customer.LateFees.Sum(); } }}
總結:這個重構經常會用到,它和之前的“移動方法”有幾分相似之處,讓方法放在合適的類中,並且簡化類的職責,同時這也是面向對象五大原則之一和設計模式中的重要思想。
15. 移除重複內容
概念:本文中的“移除重複內容”是指把一些很多地方都用到的邏輯提煉出來,然後提供給調用者統一調用。
正文:如下代碼所示,ArchiveRecord和CloseRecord都會用到Archived = true; 和DateArchived = DateTime.Now; 這兩條語句,所以我們就可以對它進行重構。
using System; namespace LosTechies.DaysOfRefactoring.RemoveDuplication.Before {public class MedicalRecord{public DateTime DateArchived { get; private set; }public bool Archived { get; private set; }public void ArchiveRecord() { Archived = true; DateArchived = DateTime.Now; }public void CloseRecord() { Archived = true; DateArchived = DateTime.Now; } } }
重構後的代碼如下所示,我們提煉了SwitchToArchived方法來封裝公用的操作,然後給ArchiveRecord和CloseRecord統一調用。
using System; namespace LosTechies.DaysOfRefactoring.RemoveDuplication.After {public class MedicalRecord{public DateTime DateArchived { get; private set; }public bool Archived { get; private set; }public void ArchiveRecord() { SwitchToArchived(); }public void CloseRecord() { SwitchToArchived(); }private void SwitchToArchived() { Archived = true; DateArchived = DateTime.Now; } } }
總結:這個重構很簡單,絕大多數程序員都會使用這種重構方法,但有時由於習慣、時間、趕進度等原因而忽略它,所以會使得整個系統雜亂無章,到處都是Ctrl+C和Ctrl+V的痕跡。
16. 封裝條件
概念:本文中的“封裝條件”是指條件關係比較複雜時,代碼的可讀性會比較差,所以這時我們應當根據條件表達式是否需要參數將條件表達式提取成可讀性更好的屬性或者方法,如果條件表達式不需要參數則可以提取成屬性,如果條件表達式需要參數則可以提取成方法。
正文:如下代碼所示,PerformCoolFunction裏面的if條件判斷比較複雜,看起來有點雜亂,所以就把它提出來。
using System; namespace LosTechies.DaysOfRefactoring.EncapsulateConditional.Before {public class RemoteControl{private string[] Functions { get; set; }private string Name { get; set; }private int CreatedYear { get; set; }public string PerformCoolFunction(string buttonPressed) {// Determine if we are controlling some extra function // that requires special conditionsif (Functions.Length > 1 && Name == "RCA" && CreatedYear > DateTime.Now.Year - 2)return "doSomething"; } } }
如下代碼所示,我們把條件表達式封裝成HasExtraFunctions屬性,這樣先前的條件判斷就成了if (HasExtraFunctions) ,所以這樣就在很大程度上提高了可讀性。
using System; namespace LosTechies.DaysOfRefactoring.EncapsulateConditional.After {public class RemoteControl{private string[] Functions { get; set; }private string Name { get; set; }private int CreatedYear { get; set; }private bool HasExtraFunctions {get { return Functions.Length > 1 && Name == "RCA" && CreatedYear > DateTime.Now.Year - 2; } }public string PerformCoolFunction(string buttonPressed) {// Determine if we are controlling some extra function // that requires special conditionsif (HasExtraFunctions)return "doSomething"; } } }
總結:這個重構在很大程度上能改善代碼的可讀性,尤其是在一個邏輯很複雜的應用中,把這些條件判斷封裝成一個有意義的名字,這樣很複雜的邏輯也會立刻變得簡單起來。
17. 提取父類
概念:本文中的“提取父類”是指類中有一些字段或方法,你想把它們提取到父類中以便同一繼承層次的其它類也可以訪問他們,這個和之前的很多重構有異曲同工之處。
正文:Dog 類中的EatFood和Groom有可能被其他類用到,因爲他們都是動物的一些公有性質,所以這個時候我們就會考慮對它進行提煉。
namespace LosTechies.DaysOfRefactoring.ExtractSuperclass.Before{ public class Dog { public void EatFood() { // eat some food } public void Groom() { // perform grooming } } }
代碼如下所示,提取了Animal 方法來封裝公用的EatFood和Groom類,從而使其他繼承了Animal 類的子類都可以使用這兩個方法了。
namespace LosTechies.DaysOfRefactoring.ExtractSuperclass.After {public class Animal{public void EatFood() {// eat some food}public void Groom() {// perform grooming} }public class Dog : Animal{ } }
總結:這個重構是典型的繼承用法,很多程序員都會選擇這樣做,但是要注意正確的使用,不要造成過度使用了繼承,如果過度使用了,請考慮用接口、組合和聚合來實現。
18. 使用條件判斷代替異常
概念:本文中的“使用條件判斷代替異常”是指把沒有必要使用異常做判斷的條件儘量改爲條件判斷。
正文:如下代碼所示,在日常的編碼中我們經常需要用到異常來控制程序流,Start方法裏面用trycatch 做條件判斷,我們知道這裏沒有必要使用這種方式,因爲你不需要做類型不可控的類型轉換,也不需要處理異常行爲,所以我們應該對它進行重構。
namespace LosTechies.DaysOfRefactoring.ReplaceException.Before {public class Microwave{private IMicrowaveMotor Motor { get; set; }public bool Start(object food) {bool foodCooked = false;try{ Motor.Cook(food); foodCooked = true; }catch (InUseException) { foodcooked = false; }return foodCooked; } } }
重構後的代碼如下所示,try catch做條件判斷的語句改成了if return的方式,這樣在很多程度上統一了代碼的書寫,同時也提高了性能。
namespace LosTechies.DaysOfRefactoring.ReplaceException.After {public class Microwave{private IMicrowaveMotor Motor { get; set; }public bool Start(object food) {if (Motor.IsInUse)return false; Motor.Cook(food);return true; } } }
總結: 這個重構在項目代碼中也經常用到,因爲對於一部分程序員,是很難把握什麼時候用trycatch ,什麼地方該用trycatch 。記得之前大家還專門討論過這些,比如如何用好以及在大中型項目中應該把它放在哪一個組件中等。
19. 提取工廠類
概念:本文中的“提取工廠類”是指如果要創建的對象很多,則代碼會變的很複雜。一種很好的方法就是提取工廠類。
正文:一般來說我們需要在代碼中設置一些對象,以便獲得它們的狀態,從而使用對象,所謂的設置通常來說就是創建對象的實例並調用對象的方法。有時如果要創建的對象很多,則代碼會變的很複雜。這便是工廠模式發揮作用的情形。工廠模式的複雜應用是使用抽象工廠創建對象集,但我們在這裏只是使用基本的工廠類創建對象的一個簡單應用。
如下代碼所示,New方法包含創建類的整個邏輯,如果現在要創建的類比較多而且邏輯比較複雜的話(如根據不同條件創建對象,什麼時候創建對象),我們的New方法邏輯會變得很大,同時代碼也變得很難維護。所以我們就會採用提取工廠類的方式進行提煉。
namespace LosTechies.DaysOfRefactoring.ExtractServiceClass.Before{public class PoliceCarController{public PoliceCar New(int mileage, bool serviceRequired) { PoliceCar policeCar = new PoliceCar(); policeCar.ServiceRequired = serviceRequired; policeCar.Mileage = mileage;return policeCar; } }}
那麼重構後的代碼如下,New方法變得很簡單了,指需要調用實現接IPoliceCarFactory 接口的PoliceCarFactory 類就可以返回對象,這樣就隔開了創建對象的邏輯,如果需求現在變爲根據不同的條件創建不同的對象,什麼時候創建對象等都變成了比較簡單的事情,在後期可以把對象都配置在XML裏面,使用反射的方式實現IOC注入創建。
namespace LosTechies.DaysOfRefactoring.ExtractServiceClass.After {public interface IPoliceCarFactory{ PoliceCar Create(int mileage, bool serviceRequired); }public class PoliceCarFactory : IPoliceCarFactory{public PoliceCar Create(int mileage, bool serviceRequired) { PoliceCar policeCar = new PoliceCar(); policeCar.ReadForService = serviceRequired; policeCar.Mileage = mileage;return policeCar; } }public class PoliceCarController{public IPoliceCarFactory PoliceCarFactory { get; set; }public PoliceCarController(IPoliceCarFactory policeCarFactory) { PoliceCarFactory = policeCarFactory; }public PoliceCar New(int mileage, bool serviceRequired) {return PoliceCarFactory.Create(mileage, serviceRequired); } } }
總結:這個重構經常會在項目中使用,如果要創建的對象是一個,你可以採用簡單工廠,但是這種方式還是會存在很多依賴,維護起來也比較不方便。所以推薦使用工廠方法模式,把實例化延遲到子類。如果你要創建一系列的對象,那麼就推薦你使用抽象工廠模式,但是要注意不要過度設計,只要能滿足不斷變化的需求和給以後的維護和重構帶來方便即可。
20. 提取子類
概念:本文中的”提取子類”是指把基類中的一些不是所有子類都需要訪問的方法調整到子類中。
正文:當你的基類中存在一些方法不是所有的子類都需要訪問,你想將它們調整到子類中時,這個重構會變得很有用了。如下代碼所示,我們需要一個 Registration類用來處理學生選課的信息。但是當Registration類開始工作後,我們意識到我們會在兩種不同的上下文中使用Registration類,NonRegistrationAction和Notes只有在我們處理未註冊情況下纔用到。
所以我們將NonRegistration和Notes提到單獨的NonRegistration類中。using System; namespace LosTechies.DaysOfRefactoring.SampleCode.ExtractSubclass.Before {public class Registration{public NonRegistrationAction Action { get; set; }public decimal RegistrationTotal { get; set; }public string Notes { get; set; }public string Description { get; set; }public DateTime RegistrationDate { get; set; } } }
重構後的代碼如下所示,這樣也滿足面向對象五大原則之一的單一職責。同時也讓類的結構變得更加清晰,增強了可維護性。
using System; namespace LosTechies.DaysOfRefactoring.SampleCode.ExtractSubclass.After {public class Registration{public decimal RegistrationTotal { get; set; }public string Description { get; set; }public DateTime RegistrationDate { get; set; } }public class NonRegistration : Registration{public NonRegistrationAction Action { get; set; }public string Notes { get; set; } } }
總結:這個重構方法經常用來規範類的職責,和之前的一些重構方法也有些類似。
21. 合併繼承
概念:本文中的”合併繼承”是指如果子類的屬性和方法也適合於基類,那麼就可以移除子類,從而減少依賴關係。
正文:上一篇我們講到“提取子類”重構是指當基類中的一個責任不被所有的子類所需要時,將這些責任提取到合適的子類中。而我們今天所要講的的“合併繼承”重構一般用在當我們覺得不需要子類的時候。
如下代碼所示,StudentWebSite子類除了有一個屬性用來說明網站是否是活動的外沒有別的責任,在這種情形下我們意識到IsActive屬性可以應用到所有的網站,所以我們可以將IsActive屬性上移到基類中,並去掉StudentWebSite類。
using System.Collections.Generic; namespace LosTechies.DaysOfRefactoring.SampleCode.CollapseHierarchy.Before {public class Website{public string Title { get; set; }public string Description { get; set; }public IEnumerable<Webpage> Pages { get; set; } }public class StudentWebsite : Website{public bool IsActive { get; set; } } }重構後的代碼如下:
using System.Collections.Generic; namespace LosTechies.DaysOfRefactoring.SampleCode.CollapseHierarchy.After {public class Website{public string Title { get; set; }public string Description { get; set; }public IEnumerable<Webpage> Pages { get; set; }public bool IsActive { get; set; } } }
總結: 這篇和上篇其實最主要論述了子類和父類的繼承關係以及如何判斷什麼時候需要使用繼承,一般我們都能處理好這些關係,所以相對比較簡單。
22. 分解方法
概念:本文中的”分解方法”是指把我們所做的這個功能不停的分解方法,直到將一個大方法分解爲名字有意義且可讀性更好的若干個小方法。
正文:如下代碼所示,因爲現實中AcceptPayment方法不會做這麼多的事情。,所以我們通過幾次分解將 AcceptPayment拆分成若干個名字有意義且可讀性更好的小方法。
using System.Collections.Generic; using System.Linq; namespace LosTechies.DaysOfRefactoring.SampleCode.BreakMethod.Before {public class CashRegister{public CashRegister() { Tax = 0.06m; }private decimal Tax { get; set; }public void AcceptPayment(Customer customer, IEnumerable<Product> products, decimal payment) {decimal subTotal = 0m;foreach (Product product in products) { subTotal += product.Price; }foreach (Product product in products) { subTotal -= product.AvailableDiscounts; }decimal grandTotal = subTotal * Tax; customer.DeductFromAccountBalance(grandTotal); } }public class Customer{public void DeductFromAccountBalance(decimal amount) {// deduct from balance} }public class Product{public decimal Price { get; set; }public decimal AvailableDiscounts { get; set; } } }
重構後的代碼如下,我們把AcceptPayment的內部邏輯拆分成了CalculateSubtotal、SubtractDiscounts、AddTax、SubtractFromCustomerBalance四個功能明確且可讀性更好的小方法。
using System.Collections.Generic; namespace LosTechies.DaysOfRefactoring.SampleCode.BreakMethod.After {public class CashRegister{public CashRegister() { Tax = 0.06m; }private decimal Tax { get; set; }private IEnumerable<Product> Products { get; set; }public void AcceptPayment(Customer customer, IEnumerable<Product> products, decimal payment) {decimal subTotal = CalculateSubtotal(); subTotal = SubtractDiscounts(subTotal);decimal grandTotal = AddTax(subTotal); SubtractFromCustomerBalance(customer, grandTotal); }private void SubtractFromCustomerBalance(Customer customer, decimal grandTotal) { customer.DeductFromAccountBalance(grandTotal); }private decimal AddTax(decimal subTotal) {return subTotal * Tax; }private decimal SubtractDiscounts(decimal subTotal) {foreach (Product product in Products) { subTotal -= product.AvailableDiscounts; }return subTotal; }private decimal CalculateSubtotal() {decimal subTotal = 0m;foreach (Product product in Products) { subTotal += product.Price; }return subTotal; } }public class Customer{public void DeductFromAccountBalance(decimal amount) {// deduct from balance} }public class Product{public decimal Price { get; set; }public decimal AvailableDiscounts { get; set; } } }
總結:其實這個重構和我們前面講的“提取方法”和“提取方法對象”如出一轍,尤其是“提取方法”,所以大家只要知道用這種思想重構就行。
23. 引入參數對象
概念:本文中的“引入參數對象”是指當一個方法的參數過多或者過爲複雜時,可以考慮把這些參數封裝成一個單獨的類。
正文:如果一個方法所需要的參數大於5個,理解該方法的簽名就變得比較困難,因爲這樣感覺參數很長、樣式不好並且沒有分類,所以我們有必要把參數進行封裝。
namespace LosTechies.DaysOfRefactoring.SampleCode.ParameterObject.Before {public class Registration{public void Create(decimal amount, Student student, IEnumerable<Course> courses, decimal credits) {// do work} } }
通常這種情形下創建一個用戶傳遞參數的類是很有幫助的,這會使得代碼更容易明白也更靈活,因爲當你需要增加參數時,只需要給參數類添加一個屬性即可。請注意只有當你發現方法的參數比較多時才應該應用該重構,如果方法的參數比較少,就沒有必要應用此重構,因爲該重構會增加系統中類的數量,同時也會加大維護負擔。所以要看參數情況而定。
重構後的代碼如下:
using System.Collections.Generic; namespace LosTechies.DaysOfRefactoring.SampleCode.ParameterObject.After {public class RegistrationContext{public decimal Amount { get; set; }public Student Student { get; set; }public IEnumerable<Course> Courses { get; set; }public decimal Credits { get; set; } }public class Registration{public void Create(RegistrationContext registrationContext) {// do work} } }
總結:這種重構很重要,尤其是當一個方法的參數比較多的時候,不管是大中型項目還是小型項目,都會遇到這種場景,所以建議大家多使用這個重構。這種封裝的思想在SOA 裏面也經常運用到,封裝輸入Message,封裝輸出Message,消息來和消息去以及消息間的交互就構成了整個應用體系。
24. 分解複雜判斷
概念:本文中的”分解複雜判斷”是指把原來複雜的條件判斷等語句用盡快返回等方式簡化代碼。
正文:簡單的來說,當你的代碼中有很深的嵌套條件時,花括號就會在代碼中形成一個長長的箭頭。我們經常在不同的代碼中看到這種情況,並且這種情況也會擾亂代碼的可讀性。
如下代碼所示,HasAccess方法裏面包含一些嵌套條件,如果再加一些條件或者增加複雜度,那麼代碼就很可能出現幾個問題:1,可讀性差。 2,很容易出現異常。 3,性能較差。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace LosTechies.DaysOfRefactoring.SampleCode.ArrowheadAntipattern.Before {public class Security{public ISecurityChecker SecurityChecker { get; set; }public Security(ISecurityChecker securityChecker) { SecurityChecker = securityChecker; }public bool HasAccess(User user, Permission permission, IEnumerable<Permission> exemptions) {bool hasPermission = false;if (user != null) {if (permission != null) {if (exemptions.Count() == 0) {if (SecurityChecker.CheckPermission(user, permission) || exemptions.Contains(permission)) { hasPermission = true; } } } }return hasPermission; } } }
那麼重構上面的代碼也很簡單,如果有可能的話,儘量將條件從方法中移除,我們讓代碼在做處理任務之前先檢查條件,如果條件不滿足就儘快返回,不繼續執行。下面是重構後的代碼:
using System.Collections.Generic; using System.Linq; namespace LosTechies.DaysOfRefactoring.SampleCode.ArrowheadAntipattern.After {public class Security{public ISecurityChecker SecurityChecker { get; set; }public Security(ISecurityChecker securityChecker) { SecurityChecker = securityChecker; }public bool HasAccess(User user, Permission permission, IEnumerable<Permission> exemptions) {if (user == null || permission == null)return false;if (exemptions.Contains(permission))return true;return SecurityChecker.CheckPermission(user, permission); } } }
總結:這個重構很重要,它和後面講的”儘快返回“有些類似,我們在做複雜的處理過程時,要經常考慮這個重構,用好了它,會對我們的幫助很大。
25. 引入契約式設計
概念:本文中的”引入契約式設計”是指我們應該對應該對輸入和輸出進行驗證,以確保系統不會出現我們所想象不到的異常和得不到我們想要的結果。
正文:契約式設計規定方法應該對輸入和輸出進行驗證,這樣你便可以保證你得到的數據是可以工作的,一切都是按預期進行的,如果不是按預期進行,異常或是錯誤就應該被返回,下面我們舉的例子中,我們方法中的參數可能會值爲null的情況,在這種情況下由於我們沒有驗證,NullReferenceException異常會報出。另外在方法的結尾處我們也沒有保證會返回一個正確的decimal值給調用方法的對象。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace LosTechies.DaysOfRefactoring.SampleCode.Day25_DesignByContract {public class CashRegister{public decimal TotalOrder(IEnumerable<Product> products, Customer customer) {decimal orderTotal = products.Sum(product => product.Price); customer.Balance += orderTotal;return orderTotal; } } }
對上面的代碼重構是很簡單的,首先我們處理不會有一個null值的customer對象,檢查我們最少會有一個product對象。在返回訂單總和之前先確保我們會返回一個有意義的值。如果上面說的檢查有任何一個失敗,我們就拋出對應的異常,並在異常裏說明錯誤的詳細信息,而不是直接拋出NullReferenceException。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Contracts; namespace LosTechies.DaysOfRefactoring.SampleCode.DesignByContract.After {public class CashRegister{public decimal TotalOrder(IEnumerable<Product> products, Customer customer) {if (customer == null)throw new ArgumentNullException("customer", "Customer cannot be null");if (products.Count() == 0)throw new ArgumentException("Must have at least one product to total", "products");decimal orderTotal = products.Sum(product => product.Price); customer.Balance += orderTotal;if (orderTotal == 0)throw new ArgumentOutOfRangeException("orderTotal", "Order Total should not be zero");return orderTotal; } } }
上面的代碼中添加了額外的代碼來進行驗證,雖然看起來代碼複雜度增加了,但我認爲這是非常值得做的,因爲當NullReferenceException發生時去追查異常的詳細信息真是很令人討厭的事情。
總結:微軟在處理代碼乃至產品的時候,很喜歡應用此重構,你如果認真看它的代碼庫,認真看一下WCF的設計,就不難發現了。這個重構建議大家經常使用,這會增強整個系統的穩定性和健壯性。
26. 避免雙重否定
概念:本文中的“避免雙重否定”是指把代碼中的雙重否定語句修改成簡單的肯定語句,這樣即讓代碼可讀,同時也給維護帶來了方便。
正文:避免雙重否定重構本身非常容易實現,但我們卻在太多的代碼中見過因爲雙重否定降低了代碼的可讀性以致於非常讓人容易誤解真正意圖。存在雙重否定的代碼具有非常大的危害性,因爲這種類型的代碼容易引起錯誤的假設,錯誤的假設又會導致書寫出錯誤的維護代碼,最終會導致bug產生。具體可以看下面的代碼:
using System.Collections.Generic; using LosTechies.DaysOfRefactoring.SampleCode.BreakMethod.After; namespace LosTechies.DaysOfRefactoring.SampleCode.DoubleNegative.Before {public class Order{public void Checkout(IEnumerable<Product> products, Customer customer) {if (!customer.IsNotFlagged) {// the customer account is flagged // log some errors and returnreturn; }// normal order processing} }public class Customer{public decimal Balance { get; private set; }public bool IsNotFlagged {get { return Balance < 30m; } } } }如上代碼中的雙重否定可讀性非常低,因爲我們很難搞明白雙重否定的正確值。要重構它也非常容易,如下是重構後的代碼:
using System.Collections.Generic; using LosTechies.DaysOfRefactoring.SampleCode.BreakMethod.After; namespace LosTechies.DaysOfRefactoring.SampleCode.DoubleNegative.After {public class Order{public void Checkout(IEnumerable<Product> products, Customer customer) {if (customer.IsFlagged) {// the customer account is flagged // log some errors and returnreturn; }// normal order processing} }public class Customer{public decimal Balance { get; private set; }public bool IsFlagged {get { return Balance >= 30m; } } } }
總結: “雙重否定”很容易讓人產生錯誤的判斷,也很難讓人理解你的代碼,所以這個重構在我們的代碼中是很重要的,尤其是在判斷條件很多且業務複雜的時候。
27. 去除上帝類
概念:本文中的“去除上帝類”是指把一個看似功能很強且很難維護的類,按照職責把自己的屬性或方法分派到各自的類中或分解成功能明確的類,從而去掉上帝類。
正文:我們經常可以在一些原來的代碼中見到一些類明確違反了SRP原則(單一原則),這些類通常以“Utils”或“Manager”後綴結尾,但有時這些類也沒有這些特徵,它僅僅是多個類多個方法的組合。另一個關於上帝類的特徵是通常這些類中的方法被用註釋分隔爲不同的分組。那麼久而久之,這些類被轉換爲那些沒有人願意進行歸併到合適類的方法的聚集地,對這些類進行重構是將類中的代碼按照職責分派到各自的類中,這樣就解除了上帝類,也減輕了維護的負擔。
using System.Collections.Generic; using LosTechies.DaysOfRefactoring.EncapsulateCollection.After; using LosTechies.DaysOfRefactoring.SampleCode.BreakMethod.After; using Customer = LosTechies.DaysOfRefactoring.BreakResponsibilities.After.Customer; namespace LosTechies.DaysOfRefactoring.SampleCode.RemoveGodClasses.Before {public class CustomerService{public decimal CalculateOrderDiscount(IEnumerable<Product> products, Customer customer) {// do work}public bool CustomerIsValid(Customer customer, Order order) {// do work}public IEnumerable<string> GatherOrderErrors(IEnumerable<Product> products, Customer customer) {// do work}public void Register(Customer customer) {// do work}public void ForgotPassword(Customer customer) {// do work} } }我們看到要重構上面的代碼是很簡單的,只要將相關的方法按職責分派到對應的類中即可,帶來的好處就是這會降低代碼的顆粒度並減少未來維護代碼的成本。下面是重構後的代碼,它將上面 的代碼按照職責分爲了兩個不同的類。
using System.Collections.Generic; using LosTechies.DaysOfRefactoring.EncapsulateCollection.After; using LosTechies.DaysOfRefactoring.SampleCode.BreakMethod.After; using Customer = LosTechies.DaysOfRefactoring.BreakResponsibilities.After.Customer; namespace LosTechies.DaysOfRefactoring.SampleCode.RemoveGodClasses.After {public class CustomerOrderService{public decimal CalculateOrderDiscount(IEnumerable<Product> products, Customer customer) {// do work}public bool CustomerIsValid(Customer customer, Order order) {// do work}public IEnumerable<string> GatherOrderErrors(IEnumerable<Product> products, Customer customer) {// do work} }public class CustomerRegistrationService{public void Register(Customer customer) {// do work}public void ForgotPassword(Customer customer) {// do work} } }
總結: “去除上帝類”是我們經常容易造成的,第一是因爲簡便,看到有一個現成的類,大家都會喜歡把代碼往裏面寫,最後導致越寫越大,並且聲明功能都有,這樣即降低了可讀性,也造成了維護的負擔。
28. 爲布爾方法命名
概念:本文中的“爲布爾方法命名”是指如果一個方法帶有大量的bool 參數時,可以根據bool 參數的數量,提取出若干個獨立的方法來簡化參數。
正文:我們現在要說的重構並不是普通字面意義上的重構,它有很多值得討論的地方。當一個方法帶有大量的bool參數時,會導致方法很容易被誤解併產生非預期的行爲,
根據布爾型參數的數量,我們可以決定提取出若干個獨立的方法來。具體代碼如下:
using LosTechies.DaysOfRefactoring.BreakResponsibilities.After; namespace LosTechies.DaysOfRefactoring.SampleCode.RenameBooleanMethod.Before {public class BankAccount{public void CreateAccount(Customer customer, bool withChecking, bool withSavings, bool withStocks) {// do work} } }我們可以將上面的bool參數以獨立方法的形式暴露給調用端以提高代碼的可讀性,同時我們還需要將原來的方法改爲private以限制其可訪問性。顯然我們關於要 提取的獨立方法會有一個很大的排列組合,這是一大缺點,所以我們可以考慮引入”參數對象“重構。
using LosTechies.DaysOfRefactoring.BreakResponsibilities.After; namespace LosTechies.DaysOfRefactoring.SampleCode.RenameBooleanMethod.After {public class BankAccount{public void CreateAccountWithChecking(Customer customer) { CreateAccount(customer, true, false); }public void CreateAccountWithCheckingAndSavings(Customer customer) { CreateAccount(customer, true, true); }private void CreateAccount(Customer customer, bool withChecking, bool withSavings) {// do work} } }
總結: “爲布爾方法命名”這個重構在很多時候都不常用,如果用戶的參數可枚舉,我們一般會枚舉它的值,不過使用這種重構也有好處,就是分解開來以後,方法多了,參數少了,代碼維護起來方便了一些。
29. 去除中間人對象
概念:本文中的“去除中間人對象”是指把 在中間關聯而不起任何其他作用的類移除,讓有關係的兩個類直接進行交互。
正文:有些時候在我們的代碼會存在一些“幽靈類”,設計模式大師Fowler稱它們爲“中間人”類,“中間人”類除了調用別的對象之外不做任何事情,所以“中間人”類沒有存在的必要,我們可以將它們從代碼中刪除,從而讓交互的兩個類直接關聯。
如下代碼所示,Consumer 類要得到AccountDataProvider的數據,但中間介入了沒起任何作用的 AccountManager 類來關聯,所以我們應當移除。
using LosTechies.DaysOfRefactoring.PullUpField.After; namespace LosTechies.DaysOfRefactoring.SampleCode.RemoveMiddleMan.Before {public class Consumer{public AccountManager AccountManager { get; set; }public Consumer(AccountManager accountManager) { AccountManager = accountManager; }public void Get(int id) { Account account = AccountManager.GetAccount(id); } }public class AccountManager{public AccountDataProvider DataProvider { get; set; }public AccountManager(AccountDataProvider dataProvider) { DataProvider = dataProvider; }public Account GetAccount(int id) {return DataProvider.GetAccount(id); } }public class AccountDataProvider{public Account GetAccount(int id) {// get account} } }重構後的代碼如下所示,Consumer 和AccountDataProvider 直接進行關聯,這樣代碼就簡單了。
using LosTechies.DaysOfRefactoring.PullUpField.After; namespace LosTechies.DaysOfRefactoring.SampleCode.RemoveMiddleMan.After {public class Consumer{public AccountDataProvider AccountDataProvider { get; set; }public Consumer(AccountDataProvider dataProvider) { AccountDataProvider = dataProvider; }public void Get(int id) { Account account = AccountDataProvider.GetAccount(id); } }public class AccountDataProvider{public Account GetAccount(int id) {// get account} } }
總結: “去除中間人對象”很多時候都會很有作用,尤其是在誤用設計模式的代碼中最容易見到,設計模式中的適配器模式和代理模式等都用中間的類是兩者進行關聯,這是比較合理的,因爲中間類做了很多事情,而對於沒有任何作用的中間類應該移除。
30. 儘快返回
概念: 本文中的“儘快返回”是指把原來複雜的條件判斷等語句用盡快返回的方式簡化代碼。
正文:如首先聲明的是前面講的“分解複雜判斷”,簡單的來說,當你的代碼中有很深的嵌套條件時,花括號就會在代碼中形成一個長長的箭頭。我們經常在不同的代碼中看到這種情況,並且這種情況也會擾亂代碼的可讀性。下代碼所示,HasAccess方法裏面包含一些嵌套條件,如果再加一些條件或者增加複雜度,那麼代碼就很可能出現幾個問題:1,可讀性差 2,很容易出現異常 3,性能較差
using System.Collections.Generic; using System.Linq; using LosTechies.DaysOfRefactoring.SampleCode.BreakMethod.After; using Customer = LosTechies.DaysOfRefactoring.BreakResponsibilities.After.Customer; namespace LosTechies.DaysOfRefactoring.SampleCode.ReturnASAP.Before {public class Order{public Customer Customer { get; private set; }public decimal CalculateOrder(Customer customer, IEnumerable<Product> products, decimal discounts) { Customer = customer;decimal orderTotal = 0m;if (products.Count() > 0) { orderTotal = products.Sum(p => p.Price);if (discounts > 0) { orderTotal -= discounts; } }return orderTotal; } } }那麼重構上面的代碼也很簡單,如果有可能的話,儘量將條件判斷從方法中移除,我們讓代碼在做處理任務之前先檢查條件,如果條件不滿足就儘快返回,不繼續執行。 下面是重構後的代碼:
using System.Collections.Generic; using System.Linq; using LosTechies.DaysOfRefactoring.SampleCode.BreakMethod.After; using Customer = LosTechies.DaysOfRefactoring.BreakResponsibilities.After.Customer; namespace LosTechies.DaysOfRefactoring.SampleCode.ReturnASAP.After {public class Order{public Customer Customer { get; private set; }public decimal CalculateOrder(Customer customer, IEnumerable<Product> products, decimal discounts) {if (products.Count() == 0)return 0; Customer = customer;decimal orderTotal = products.Sum(p => p.Price);if (discounts == 0)return orderTotal; orderTotal -= discounts;return orderTotal; } } }
總結: 總結:這個重構很重要,它和前面講的“分解複雜判斷”有些類似,我們在做複雜的處理過程時,要經常考慮這個重構,用好了它,會對我們的幫助很大。
31. 使用多態代替條件判斷
概念:本文中的“使用多態代替條件判斷”是指如果你需要檢查對象的類型或者根據類型執行一些操作時,一種很好的辦法就是將算法封裝到類中,並利用多態性進行抽象調用。
正文:本文展示了面向對象編程的基礎之一“多態性”, 有時你需要檢查對象的類型或者根據類型執行一些操作時,一種很好的辦法就是將算法封裝到類中,並利用多態性進行抽象調用。
如下代碼所示,OrderProcessor 類的ProcessOrder方法根據Customer的類型分別執行一些操作,正如上面所講的那樣,我們最好將OrderProcessor 類中這些算法(數據或操作)封裝在特定的Customer 子類中。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using LosTechies.DaysOfRefactoring.SampleCode.BreakMethod.After; namespace LosTechies.DaysOfRefactoring.SampleCode.ReplaceWithPolymorphism.Before {public abstract class Customer{ }public class Employee : Customer{ }public class NonEmployee : Customer{ }public class OrderProcessor{public decimal ProcessOrder(Customer customer, IEnumerable<Product> products) {// do some processing of orderdecimal orderTotal = products.Sum(p => p.Price);Type customerType = customer.GetType();if (customerType == typeof(Employee)) { orderTotal -= orderTotal * 0.15m; }else if (customerType == typeof(NonEmployee)) { orderTotal -= orderTotal * 0.05m; }return orderTotal; } } }重構後的代碼如下,每個Customer 子類都封裝自己的算法,然後OrderProcessor 類的ProcessOrder方法的邏輯也變得簡單並且清晰了。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using LosTechies.DaysOfRefactoring.SampleCode.BreakMethod.After; namespace LosTechies.DaysOfRefactoring.SampleCode.ReplaceWithPolymorphism.After {public abstract class Customer{public abstract decimal DiscountPercentage { get; } }public class Employee : Customer{public override decimal DiscountPercentage {get { return 0.15m; } } }public class NonEmployee : Customer{public override decimal DiscountPercentage {get { return 0.05m; } } }public class OrderProcessor{public decimal ProcessOrder(Customer customer, IEnumerable<Product> products) {// do some processing of orderdecimal orderTotal = products.Sum(p => p.Price); orderTotal -= orderTotal * customer.DiscountPercentage;return orderTotal; } } }
總結: “使用多態代替條件判斷”這個重構在很多時候會出現設計模式中(常見的工廠家族、策略模式等都可以看到它的影子),因爲運用它可以省去很多的條件判斷,同時也能簡化代碼、規範類和對象之間的職責。
代碼及PDF下載
代碼下載地址:http://github.com/schambers/days-of-refactoring
中文版PDF下載地址: 猛擊下載