前言
“藝術源於生活”——代碼也源於生活,你在生活中的一些行為習慣,可能會恰如其分地體現在代碼中。
當實現較為復雜的功能時,由於它包含一系列的邏輯,我們傾向於編寫一個“大方法”來實現。
為了使項目便於維護,以及增強代碼的可讀性,我們有必要對“大方法”的邏輯進行整理,並提取出分散的“小方法”。
這就是本文要講的兩種重構策略:提取方法、提取方法對象。
如何快速地找到想讀的書?
在生活中,我是一個比較隨意的人,平時也買了不少書去看。
我的書櫃不夠大,且已經裝滿了書,每當讀完一本書時,我懶得花些時間整理,想着以后再去整理這些書籍,所以我通常都將這些書塞到一個大箱子里面。
每次要重讀某些書時,我恨不得把這個箱子翻個底朝天,在花費“九牛二虎之力”之后,我終於找到了要看的書。
然后,我把其他翻出來的書再塞回去。
箱子那么大,所有的書都放在一個箱子里,一整箱書都沒有分類,有些書藏得很深,找起書來的確不方便。
后來,我想到了一個方法——最近快遞小哥送貨的包裝紙箱還在家里,這些箱子不會很大,但裝書應該綽綽有余,何不把這些箱子利用起來?
於是,我就動手挑選了一些大小合適的小紙箱,用簽字筆給每個紙箱做了一個標記。
1號紙箱,裝ASP.NET編程相關的書
2號紙箱,裝架構設計相關的書
3號紙箱,裝管理相關的書
…
N號紙箱,裝旅游相關的書
在生活中,很多讀者可能也遇到過此類問題,為什么找個東西就這么難呢?
結果就是抱着這種心理,這個方法一直到項目上線都沒有整理過。
在項目維護期間,需要修改這個方法時,再次閱讀到這個方法,我們不禁抱怨:“我擦,這方法怎么這么長,這是誰寫的,我給他666!哎呦,不對,這好像是我自己寫的!”

提取方法
當一個方法包含實現一個功能的所有邏輯時,不僅方法會看起來比較臃腫(可讀性差),也會給將來的維護造成困擾,每次改動都會讓你如履薄冰,並且較大可能帶來新的bug。這不符合我們“將來的利益”,我們可以使用提取方法的重構策略來規避這個問題。
下面是我對提取方法的定義:
如果一個方法包含多個邏輯,我們應將每個邏輯提取出來,並確保每個方法只做一件事情。
示例
重構前
下面這段代碼定義了一個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的過程是我們對客觀事物的探索和認知過程,它也會隨着我們了解到更多的事物細節而進化。