小酌重構系列[2]——提取方法、提取方法對象

前言

「藝術源於生活」——代碼也源於生活,你在生活中的一些行爲習慣,可能會恰如其分地體如今代碼中。
當實現較爲複雜的功能時,因爲它包含一系列的邏輯,咱們傾向於編寫一個「大方法」來實現。
爲了使項目便於維護,以及加強代碼的可讀性,咱們有必要對「大方法」的邏輯進行整理,並提取出分散的「小方法」。
這就是本文要講的兩種重構策略:提取方法、提取方法對象。html

如何快速地找到想讀的書?

在生活中,我是一個比較隨意的人,平時也買了很多書去看。
個人書櫃不夠大,且已經裝滿了書,每當讀完一本書時,我懶得花些時間整理,想着之後再去整理這些書籍,因此我一般都將這些書塞到一個大箱子裏面。
每次要重讀某些書時,我巴不得把這個箱子翻個底朝天,在花費「九牛二虎之力」以後,我終於找到了要看的書。
而後,我把其餘翻出來的書再塞回去。編程

每次找書,我老是經歷千辛萬苦,弄得家裏的地板一片狼藉,還得被媳婦兒臭罵一頓。
1ADC1548

箱子那麼大,全部的書都放在一個箱子裏,一整箱書都沒有分類,有些書藏得很深,找起書來的確不方便。架構

後來,我想到了一個方法——最近快遞小哥送貨的包裝紙箱還在家裏,這些箱子不會很大,但裝書應該綽綽有餘,何不把這些箱子利用起來?
因而,我就動手挑選了一些大小合適的小紙箱,用簽字筆給每一個紙箱作了一個標記。this

1號紙箱,裝ASP.NET編程相關的書
2號紙箱,裝架構設計相關的書
3號紙箱,裝管理相關的書

N號紙箱,裝旅遊相關的書spa

自從將書分類裝到各個小紙箱後,經過標記我總能很快地找到想讀的書了,媳婦兒不再爲這事兒罵我了。
  1ADCD1B2

在生活中,不少讀者可能也遇到過此類問題,爲何找個東西就這麼難呢?架構設計

生活中的習慣會折射到編程中。當寫完一個方法時,有時因懶惰心理和拖延習慣,咱們可能會對本身說:「這個方法有時間再整理吧,先完成後續的功能」。
結果就是抱着這種心理,這個方法一直到項目上線都沒有整理過。
在項目維護期間,須要修改這個方法時,再次閱讀到這個方法,咱們不由抱怨:「我擦,這方法怎麼這麼長,這是誰寫的,我給他666!哎呦,不對,這好像是我本身寫的!」
  1AD76044 

提取方法

當一個方法包含實現一個功能的全部邏輯時,不只方法會看起來比較臃腫(可讀性差),也會給未來的維護形成困擾,每次改動都會讓你如履薄冰,而且較大可能帶來新的bug。這不符合咱們「未來的利益」,咱們可使用提取方法的重構策略來規避這個問題。設計

下面是我對提取方法的定義:3d

若是一個方法包含多個邏輯,咱們應將每一個邏輯提取出來,並確保每一個方法只作一件事情。htm

下圖表示了這個重構策略(藍色爲重構前,紅色爲重構後)。
image對象

示例

重構前

下面這段代碼定義了一個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()方法只包含調用各個子方法的邏輯,這已經精簡了不少,可讀性也有所加強。

image

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屬性體現。

image

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的過程是咱們對客觀事物的探索和認知過程,它也會隨着咱們瞭解到更多的事物細節而進化。

相關文章
相關標籤/搜索