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


前言

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

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

在生活中,我是一個比較隨意的人,平時也買了不少書去看。
我的書櫃不夠大,且已經裝滿了書,每當讀完一本書時,我懶得花些時間整理,想着以后再去整理這些書籍,所以我通常都將這些書塞到一個大箱子里面。
每次要重讀某些書時,我恨不得把這個箱子翻個底朝天,在花費“九牛二虎之力”之后,我終於找到了要看的書。
然后,我把其他翻出來的書再塞回去。

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

箱子那么大,所有的書都放在一個箱子里,一整箱書都沒有分類,有些書藏得很深,找起書來的確不方便。

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

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

N號紙箱,裝旅游相關的書

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

在生活中,很多讀者可能也遇到過此類問題,為什么找個東西就這么難呢?

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

提取方法

當一個方法包含實現一個功能的所有邏輯時,不僅方法會看起來比較臃腫(可讀性差),也會給將來的維護造成困擾,每次改動都會讓你如履薄冰,並且較大可能帶來新的bug。這不符合我們“將來的利益”,我們可以使用提取方法的重構策略來規避這個問題。

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

如果一個方法包含多個邏輯,我們應將每個邏輯提取出來,並確保每個方法只做一件事情。

下圖表示了這個重構策略(藍色為重構前,紅色為重構后)。
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的過程是我們對客觀事物的探索和認知過程,它也會隨着我們了解到更多的事物細節而進化。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM