小酌重構(gòu)系列[2]——提取方法、提取方法對(duì)象
前言
“藝術(shù)源于生活”——代碼也源于生活,你在生活中的一些行為習(xí)慣,可能會(huì)恰如其分地體現(xiàn)在代碼中。
當(dāng)實(shí)現(xiàn)較為復(fù)雜的功能時(shí),由于它包含一系列的邏輯,我們傾向于編寫一個(gè)“大方法”來(lái)實(shí)現(xiàn)。
為了使項(xiàng)目便于維護(hù),以及增強(qiáng)代碼的可讀性,我們有必要對(duì)“大方法”的邏輯進(jìn)行整理,并提取出分散的“小方法”。
這就是本文要講的兩種重構(gòu)策略:提取方法、提取方法對(duì)象。
如何快速地找到想讀的書?
在生活中,我是一個(gè)比較隨意的人,平時(shí)也買了不少書去看。
我的書柜不夠大,且已經(jīng)裝滿了書,每當(dāng)讀完一本書時(shí),我懶得花些時(shí)間整理,想著以后再去整理這些書籍,所以我通常都將這些書塞到一個(gè)大箱子里面。
每次要重讀某些書時(shí),我恨不得把這個(gè)箱子翻個(gè)底朝天,在花費(fèi)“九牛二虎之力”之后,我終于找到了要看的書。
然后,我把其他翻出來(lái)的書再塞回去。
箱子那么大,所有的書都放在一個(gè)箱子里,一整箱書都沒(méi)有分類,有些書藏得很深,找起書來(lái)的確不方便。
后來(lái),我想到了一個(gè)方法——最近快遞小哥送貨的包裝紙箱還在家里,這些箱子不會(huì)很大,但裝書應(yīng)該綽綽有余,何不把這些箱子利用起來(lái)?
于是,我就動(dòng)手挑選了一些大小合適的小紙箱,用簽字筆給每個(gè)紙箱做了一個(gè)標(biāo)記。
1號(hào)紙箱,裝ASP.NET編程相關(guān)的書
2號(hào)紙箱,裝架構(gòu)設(shè)計(jì)相關(guān)的書
3號(hào)紙箱,裝管理相關(guān)的書
…
N號(hào)紙箱,裝旅游相關(guān)的書
在生活中,很多讀者可能也遇到過(guò)此類問(wèn)題,為什么找個(gè)東西就這么難呢?
結(jié)果就是抱著這種心理,這個(gè)方法一直到項(xiàng)目上線都沒(méi)有整理過(guò)。
在項(xiàng)目維護(hù)期間,需要修改這個(gè)方法時(shí),再次閱讀到這個(gè)方法,我們不禁抱怨:“我擦,這方法怎么這么長(zhǎng),這是誰(shuí)寫的,我給他666!哎呦,不對(duì),這好像是我自己寫的!”
提取方法
當(dāng)一個(gè)方法包含實(shí)現(xiàn)一個(gè)功能的所有邏輯時(shí),不僅方法會(huì)看起來(lái)比較臃腫(可讀性差),也會(huì)給將來(lái)的維護(hù)造成困擾,每次改動(dòng)都會(huì)讓你如履薄冰,并且較大可能帶來(lái)新的bug。這不符合我們“將來(lái)的利益”,我們可以使用提取方法的重構(gòu)策略來(lái)規(guī)避這個(gè)問(wèn)題。
下面是我對(duì)提取方法的定義:
如果一個(gè)方法包含多個(gè)邏輯,我們應(yīng)將每個(gè)邏輯提取出來(lái),并確保每個(gè)方法只做一件事情。
下圖表示了這個(gè)重構(gòu)策略(藍(lán)色為重構(gòu)前,紅色為重構(gòu)后)。
示例
重構(gòu)前
下面這段代碼定義了一個(gè)Receipt類,用于描述收入信息,并計(jì)算總收入。
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;
// 計(jì)算subTotal
foreach (decimal itemTotal in ItemTotals)
subTotal += itemTotal;
// 計(jì)算折扣
if (Discounts.Count > 0)
{
foreach (decimal discount in Discounts)
{
subTotal -= discount;
}
}
// 計(jì)算稅額
decimal tax = subTotal*0.065m;
subTotal += tax;
return subTotal;
}
}
}
CalculateGrandTotal()方法包含了多處邏輯:計(jì)算subTotal,計(jì)算折扣,計(jì)算稅額。
這幾處邏輯是相對(duì)獨(dú)立的,我們可以將其提取出來(lái),重構(gòu)為3個(gè)方法。
重構(gòu)后
重構(gòu)后,CalculateGrandTotal()方法只包含調(diào)用各個(gè)子方法的邏輯,這已經(jīng)精簡(jiǎn)了很多,可讀性也有所增強(qiáng)。
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()
{
// 計(jì)算subTotal
decimal subTotal = CalculateSubTotal();
// 計(jì)算折扣
subTotal = CalculateDiscounts(subTotal);
// 計(jì)算稅額
subTotal = CalculateTax(subTotal);
return subTotal;
}
// 計(jì)算subTotal
private decimal CalculateSubTotal()
{
return ItemTotals.Sum();
}
// 計(jì)算折扣
private decimal CalculateDiscounts(decimal subTotal)
{
if (Discounts.Count > 0)
{
subTotal = Discounts.Aggregate(subTotal, (current, discount) => current - discount);
}
return subTotal;
}
// 計(jì)算稅額
private decimal CalculateTax(decimal subTotal)
{
decimal tax = subTotal * 0.065m;
subTotal += tax;
return subTotal;
}
}
}
二次重構(gòu)
我認(rèn)為這仍然不夠。CalculateGrandTotal() 方法所表現(xiàn)的“語(yǔ)義”,是為了計(jì)算收入總額。
但上面這段代碼不能讓我們快速地知道這個(gè)語(yǔ)義,我們需要通過(guò)3個(gè)子方法來(lái)理解這個(gè)語(yǔ)義。
“計(jì)算收入總額”本質(zhì)上是有一個(gè)公式的,即“收入總額 = (各項(xiàng)子收入總和 - 折扣總和) * (1 + 稅率)”,公式的右側(cè)是一個(gè)簡(jiǎn)單的三項(xiàng)式。
這個(gè)方法沒(méi)有體現(xiàn)”公式“這個(gè)概念,為了讓這段代碼OO的味道更濃厚一些。
我們?cè)俅螌?duì)其重構(gòu),將公式右側(cè)的每一項(xiàng)提取為屬性,每一項(xiàng)的計(jì)算邏輯都通過(guò)get屬性體現(xiàn)。
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()
{
// 收入總額 = (各項(xiàng)子收入總和 - 折扣總和) * (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; }
}
}
}
再次重構(gòu)后的代碼,是不是一目了然?
這里可能有人會(huì)疑惑了,本文不是講提取方法的嗎?現(xiàn)在怎么去提取屬性了呢?
在C#中,屬性的本質(zhì)是字段的get, set方法,所以它仍然算是提取方法。
請(qǐng)注意,并不是所有情況下,都適合使用提取屬性來(lái)代替提取方法。我的建議是,當(dāng)提取的方法邏輯較少時(shí),可以使用提取屬性代替。當(dāng)提取的方法邏輯較多時(shí),如果使用提取屬性代替,也會(huì)讓人覺(jué)得困擾。因?yàn)閷傩允菫榱嗣枋鰧?duì)象的特征,描述特征的過(guò)程如果較為復(fù)雜,會(huì)讓人難以理解,我們應(yīng)該keep it simple!
提取方法對(duì)象
以上示例描述了一個(gè)客觀對(duì)象:“收入”,這個(gè)對(duì)象包含兩個(gè)層面的“語(yǔ)義”——“收入相關(guān)的信息”和“計(jì)算收入的方法”。
“收入相關(guān)的信息”用名詞來(lái)體現(xiàn),它揭示了收入客觀存在的特征(例如:所有的子收入、折扣和稅率)。
“計(jì)算收入的方法”用動(dòng)詞來(lái)體現(xiàn),它揭示了收入的計(jì)算過(guò)程。
這兩層“語(yǔ)義”可以看做兩種不同的職責(zé),為了將這兩層“語(yǔ)義”隔離開(kāi)來(lái),我們可以將“計(jì)算收入的方法”提取為一個(gè)新的對(duì)象。
using System.Collections.Generic;
using System.Linq;
namespace ExtractMethod.After
{
/// <summary>
/// 描述收入相關(guān)的信息
/// </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>
/// 描述收入的計(jì)算方法
/// </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對(duì)象的“計(jì)算收入的方法”提取到了ReceiptCalculator對(duì)象,Receipt對(duì)象則只保留了屬性和精簡(jiǎn)的CalculateGrandTotal()方法。
“提取方法對(duì)象”也是一個(gè)不錯(cuò)的重構(gòu)策略,“提取方法對(duì)象”有什么作用呢?它可以精確類的職責(zé),控制類的粒度。
一開(kāi)始,我們用Receipt來(lái)描述“收入”這件事情;后來(lái)我們發(fā)現(xiàn)這件事情可以拆分為兩個(gè)細(xì)節(jié),“收入相關(guān)的信息”和“計(jì)算收的方法”,于是我們將這兩個(gè)細(xì)節(jié)拆分開(kāi)來(lái)。
到這里,也許大家又能看出一點(diǎn)點(diǎn)”O(jiān)O”的味道了,它體現(xiàn)了我們看待客觀事物的角度,以及對(duì)客觀事物的理解程度。OO的過(guò)程是我們對(duì)客觀事物的探索和認(rèn)知過(guò)程,它也會(huì)隨著我們了解到更多的事物細(xì)節(jié)而進(jìn)化。
本文鏈接: 文章作者:keepfool 文章出處:http://www.rzrgm.cn/keepfool/ 如果您覺(jué)得閱讀本文對(duì)您有幫助,請(qǐng)點(diǎn)一右下角的“推薦”按鈕,您的“推薦”將是我最大的寫作動(dòng)力!歡迎看官們轉(zhuǎn)載,轉(zhuǎn)載之后請(qǐng)給出作者和原文連接。

“藝術(shù)源于生活”——代碼也源于生活,你在生活中的一些行為習(xí)慣,可能會(huì)恰如其分地體現(xiàn)在代碼中。
當(dāng)實(shí)現(xiàn)較為復(fù)雜的功能時(shí),由于它包含一系列的邏輯,我們傾向于編寫一個(gè)“大方法”來(lái)實(shí)現(xiàn)。
為了使項(xiàng)目便于維護(hù),以及增強(qiáng)代碼的可讀性,我們有必要對(duì)“大方法”的邏輯進(jìn)行整理,并提取出分散的“小方法”。
這就是本文要講的兩種重構(gòu)策略:提取方法、提取方法對(duì)象。




浙公網(wǎng)安備 33010602011771號(hào)