[ASP.NET]重構之路系列v8 –合併重複的條件片段

[ASP.NET]重構之路系列v8 –合併重複的條件片段

前言
這次的目標一樣是用抽象地角度來看我們的程式碼,用人類的話來解釋程式碼的目的與行為,並且避免重複的程式碼出現。

需求說明
我們有一個Bus的class,上面有一個Charge的方法,會根據乘客的年齡與性別,來決定要收費多少。

Spec是這樣寫的:

  1. 乘客是女生的話,低於60歲,收費為原價的8折;超過60歲則不收費。
  2. 乘客是男生的話,低於50歲,收費為原價的9折;50~60歲的,收費為原價的95折;超過60歲則不收費。


不先思考,直接動手寫程式,就會長出這樣『合理』的程式碼:


    public class Bus
    {
        public double Charge(Person customer)
        {
            const double ticketCost = 100;
            double result = 0;

            if (customer.IsFemale)
            {
                if (customer.Age <= 60)
                {
                    result = ticketCost * 0.8;
                }
                else
                {
                    return 0;
                }
            }
            else
            {
                if (customer.Age <= 50)
                {
                    result = ticketCost * 0.9;
                }
                else if (customer.Age <= 60)
                {
                    result = ticketCost * 0.85;
                }
                else
                {
                    return 0;
                }
            }

            return result;
        }
    }

這一段code問題在哪?我們繼續看下去。

重構步驟
步驟一:
首先,先看重複的code在哪裡,不管從需求或是程式碼,都可以看到:『超過60歲,則不收費』=> 這代表了老人的票價與性別無關。既然與性別無關,這一段程式碼就不該放在判斷性別的if判斷式裡面。

image 

調整之後如下:


        public double Charge(Person customer)
        {
            ////超過60歲 => 老人,票價為0
            if (customer.Age > 60)
            {
                return 0;
            }

            const double ticketCost = 100;
            double result = 0;

            if (customer.IsFemale)
            {
                if (customer.Age <= 60)
                {
                    result = ticketCost * 0.8;
                }                
            }
            else
            {
                if (customer.Age <= 50)
                {
                    result = ticketCost * 0.9;
                }
                else if (customer.Age <= 60)
                {
                    result = ticketCost * 0.85;
                }                
            }

            return result;
        }


步驟二:
還有哪邊是一樣的,乍看之下沒有,但倘若抽象一點來看(大家可以把眼睛瞇起來一點 XD),我們會看到,實際判斷式影響的內容是『折扣』。這個時候,千萬不要直接很高興、很帥氣的就把重複的東西都抽出來。要思考的是,是否計價的商業邏輯,就是『原價*折扣』。

建議此時可以問一下RA, SA或domain expert,詢問一下,計價方式=原價*折扣,『基本上』是否穩定不變。

我們假設專家的回答是:「基本上就是原價*折扣,但偶爾會有例外,而且折扣的值可能會變」。

基於這樣的domain know-how,我們可以再將計價公式抽象化。

image 

步驟三:
接著,我們要消滅magic number,基本上除了0跟1以外,邏輯的code裡面應該是不會出現其他數字的。(甚至應該說,只有0是被允許的)

如果這樣還是不容易懂,簡單的說,我們應該賦予那些數字意義,用意義來寫code。程式碼才好懂,也才好維護。我們這邊用最簡單的方法,將這些數字定義成const常數。


    public class Bus
    {
        private const double olderPrice = 0;
        private const double youngLadyDiscount = 0.8;
        private const double youngManDiscount = 0.9;
        private const double strongManDiscount = 0.85;

        private const int olderAge = 60;
        private const int strongAge = 50;

        private const double ticketCost = 100;

        public double Charge(Person customer)
        {
            ////超過60歲 => 老人,票價為0
            if (customer.Age > olderAge)
            {
                return olderPrice;
            }

            ////折扣
            double discount = 0;

            if (customer.IsFemale)
            {
                if (customer.Age <= olderAge)
                {
                    discount = youngLadyDiscount;
                }
            }
            else
            {
                if (customer.Age <= strongAge)
                {
                    discount = youngManDiscount;
                }
                else if (customer.Age <= olderAge)
                {
                    discount = strongManDiscount;
                }
            }

            ////計價方式=票價*折扣
            double result = ticketCost * discount;
            return result;
        }
    }

未來,倘若只是折扣改變,我們可以直接改const的值就可以。如果折扣是透過其他business logic來決定,那麼我們可以把const封裝成property,或其他function,這樣我們的Charge方法還是不需要改變。

如果,連計價方式都會有許多種,且未來可能有更多種,那麼計價方式可能就可以以其他的pattern來進行重構,例如strategy pattern。

結論
這一篇的重點在於,在條件式的分支中,有著相同的程式碼,代表著這一段程式碼並不會被這個條件式所影響,這時候放在條件式的分支中,就會顯得無法清楚解釋出邏輯。條件式分支中的程式碼,應該就只是說明,根據這樣的條件所影響不同的變化。

把原本順著spec寫的程式碼,重新的整理了一下,相信很多人會覺得原本的程式,並沒有太大問題。有沒有需要重構,基本上還算是見仁見智。不過,比較一下前後的程式碼,會不會覺得後面的程式碼比較乾淨一點,比較好懂一點,以後比較容易維護一點了呢?

最後,步驟三的程式碼,還是有重構的空間,不過這篇文章要表達的意思已經到了,我就不繼續往下重構了。『重構-改善既有程式的設計』這本書中的第二章『重構原則』中有提到,何時該重構,以及何時不該重構。建議大家可以看看。

希望每個工程師,都可以對的起自己寫出來的code,也都可以對他們負責 :)

 

對敏捷開發有興趣的朋友,可以參考我的粉絲專頁:91敏捷開發之路

對 TDD 課程有興趣的朋友,課程內容、大綱與學員心得,可以參考 skilltree 的公開課程:自動測試與 TDD 實務開發

若需要聯絡我,可以透過粉絲專頁私訊或是側欄的關於我。