「藝術源於生活」——代碼也源於生活,你在生活中的一些行爲習慣,可能會恰如其分地體如今代碼中。
當實現較爲複雜的功能時,因爲它包含一系列的邏輯,咱們傾向於編寫一個「大方法」來實現。
爲了使項目便於維護,以及加強代碼的可讀性,咱們有必要對「大方法」的邏輯進行整理,並提取出分散的「小方法」。
這就是本文要講的兩種重構策略:提取方法、提取方法對象。html
在生活中,我是一個比較隨意的人,平時也買了很多書去看。
個人書櫃不夠大,且已經裝滿了書,每當讀完一本書時,我懶得花些時間整理,想着之後再去整理這些書籍,因此我一般都將這些書塞到一個大箱子裏面。
每次要重讀某些書時,我巴不得把這個箱子翻個底朝天,在花費「九牛二虎之力」以後,我終於找到了要看的書。
而後,我把其餘翻出來的書再塞回去。編程
箱子那麼大,全部的書都放在一個箱子裏,一整箱書都沒有分類,有些書藏得很深,找起書來的確不方便。架構
後來,我想到了一個方法——最近快遞小哥送貨的包裝紙箱還在家裏,這些箱子不會很大,但裝書應該綽綽有餘,何不把這些箱子利用起來?
因而,我就動手挑選了一些大小合適的小紙箱,用簽字筆給每一個紙箱作了一個標記。this
1號紙箱,裝ASP.NET編程相關的書
2號紙箱,裝架構設計相關的書
3號紙箱,裝管理相關的書
…
N號紙箱,裝旅遊相關的書spa
在生活中,不少讀者可能也遇到過此類問題,爲何找個東西就這麼難呢?架構設計
當一個方法包含實現一個功能的全部邏輯時,不只方法會看起來比較臃腫(可讀性差),也會給未來的維護形成困擾,每次改動都會讓你如履薄冰,而且較大可能帶來新的bug。這不符合咱們「未來的利益」,咱們可使用提取方法的重構策略來規避這個問題。設計
下面是我對提取方法的定義:3d
若是一個方法包含多個邏輯,咱們應將每一個邏輯提取出來,並確保每一個方法只作一件事情。htm
下面這段代碼定義了一個Receipt類,用於描述收入信息,並計算總收入。
using System.Collections.Generic; namespace ExtractMethod.Before { public class Receipt { public IList<decimal> Discounts { get; set; } public IList<decimal> ItemTotals { get; set; } public decimal CalculateGrandTotal() { decimal subTotal = 0m; // 計算subTotal 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()方法包含了多處邏輯:計算subTotal,計算折扣,計算稅額。
這幾處邏輯是相對獨立的,咱們能夠將其提取出來,重構爲3個方法。
重構後,CalculateGrandTotal()方法只包含調用各個子方法的邏輯,這已經精簡了不少,可讀性也有所加強。
using System.Collections.Generic; using System.Linq; namespace ExtractMethod.After { public class Receipt { public IList<decimal> Discounts { get; set; } public IList<decimal> ItemTotals { get; set; } public decimal CalculateGrandTotal() { // 計算subTotal decimal subTotal = CalculateSubTotal(); // 計算折扣 subTotal = CalculateDiscounts(subTotal); // 計算稅額 subTotal = CalculateTax(subTotal); return subTotal; } // 計算subTotal private decimal CalculateSubTotal() { return ItemTotals.Sum(); } // 計算折扣 private decimal CalculateDiscounts(decimal subTotal) { if (Discounts.Count > 0) { subTotal = Discounts.Aggregate(subTotal, (current, discount) => current - discount); } return subTotal; } // 計算稅額 private decimal CalculateTax(decimal subTotal) { decimal tax = subTotal * 0.065m; subTotal += tax; return subTotal; } } }
我認爲這仍然不夠。CalculateGrandTotal() 方法所表現的「語義」,是爲了計算收入總額。
但上面這段代碼不能讓咱們快速地知道這個語義,咱們須要經過3個子方法來理解這個語義。
「計算收入總額」本質上是有一個公式的,即「收入總額 = (各項子收入總和 - 折扣總和) * (1 + 稅率)」,公式的右側是一個簡單的三項式。
這個方法沒有體現」公式「這個概念,爲了讓這段代碼OO的味道更濃厚一些。
咱們再次對其重構,將公式右側的每一項提取爲屬性,每一項的計算邏輯都經過get屬性體現。
using System.Collections.Generic; using System.Linq; namespace ExtractMethod.After { public class Receipt2 { private IList<decimal> Discounts { get; set; } private IList<decimal> ItemTotals { get; set; } public decimal CalculateGrandTotal() { // 收入總額 = (各項子收入總和 - 折扣總和) * (1 + 稅率) decimal grandTotal = (SubTotal - TotalDiscounts) * (1 + TaxRate); return grandTotal; } // 獲取subTotal private decimal SubTotal { get { return ItemTotals.Sum(); } } // 獲取TotalDiscounts private decimal TotalDiscounts { get { return Discounts.Sum(); } } // 獲取TaxRate private decimal TaxRate { get { return 0.065m; } } } }
再次重構後的代碼,是否是一目瞭然?
這裏可能有人會疑惑了,本文不是講提取方法的嗎?如今怎麼去提取屬性了呢?
在C#中,屬性的本質是字段的get, set方法,因此它仍然算是提取方法。
請注意,並非全部狀況下,都適合使用提取屬性來代替提取方法。個人建議是,當提取的方法邏輯較少時,可使用提取屬性代替。當提取的方法邏輯較多時,若是使用提取屬性代替,也會讓人以爲困擾。由於屬性是爲了描述對象的特徵,描述特徵的過程若是較爲複雜,會讓人難以理解,咱們應該keep it simple!
以上示例描述了一個客觀對象:「收入」,這個對象包含兩個層面的「語義」——「收入相關的信息」和「計算收入的方法」。
「收入相關的信息」用名詞來體現,它揭示了收入客觀存在的特徵(例如:全部的子收入、折扣和稅率)。
「計算收入的方法」用動詞來體現,它揭示了收入的計算過程。
這兩層「語義」能夠看作兩種不一樣的職責,爲了將這兩層「語義」隔離開來,咱們能夠將「計算收入的方法」提取爲一個新的對象。
using System.Collections.Generic; using System.Linq; namespace ExtractMethod.After { /// <summary> /// 描述收入相關的信息 /// </summary> public class Receipt { public IList<decimal> Discounts { get; set; } public IList<decimal> ItemTotals { get; set; } // 獲取TaxRate public decimal TaxRate { get { return 0.065m; } } public decimal CalculateGrandTotal() { return new ReceiptCalculator(this).CalculateGrandTotal(); } } /// <summary> /// 描述收入的計算方法 /// </summary> public class ReceiptCalculator { private readonly Receipt _receipt; public ReceiptCalculator(Receipt receipt) { _receipt = receipt; } public decimal CalculateGrandTotal() { decimal grandTotal = (SubTotal - TotalDiscounts) * (1 + _receipt.TaxRate); return grandTotal; } // 獲取subTotal private decimal SubTotal { get { return _receipt.ItemTotals.Sum(); } } // 獲取TotalDiscounts private decimal TotalDiscounts { get { return _receipt.Discounts.Sum(); } } } }
這則代碼將Receipt對象的「計算收入的方法」提取到了ReceiptCalculator對象,Receipt對象則只保留了屬性和精簡的CalculateGrandTotal()方法。
「提取方法對象」也是一個不錯的重構策略,「提取方法對象」有什麼做用呢?它能夠精確類的職責,控制類的粒度。
一開始,咱們用Receipt來描述「收入」這件事情;後來咱們發現這件事情能夠拆分爲兩個細節,「收入相關的信息」和「計算收的方法」,因而咱們將這兩個細節拆分開來。
到這裏,也許你們又能看出一點點」OO」的味道了,它體現了咱們看待客觀事物的角度,以及對客觀事物的理解程度。OO的過程是咱們對客觀事物的探索和認知過程,它也會隨着咱們瞭解到更多的事物細節而進化。