1. 程式人生 > >31天重構學習筆記重新整理下載

31天重構學習筆記重新整理下載

前言

            前兩天寫了一篇程式猿也愛學英語(上),有圖有真相的文章,寫作那篇文章只是自己一時興起,或者說是自己的興趣使然。文中的觀點只是自己的學習心得和體會,屬一家之言且鑑於本人不是學英語出身,所以也肯定有不正確的地方,也歡迎大家積極討論並給我留言,再次感謝大家的熱烈支援。關於大家詢問下篇的釋出問題,我想我會盡力在週末完成。

            這幾天由於剛釋出完專案,所以有比較充裕的時間整理自己的知識庫,發現三年多以前學習並記錄了31天重構系列筆記,至今仍回味無窮,索性重新閱讀、糾正錯誤並重新排版整理出來,希望再次和大家一起分享。

            對31天重構系列文章最早接觸是在2009年10月份,由於當時沒有訂閱

Sean Chambers的 blog,所以是在國外的社群上閒逛的時候連結過去的。基於文章中的重構點都比較常用,所以一口氣看完了整個系列,同時由於這些重構Tips基本上專案都在使用,只是我們沒有專門把它標示和整理出來,所以當時也沒有引起多大的重視。

            但就在三年前,當時我們在做一個WPF的重構專案且鑑於團隊成員技術和經驗參差不齊,非常必要專門整理一個重構的綱要,所以就收集和整理了很多的資料(31天重構也在其中)。當然這個系列除了用語重構Tips之外,也非常適合做新系統的程式碼規範參考。總而言之:只要有程式碼的地方,這個重構規範就很有價值。同時鑑於當時自己剛到新加坡這個美麗的城市,沒有親戚或者朋友,週末也不想出去閒逛,所以才靜下心來用了足足兩天時間學習並寫完了這個系列筆記。

            31天重構這個系列和《程式碼大全》、《重構:改善既有程式碼的設計》比較起來最大的特點就是比較簡單且淺顯易懂。我這系列文章也都是學習並概括Sean Chambers的31天重構的知識要領,所以如果大家對這個筆記有任何的問題或者異議也可以指出,或者大家可以直接去看原文(即可掌握了技術,又可以學習英語!):

imageimage

目錄

概念:本文所講的封裝集合就是把集合進行封裝,只提供呼叫端需要的介面。

正文:在很多時候,我們都不希望把一些不必要的操作暴露給呼叫端,只需要給它所需要的操作或資料就行,那麼做法就是封裝。這個重構在微軟的程式碼庫也經常遇到。比如最經典的屬性對欄位的封裝就是一個很好的例子,那麼下面我們將看到對集合的封裝,如下程式碼所示,呼叫端只需要一個集合的資訊,而我們則提供了一個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來隔開這些耦合,讓一些只需要資料展示的系統得到自己需要的資料即可。

概念:本文所講的移動方法就是方法放在合適的位置(通常指放在合適的類中)。

正文:移動方法是一個很簡單也很常見的重構,只要是系統就會存在很多類,那麼類裡面包括很多方法,如果一個方法經常被另外一個類使用(比本身的類使用還多)或者這個方法本身就不應該放在這個類裡面,那麼這個適合應該考慮把它移到合適的類中。程式碼如下:

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; }
    }
}

總結:這個重構法則在很多時候能讓我們把程式碼組織的結構調整得更合理,同時也能給以後的維護帶來方便。

概念:提升方法是指將一個很多繼承類都要用到的方法提升到基類中。

正文:提升方法是指將一個很多繼承類都要用到的方法提升到基類中,這樣就能減少程式碼量,同時讓類的結構更清晰。如下程式碼所示,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
    }
}

總結:這個重構要根據具體情況使用,如果不是每個子類都有這個方法的話,可以考慮使用介面或者其他方式。

概念:本文中的降低方法和前篇的提升方法整好相反,也就是把個別子類使用到的方法從基類移到子類裡面去。

正文:如下程式碼所示,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
    {
    }
}

總結:面向物件三大特徵(繼承、封裝、多型)很多時候可以幫助我們,但同時也可能會造成使用過度或者使用不當,所以如何把握好設計,這個就變得至關重要。在什麼時候使用繼承的方式,在什麼時候使用組合和聚合,介面和繼承類的選擇等久成了我們的重點。

概念:本文中的提升欄位和前面的提升方法頗為相似,就是把子類公用的欄位提升到基類中,從而達到公用的目的。

正文:如下程式碼所示, 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
    {
    }
}

總結:這個重構的策略比較簡單,同時也是比較常用的一些做法,最主要就是要注意權衡是否真的有這個必要,看這樣做究竟有沒有什麼好處(比如只需要改一個地方,維護簡便了,同時代碼量也更少了等)。

概念:本文中的降低欄位和前篇的提升欄位正好相反,就是把基類中只有某些少數類用到的欄位降低到使用它們的子類中。

正文:如下程式碼所示,基類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
    {
    }
}

總結:此重構也是一個非常簡單的重構,在很多時候我們都會不自覺的使用它。

概念:本文中的改名(方法,類,引數)是指在寫程式碼的時候對類、方法、引數、委託、事件等等元素取一個有意義的名稱。

