簡介
重構是持續改進代碼的基礎。抵制重構將帶來技術麻煩:忘記代碼片斷的功能、建立沒法測試的代碼等等。算法
而有了重構,使用單元測試、共享代碼以及更可靠的無bug 的代碼這些最佳實踐就顯得簡單多了。編程
鑑於重構的重要性,我決定在整個8 月份天天介紹一個重構。在開始以前,請容許我事先聲明,儘管我試着對每一個重構進行額外的描述和討論,但我並非在聲明它們的全部權。網絡
我介紹的大多數重構均可以在Refactoring.com 中找到,有一些來自《代碼大全(第2 版)》,剩下的則是我本身常用或從其餘網站找到的。我以爲註明每一個重構的出處並非重要的,由於你能夠在網上不一樣的帖子或文章中找到名稱相似的重構。框架
本着這一精神,我將在明天發佈第一篇帖子並開始長達31天的重構馬拉松之旅。但願大家可以享受重構並從中獲益。ide
代碼重構第1天:封裝集合
在某些場景中,向類的使用者隱藏類中的完整集合是一個很好的作法,好比對集合的add/remove操做中包含其餘的相關邏輯時。所以,以可迭代但不直接在集合上進行操做的方式來暴露集合,是個不錯的主意。咱們來看代碼:函數
- public class Order
- {
- private int _orderTotal;
- private List<OrderLine> _orderLines;
-
- 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);
- }
- }
如你所見,咱們對集合進行了封裝,沒有將Add/Remove 方法暴露給類的使用者。在.NET Framework中,有些類如ReadOnlyCollection,會因爲封裝集合而產生不一樣的行爲,但它們各自都有防止誤解的說明。這是一個很是簡單但卻極具價值的重構,能夠確保用戶不會誤用你暴露的集合,避免代碼中的一些bug。工具
代碼重構第2天:移動方法
今天的重構一樣很是簡單,以致於人們並不認爲這是一個有價值的重構。遷移方法(Move Method),顧名思義就是將方法遷移到合適的位置。在開始重構前,咱們先看看一下代碼:post
- 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.CalculateInterest 方法。當一個方法被其餘類使用比在它所在類中的使用還要頻繁時,咱們就須要使用遷移方法重構了——將方法遷移到更頻繁地使用它的類中。因爲依賴關係,該重構並不能應用於全部實例,但人們仍是常常低估它的價值。單元測試
最終的代碼應該是這樣的:學習
- 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 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;
- }
- }
夠簡單吧?
代碼重構第3天:提高方法
提高方法(Pull Up Method)重構是將方法向繼承鏈上層遷移的過程。用於一個方法被多個實現者使用時。
- 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類中包含Turn方法,但咱們也但願在Motorcycle 類中使用。所以,若是沒有基類,咱們就建立一個基類並將該方法「上移」到基類中,這樣兩個類就均可以使用Turn 方法了。這樣作惟一的缺點是擴充了基類的接口、增長了其複雜性,所以需謹慎使用。只有當一個以上的子類須要使用該方法時才須要進行遷移。若是濫用繼承,系統將會很快崩潰。這時你應該使用組合代替繼承。重構以後的代碼以下:
- 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天:下降方法
昨天咱們介紹了將方法遷移到基類以供多個子類使用的上移方法重構,今天咱們來看看相反的操做。重構前的代碼以下:
- public abstract class Animal
- {
- public void Bark()
- {
- // code to bark
- }
- }
-
- public class Dog : Animal
- {
- }
-
- public class Cat : Animal
- {
- }
這裏的基類有一個Bark方法。或許咱們的貓咪們一時半會也無法學會汪汪叫(bark),所以Cat 類中再也不須要這個功能了。儘管基類不須要這個方法,但在顯式處理Dog 類時也許還須要,所以咱們將Bark 方法「下降」到Dog 類中。這時,有必要評估Animal基類中是否還有其餘行爲。若是沒有,則是一個將Animal抽象類轉換成接口的好時機。由於契約中不須要任何代碼,能夠認爲是一個標記接口。
- public abstract class Animal
- {
- }
-
- public class Dog : Animal
- {
- public void Bark()
- {
- // code to bark
- }
- }
-
- public class Cat : Animal
- {
- }
代碼重構第5天:提高字段
今天咱們來看看一個和提高方法十分相似的重構。不過今天咱們處理的不是方法,而是字段。
- public abstract class Account
- {
- }
-
- public class CheckingAccount : Account
- {
- private decimal _minimumCheckingBalance = 5m;
- }
-
- public class SavingsAccount : Account
- {
- private decimal _minimumSavingsBalance = 5m;
- }
在這個例子中,兩個子類中包含重複的常量。爲了提升複用性咱們將字段上移到基類中,並簡化其名稱。
- public abstract class Account
- {
- protected decimal _minimumBalance = 5m;
- }
-
- public class CheckingAccount : Account
- {
- }
-
- public class SavingsAccount : Account
- {
- }
重構代碼第6天:下降字段
與提高字段相反的重構是下降字段。一樣,這也是一個無需多言的簡單重構。
- public abstract class Task
- {
- protected string _resolution;
- }
-
- public class BugTask : Task
- {
- }
-
- public class FeatureTask : Task
- {
- }
在這個例子中,基類中的一個字符串字段只被一個子類使用,所以能夠進行下移。只要沒有其餘子類使用基類的字段時,就應該當即執行該重構。保留的時間越長,就越有可能不去重構而保持原樣。
- public abstract class Task
- {
- }
-
- public class BugTask : Task
- {
- private string _resolution;
- }
-
- public class FeatureTask : Task
- {
- }
重構代碼第7天:重命名(方法,類,參數)
這是我最經常使用也是最有用的重構之一。咱們對方法/類/參數的命名每每不那麼合適,以致於誤導閱讀者對於方法/類/參數功能的理解。這會形成閱讀者的主觀臆斷,甚至引入bug。這個重構看起來簡單,但卻十分重要。
- public class Person
- {
- public string FN { get; set; }
- public decimal ClcHrlyPR()
- {
- // code to calculate hourly payrate
- return 0m;
- }
- }
如你所見,咱們的類/方法/參數的名稱十分晦澀難懂,能夠理解爲不一樣的含義。應用這個重構你只需隨手將
名稱修改得更具描述性、更容易傳達其含義便可。簡單吧。
- // Changed the class name to Employee
- public class Employee
- {
- public string FirstName { get; set; }
-
- public decimal CalculateHourlyPay()
- {
- // code to calculate hourly payrate
- return 0m;
- }
- }
重構代碼第8天:使用委派代替繼承
繼承的誤用十分廣泛。它只能用於邏輯環境,但卻常常用於簡化,這致使複雜的沒有意義的繼承層次。看下面的代碼:
- public class Sanitation
- {
- public string WashHands()
- {
- return "Cleaned!";
- }
- }
-
- public class Child : Sanitation
- {
- }
在該例中,Child 並非Sanitation,所以這樣的繼承層次是毫無心義的。咱們能夠這樣重構:在Child 的構造函數裏實現一個Sanitation實例,並將方法的調用委託給這個實例。若是你使用依賴注入,能夠經過構造函數傳遞Sanitation實例,儘管在我看來還要向IoC容器註冊模型是一種壞味道,但領會精神就能夠了。繼承只能用於嚴格的繼承場景,並非用來快速編寫代碼的工具。
- 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天:提取接口
今天咱們來介紹一個經常被忽視的重構:提取接口。若是你發現多於一個類使用另一個類的某些方法,引入接口解除這種依賴每每十分有用。該重構實現起來很是簡單,而且可以享受到鬆耦合帶來的好處。
- public class ClassRegistration
- {
- public void Create()
- {
- // create registration code
- }
-
- public void Transfer()
- {
- // class transfer code
- }
-
- public decimal Total { get; private set; }
- }
-
- public class RegistrationProcessor
- {
- public decimal ProcessRegistration(ClassRegistration registration)
- {
- registration.Create();
- return registration.Total;
- }
- }
在下面的代碼中,你能夠看到我提取出了消費者所使用的兩個方法,並將其置於一個接口中。如今消費者沒必要關心和了解實現了這些方法的類。咱們解除了消費者與實際實現之間的耦合,使其只依賴於咱們建立的契約。
- public interface IClassRegistration
- {
- void Create();
- decimal Total { get; }
- }
-
- public class ClassRegistration : IClassRegistration
- {
- public void Create()
- {
- // create registration code
- }
-
- public void Transfer()
- {
- // class transfer code
- }
-
- public decimal Total { get; private set; }
- }
-
- public class RegistrationProcessor
- {
- public decimal ProcessRegistration(IClassRegistration registration)
- {
- registration.Create();
- return registration.Total;
- }
- }
代碼重構第10天:提取方法
今天咱們要介紹的重構是提取方法。這個重構極其簡單但卻大有裨益。首先,將邏輯置於命名良好的方法內有助於提升代碼的可讀性。當方法的名稱能夠很好地描述這部分代碼的功能時,能夠有效地減小其餘開發者的研究時間。假設越少,代碼中的bug 也就越少。重構以前的代碼以下:
- 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方法一共作了3件不一樣的事情:計算總額、折扣和發票稅額。開發者爲了搞清楚每一個功能如何處理而不得不將代碼從頭看到尾。相比於此,向下面的代碼那樣將每一個任務分解成單獨的方法則要節省更多時間,也更具可讀性:
- 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 語句塊很大,而且會隨時引入新的判斷條件。這時,最好使用策略模式將每一個條件封裝到單獨的類中。實現策略模式的方式是不少的。我在這裏介紹的策略重構使用的是字典策略,這麼作的好處是調用者沒必要修改原來的代碼。
- 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;
- }
- }
- }
要應用該重構,需將每一個測試條件至於單獨的類中,這些類實現了一個共同的接口。而後將枚舉做爲字典的鍵,這樣就能夠獲取正確的實現,並執行其代碼了。之後若是但願添加新的條件,只需添加新的實現類,並將其添加至ShippingCalculations 字典中。正如前面說過的,這不是實現策略模式的惟一方式。我在這裏將字體加粗顯示,是由於確定會有人在評論裏指出這點:)用你以爲好用的方法。我用這種方式實現重構的好處是,不用修改客戶端代碼。全部的修改都在ShippingInfo 類內部。
Jayme Davis指出這種重構因爲仍然須要在構造函數中進行綁定,因此只不過是增長了一些類而已,但若是綁定IShippingCalculation的策略能夠置於IoC中,帶來的好處仍是不少的,它可使你更靈活地捆綁策略。
- namespace LosTechies.DaysOfRefactoring.SwitchToStrategy.After
- {
- public class ClientCode
- {
- public decimal CalculateShipping()
- {
- ShippingInfo shippingInfo = new ShippingInfo();
- return shippingInfo.CalculateShippingAmount(State.Alaska);
- }
- }
-
- public enum State
- {
- Alaska,
- NewYork,
- Florida
- }
-
- public class ShippingInfo
- {
- private IDictionary<State, IShippingCalculation> ShippingCalculations
- { get; set; }
-
- public ShippingInfo()
- {
- ShippingCalculations = new Dictionary<State, IShippingCalculation>
- {
- { State.Alaska, new AlaskShippingCalculation() },
- { State.NewYork, new NewYorkShippingCalculation() },
- { State.Florida, new FloridaShippingCalculation() }
- };
- }
-
- public decimal CalculateShippingAmount(State shipToState)
- {
- return ShippingCalculations[shipToState].Calculate();
- }
- }
-
- public interface IShippingCalculation
- {
- decimal Calculate();
- }
-
- public class AlaskShippingCalculation : IShippingCalculation
- {
- public decimal Calculate()
- {
- return 15m;
- }
- }
-
- public class NewYorkShippingCalculation : IShippingCalculation
- {
- public decimal Calculate()
- {
- return 10m;
- }
- }
-
- public class FloridaShippingCalculation : IShippingCalculation
- {
- public decimal Calculate()
- {
- return 3m;
- }
- }
- }
爲了使這個示例圓滿,咱們來看看在ShippingInfo 構造函數中使用Ninject 爲IoC容器時如何進行綁定。須要更改的地方不少,主要是將state 的枚舉放在策略內部,以及Ninject 向構造函數傳遞一個IShippingInfo 的IEnumerable泛型。接下來咱們使用策略類中的state屬性建立字典,其他部分保持不變。(感謝Nate Kohari和Jayme Davis)
- 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天:分解依賴
有些單元測試須要恰當的測試「縫隙」(test seam)來模擬/隔離一些不想被測試的部分。若是你正想在代碼中引入這種單元測試,那麼今天介紹的重構就十分有用。在這個例子中,咱們的客戶端代碼使用一個靜態類來實現功能。但當須要單元測試時,問題就來了。咱們沒法在單元測試中模擬靜態類。解決的方法是使用一個接口將靜態類包裝起來,造成一個縫隙來切斷與靜態類之間的依賴。
- 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
- }
- }
重構時咱們所要作的就是引入一個接口和簡單調用上面那個靜態類的類。所以行爲仍是同樣的,只是調用的方式產生了變化。這是一個不錯的重構起始點,也是向代碼添加單元測試的簡單方式。
- 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
- }
- }
如今,咱們能夠在單元測試中將模擬的IFeederService 傳入AnimalFeedingService 構造函數。測試成功後,咱們能夠將靜態方法中的代碼移植到FeederService 類中,並刪除靜態類。
代碼重構第13天:提取方法對象
今天的重構來自於Martin Fowler的重構目錄。在我看來,這是一個比較罕見的重構,但有時卻終能派上用場。當你嘗試進行提取方法的重構時,須要引入大量的方法。在一個方法中使用衆多的本地變量有時會使代碼變得醜陋。所以最好使用提取方法對象這個重構,將執行任務的邏輯分開。
- 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 items
- foreach (OrderLineItem lineItem in OrderLineItems)
- {
- subTotal += lineItem.Price;
- }
-
- // Subtract Discounts
- foreach (decimal discount in Discounts)
- subTotal -= discount;
-
- // Calculate Tax
- decimal tax = subTotal * Tax;
-
- // Calculate GrandTotal
- decimal grandTotal = subTotal + tax;
- return grandTotal;
- }
- }
咱們經過構造函數,將返回計算結果的類的引用傳遞給包含多個計算方法的新建對象,或者向方法對象的構造函數中單獨傳遞各個參數。以下面的代碼:
- 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 items
- foreach (OrderLineItem lineItem in OrderLineItems)
- SubTotal += lineItem.Price;
- }
-
- private void SubtractDiscounts()
- {
- // Subtract Discounts
- foreach (decimal discount in Discounts)
- SubTotal -= discount;
- }
-
- private void CalculateTax()
- {
- // Calculate Tax
- SubTotal += SubTotal * Tax;
- }
- }
代碼重構第14天:分離職責
把一個類的多個職責進行拆分,這貫徹了SOLID 中的單一職責原則(SRP)。儘管對於如何劃分「職責」常常存在爭論,但應用這項重構仍是十分簡單的。我這裏並不會回答劃分職責的問題,只是演示一個結構清晰的示例,將類劃分爲多個負責具體職責的類。
- public class Video
- {
- public void PayFee(decimal fee)
- {
- }
-
- public void RentVideo(Video video, Customer customer)
- {
- customer.Videos.Add(video);
- }
-
- public decimal CalculateBalance(Customer customer)
- {
- return customer.LateFees.Sum();
- }
- }
-
- public class Customer
- {
- public IList<decimal> LateFees { get; set; }
- public IList<Video> Videos { get; set; }
- }
如你所見,Video類包含兩個職責,一個負責處理錄像租賃,另外一個負責管理管理用戶的租賃總數。要分離職責,咱們能夠將用戶的邏輯轉移到用戶類中。
- 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天:移除重複內容
這大概是處理一個方法在多處使用時最多見的重構。若是不加以注意的話,你會慢慢地養成重複的習慣。開發者經常因爲懶惰或者在想要儘快生成儘量多的代碼時,向代碼中添加不少重複的內容。我想也不必過多解釋了吧,直接看代碼把。
- 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;
- }
- }
咱們用共享方法的方式來刪除重複的代碼。看!沒有重複了吧?請務必在必要的時候執行這項重構。它能有效地減小bug,由於你不會將有bug的代碼複製/粘貼到各個角落。
- 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;
- }
- }
代碼重構第16天:封裝條件
當代碼中充斥着若干條件判斷時,代碼的真正意圖會迷失於這些條件判斷之中。這時我喜歡將條件判斷提取到一個易於讀取的屬性或方法(若是有參數)中。重構以前的代碼以下:
- 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 conditions
- if (Functions.Length > 1 && Name == "RCA" &&
- CreatedYear > DateTime.Now.Year - 2)
- return "doSomething";
- }
- }
重構以後,代碼的可讀性更強,意圖更明顯:
- 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 conditions
- if (HasExtraFunctions)
- return "doSomething";
- }
- }
代碼重構第17天:提取父類
今天的重構來自於Martin Fowler的重構目錄,當一個類有不少方法但願將它們「提拔」到基類以供同層次的其餘類使用時,會常用該重構。下面的類包含兩個方法,咱們但願提取這兩個方法並容許其餘類使用。
- public class Dog
- {
- public void EatFood()
- {
- // eat some food
- }
-
- public void Groom()
- {
- // perform grooming
- }
- }
重構以後,咱們僅僅將須要的方法轉移到了一個新的基類中。這很相似「Pull Up」重構,只是在重構以前,並不存在基類。
- public class Animal
- {
- public void EatFood()
- {
- // eat some food
- }
-
- public void Groom()
- {
- // perform grooming
- }
- }
-
- public class Dog : Animal
- {
- }
代碼重構第18天:使用條件判斷代替異常
今天的重構沒有什麼出處,是我平時常用而總結出來的。歡迎您發表任何改進意見或建議。我相信必定還有其餘比較好的重構能夠解決相似的問題。
我曾無數次面對的一個代碼壞味道就是,使用異常來控制程序流程。您可能會看到相似的代碼:
- 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;
- }
- }
異常應該僅僅完成本身的本職工做:處理異常行爲。大多數狀況你均可以將這些代碼用恰當的條件判斷替換,並進行恰當的處理。下面的代碼能夠稱之爲契約式設計,由於咱們在執行具體工做以前明確了Motor類的狀態,而不是經過異常來進行處理。
- public class Microwave
- {
- private IMicrowaveMotor Motor { get; set; }
- public bool Start(object food)
- {
- if (Motor.IsInUse)
- return false;
- Motor.Cook(food);
- return true;
- }
- }
代碼重構第19天:提取工廠類
今天的重構是由GangOfFour首先提出的,網絡上有不少關於該模式不一樣的用法。
在代碼中,一般須要一些複雜的對象建立工做,以使這些對象達到一種可使用的狀態。一般狀況下,這種建立不過是新建對象實例,並以咱們須要的方式進行工做。可是,有時候這種建立對象的需求會極具增加,而且混淆了建立對象的原始代碼。這時,工廠類就派上用場了。最複雜的工廠模式是使用抽象工廠建立對象族。而咱們只是使用最基本的方式,用一個工廠類建立
一個特殊類的實例。來看下面的代碼:
- public class PoliceCarController
- {
- public PoliceCar New(int mileage, bool serviceRequired)
- {
- PoliceCar policeCar = new PoliceCar();
- policeCar.ServiceRequired = serviceRequired;
- policeCar.Mileage = mileage;
- return policeCar;
- }
- }
如您所見,New方法負責建立PoliceCar並根據一些外部輸入初始化PoliceCar的某些屬性。對於簡單的建立工做來講,這樣作能夠從容應對。可是長此以往,建立的工做量愈來愈大,而且被附加在controller 類上,但這並非controller類的職責。這時,咱們能夠將建立代碼提取到一個Factory類中去,由該類負責PoliceCar實例的建立。
- 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天:提取子類
今天的重構來自於Martin Fowler的模式目錄。你能夠在他的目錄中找到該重構。
當一個類中的某些方法並非面向全部的類時,可使用該重構將其遷移到子類中。我這裏舉的例子十分簡單,它包含一個Registration類,該類處理與學生註冊課程相關的全部信息。
- 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; }
- }
當使用了該類以後,咱們就會意識到問題所在——它應用於兩個徹底不一樣的場景。屬性NonRegistrationAction和Notes 只有在處理與普通註冊略有不一樣的NonRegistration 時纔會使用。所以,咱們能夠提取一個子類,並將這兩個屬性轉移到NonRegistration類中,這樣才更合適。
- 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天:合併繼承
今天的重構來自於Martin Fowler的模式目錄。你能夠在他的目錄中找到該重構。昨天,咱們經過提取子類來下放職責。而今天,當咱們意識到再也不須要某個子類時,可使用Collapse Hierarchy 重構。若是某個子類的屬性(以及其餘成員)能夠被合併到基類中,這時再保留這個子類已經沒有任何意義了。
- 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; }
- }
這裏的子類並無過多的功能,只是表示站點是否激活。這時咱們會意識到判斷站點是否激活的功能應該是通用的。所以能夠將子類的功能放回到Website 中,並刪除StudentWebsite 類型。
- 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天:分解方法
今天的重構沒有任何出處。可能已經有其餘人使用了相同的重構,只是名稱不一樣罷了。若是你知道誰的名字比Break Method更好,請轉告我。
這個重構是一種元重構(meta-refactoring),它只是不停地使用提取方法重構,直到將一個大的方法分解成若干個小的方法。下面的例子有點作做,AcceptPayment 方法沒有豐富的功能。所以爲了使其更接近真實場景,咱們只能假設該方法中包含了其餘大量的輔助代碼。
下面的AcceptPayment 方法能夠被劃分爲多個單獨的方法。
- 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方法包含多個功能,能夠被分解爲多個子方法。所以咱們屢次使用提取方法重構,結果以下:
- 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天:引入參數對象
該重構來自於Fowler的重構目錄。有時當使用一個包含多個參數的方法時,因爲參數過多會致使可讀性嚴重降低,如:
- public void Create(decimal amount, Student student, IEnumerable<Course> courses, decimal credits)
- {
- // do work
- }
這時有必要新建一個類,負責攜帶方法的參數。若是要增長更多的參數,只需爲對參數對象增長其餘的字段就能夠了,代碼顯得更加靈活。要注意,僅僅在方法的參數確實過多時才使用該重構,不然會使類的數量暴增,而這本應該越少越好。
- 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
- }
- }
代碼重構第24天:分解複雜判斷
今天的重構基於c2的wiki條目。Los Techies的Chris Missal一樣也些了一篇關於反模式的post。簡單地說,當你使用大量的嵌套條件判斷時,造成了箭頭型的代碼,這就是箭頭反模式(arrowhead
antipattern)。我常常在不一樣的代碼庫中看到這種現象,這提升了代碼的圈複雜度(cyclomatic complexity)。下面的例子演示了箭頭反模式:
- 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;
- }
- }
移除箭頭反模式的重構和封裝條件判斷同樣簡單。這種方式的重構在方法執行以前每每會評估各個條件,這有點相似於契約式設計驗證。下面是重構以後的代碼:
- 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天:引入契約式設計
契約式設計(DBC,Design By Contract)定義了方法應該包含輸入和輸出驗證。所以,能夠確保全部的工做都是基於可用的數據,而且全部的行爲都是可預料的。不然,將返回異常或錯誤並在方法中進行處理。要了解更多關於DBC的內容,能夠訪問wikipedia。
在咱們的示例中,輸入參數極可能爲null。因爲沒有進行驗證,該方法最終會拋出NullReferenceException。在方法最後,咱們也並不肯定是否爲用戶返回了一個有效的decimal,這可能致使在別的地方引入其餘方法。
- public class CashRegister
- {
- public decimal TotalOrder(IEnumerable<Product> products, Customer customer)
- {
- decimal orderTotal = products.Sum(product => product.Price);
- customer.Balance += orderTotal;
- return orderTotal;
- }
- }
在此處引入DBC 驗證是十分簡單的。首先,咱們要聲明customer 不能爲null,而且在計算總值時至少要有一個product。在返回訂單總值時,咱們要肯定其值是否有效。若是此例中任何一個驗證失敗,咱們將以友好的方式拋出相應的異常來描述具體信息,而不是拋出一個晦澀的NullReferenceException。
在.NET Framework 3.5的Microsoft.Contracts命名空間中包含一些DBC框架和異常。我我的尚未使用,但它們仍是值得一看的。關於該命名空間只有在MSDN上能找到點資料。
- 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;
- }
- }
在驗證過程當中確實增長了很多代碼,你也許會認爲過分使用了DBC。但我認爲在大多數狀況下,處理這些棘手的問題所作的努力都是值得的。追蹤無詳細內容的NullReferenceException的確不是什麼美差。
代碼重構第26天:避免雙重否認
今天的重構來自於Fowler的重構目錄。
儘管我在不少代碼中發現了這種嚴重下降可讀性並每每傳達錯誤意圖的壞味道,但這種重構自己仍是很容易實現的。這種毀滅性的代碼所基於的假設致使了錯誤的代碼編寫習慣,並最終致使bug。以下例所示:
- public class Order
- {
- public void Checkout(IEnumerable<Product> products, Customer customer)
- {
- if (!customer.IsNotFlagged)
- {
- // the customer account is flagged
- // log some errors and return
- return;
- }
-
- // normal order processing
- }
- }
-
- public class Customer
- {
- public decimal Balance { get; private set; }
-
- public bool IsNotFlagged
- {
- get { return Balance < 30m; }
- }
- }
如你所見,這裏的雙重否認十分難以理解,咱們不得不找出什麼纔是雙重否認所要表達的確定狀態。修改代碼是很容易的。若是咱們找不到確定的判斷,能夠添加一個處理雙重否認的假設,而不要在獲得結果以後再去驗證。
- public class Order
- {
- public void Checkout(IEnumerable<Product> products, Customer customer)
- {
- if (customer.IsFlagged)
- {
- // the customer account is flagged
- // log some errors and return
- return;
- }
-
- // normal order processing
- }
- }
-
- public class Customer
- {
- public decimal Balance { get; private set; }
-
- public bool IsFlagged
- {
- get { return Balance >= 30m; }
- }
- }
代碼重構第27天:去除上帝類
在傳統的代碼庫中,咱們經常會看到一些違反了SRP原則的類。這些類一般以Utils或Manager結尾,有時也沒有這麼明顯的特徵而僅僅是普通的包含多個功能的類。這種God 類還有一個特徵,使用語句或註釋將代碼分隔爲多個不一樣角色的分組,而這些角色正是這一個類所扮演的。
長此以往,這些類成爲了那些沒有時間放置到恰當類中的方法的垃圾桶。這時的重構須要將方法分解成多個負責單一職責的類。
- 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
- }
- }
使用該重構是很是簡單明瞭的,只需把相關方法提取出來並放置到負責相應職責的類中便可。這使得類的粒度更細、職責更分明、往後的維護更方便。上例的代碼最終被分解爲兩個類:
- 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天:爲布爾方法命名
今天的重構不是來自於Fowler的重構目錄。若是誰知道這項「重構」的確切出處,請告訴我。
固然,你也能夠說這並非一個真正的重構,由於方法實際上改變了,但這是一個灰色地帶,能夠開放討論。一個擁有大量布爾類型參數的方法將很快變得沒法控制,產生難以預期的行爲。參數的數量將決定分解的方法的數量。來看看該重構是如何開始的:
- public class BankAccount
- {
- public void CreateAccount(Customer customer, bool withChecking, bool withSavings, bool withStocks)
- {
- // do work
- }
- }
要想使這樣的代碼運行得更好,咱們能夠經過命名良好的方法暴露布爾參數,並將原始方法更改成private以阻止外部調用。顯然,你可能須要進行大量的代碼轉移,也許重構爲一個Parameter Object 會更有意義。
- 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的重構目錄。
有時你的代碼裏可能會存在一些「Phantom」或「Ghost」類,Fowler 稱之爲「中間人(Middle Man)」。這些中間人類僅僅簡單地將調用委託給其餘組件,除此以外沒有任何功能。這一層是徹底沒有必要的,咱們能夠不費吹灰之力將其徹底移除。
- 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