正文:如下程式碼所示,加入一個公司建立一個員工的類,類中有一個員工名字的欄位和一個按照小時計算員工收入的方法,那麼下面程式碼的取名就顯得很難理解了,所以我們會重構名稱。

namespace LosTechies.DaysOfRefactoring.Rename.Before
{
    public class Person
    {
        public string FN { get; set; }

        public decimal ClcHrlyPR()
        {
            // code to calculate hourly payrate
            return 0m;
        }
    }
}

重構後代碼如下所示,這樣看起來就非常清晰,如果有新進專案組的成員,也會變得很樂意看這個程式碼。

namespace LosTechies.DaysOfRefactoring.Rename.After
{
    // Changed the class name to Employee
    public class Employee
    {
        public string FirstName { get; set; }

        public decimal CalculateHourlyPay()
        {
            // code to calculate hourly payrate
            return 0m;
        }
    }
}

總結:此重構經常被廣大程式設計師所忽視,但是帶來的隱患是不可估量的,也許老闆要修改功能,那我們來看這段沒有重構的程式碼(就算是自己寫的,但由於時間和專案多等關係,我們也很難理解了),然後就會變得焦頭爛額。相反重構後的程式碼就會覺得一目瞭然、賞心悅目。

概念:本文中的“使用委派代替繼承”是指在根本沒有父子關係的類中使用繼承是不合理的,可以用委派的方式來代替。

如下程式碼所示,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();
        }
    }
}

總結:這個重構是一個很好的重構,在很大程度上解決了濫用繼承的情況,很多設計模式也用到了這種思想(比如橋接模式、介面卡模式、策略模式等)。

概念:本文中的“提取介面” 是指超過一個的類要使用某一個類中部分方法時,我們應該解開它們之間的依賴,讓呼叫者使用介面,這很容易實現也可以降低程式碼的耦合性。

正文:如下程式碼所示,RegistrationProcessor 類只使用到了ClassRegistration 類中的Create方法和Total 欄位,所以可以考慮把他們做成介面給RegistrationProcessor 呼叫。

namespace LosTechies.DaysOfRefactoring.ExtractInterface.Before
{
    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;
        }
    }
}

重構後的程式碼如下,我們提取了一個IClassRegistration 介面,同時讓ClassRegistration 繼承此介面,然後呼叫端RegistrationProcessor 就可以直接通過IClassRegistration 介面進行呼叫。

namespace LosTechies.DaysOfRefactoring.ExtractInterface.After
{
    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;
        }
    }
}

總結:這個重構策略也是一個常見的運用,很多設計模式也會在其中運用此思想(如簡單工程、抽象工廠等都會通過介面來解開依賴)。

概念:本文中的把某些計算複雜的過程按照功能提取成各個小方法,這樣就可以使程式碼的可讀性、維護性得到提高。

正文:如下程式碼所示,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;
        }
    }
} 

總結:這個重構在很多公司都有一些的程式碼規範作為參考,比如一個類不能超過多少行程式碼,一個方法裡面不能超過多少行程式碼,這在一定程度上也能使程式設計師把這些複雜的邏輯剝離成意義很清楚的小方法。

概念:本文中的“使用策略類” 是指用設計模式中的策略模式來替換原來的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;
        }
    }
}

總結:這種重構在設計模式當中把它單獨取了一個名字——策略模式,這樣做的好處就是可以隔開耦合,以注入的形式實現功能,這使增加功能變得更加容易和簡便,同樣也增強了整個系統的穩定性和健壯性。

概念:本文中的“分解依賴” 是指對部分不滿足我們要求的類和方法進行依賴分解,通過裝飾器來達到我們需要的功能。

正文:正如下面程式碼所示,如果你要在你的程式碼中加入單元測試但有一部分程式碼是你不想測試的,那麼你應用使用這個的重構。下面的例子中我們應用靜態類來完成某些工作,但問題是在單元測試時我們無法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
        }
    }
}

總結:這個重構在很多時候和設計模式中的一些思想類似,使用中間的裝飾介面來分解兩個類之間的依賴,對類進行裝飾,然後使它滿足我們所需要的功能。

概念:本文中的“提取方法物件”是指當你發現一個方法中存在過多的區域性變數時,你可以通過使用“提取方法物件”重構來引入一些方法,每個方法完成任務的一個步驟,這樣可以使得程式變得更具有可讀性。

正文:如下程式碼所示,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 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;
        }
    }
}

正如下程式碼所示,我們引入了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 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;
        }
    }
}

總結:本文的重構方法在有的時候還是比較有用,但這樣會造成欄位的增加,同時也會帶來一些維護的不便,它和“提取方法”最大的區別就是一個通過方法返回需要的資料,另一個則是通過欄位來儲存方法的結果值,所以在很大程度上我們都會選擇“提取方法”。

概念:本文中的“分離職責”是指當一個類有許多職責時,將部分職責分離到獨立的類中,這樣也符合面向物件的五大特徵之一的單一職責原則,同時也可以使程式碼的結構更加清晰,維護性更高。

正文:如下程式碼所示,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;