iT邦幫忙

2025 iThome 鐵人賽

0
Software Development

沒測試也敢重構?IDE 安全重構 30 日生存指南系列 第 18

Day 18. Push Members Down:將力量傳承給子嗣

  • 分享至 

  • xImage
  •  

在現代程式設計中,善用物件導的特性,可以解決很多不必要的判斷。接下來要來探討各種在物件導向的演化路上,會用到的重構。

今天先從簡單的開始。

1. 當父類職責太多

底下是一個處理促銷套用的父類別和它的子類別們。子類別會進行一些欄位初始化,並告訴父類別它是屬於哪一種促銷;而父類別的 Apply 負責依照不同的促銷類型來進行套用。先別問為何程式會長這樣,Senior 說他接手的時候就是這個樣子了。

public abstract class PromotionApplier
{
    private readonly PromotionType _promotionType;

    internal PromotionApplier(PromotionType promotionType)
    {
        _promotionType = promotionType;
    }

    protected string? CouponCode { get; set; }
    protected decimal? Threshold { get; set; }
    protected decimal? Percentage { get; set; }
    protected bool FreeShipping { get; set; }

    public void Apply(Order order)
    {
        switch (_promotionType)
        {
            case PromotionType.FreeShipping:
            {
                order.ShippingFee = 0;
                break;
            }
            case PromotionType.Coupon:
            {
                if (!string.IsNullOrEmpty(CouponCode) && order.HasCoupon(CouponCode))
                    ApplyPercentage(order, Percentage ?? 0);
                break;
            }
            case PromotionType.Threshold:
            {
                if (Threshold.HasValue && Percentage.HasValue && order.Subtotal >= Threshold.Value)
                    ApplyPercentage(order, Percentage.Value);
                break;
            }
            default:
                throw new NotSupportedException();
        }
    }

    protected void ApplyPercentage(Order order, decimal percent)
    {
        order.Subtotal -= decimal.Round(order.Subtotal * (percent / 100m), 2);
    }
}

public class FreeShippingPromotionApplier : PromotionApplier
{
    public FreeShippingPromotionApplier() : base(PromotionType.FreeShipping)
    {
        FreeShipping = true;
    }
}

public class CouponPromotionApplier : PromotionApplier
{
    public CouponPromotionApplier(string code, decimal percent) : base(PromotionType.Coupon)
    {
        CouponCode = code;
        Percentage = percent;
    }
}

public class ThresholdPromotionApplier : PromotionApplier
{
    public ThresholdPromotionApplier(decimal threshold, decimal percent) : base(PromotionType.Threshold)
    {
        Threshold = threshold;
        Percentage = percent;
    }
}

這樣的實作會引起幾個疑問:

  1. 子類明明就知道他是誰了,為何還要再告訴父類別促銷類型。
  2. 父類別職責太多了。而且新增子類的時候,還會改到父類別。
  3. 要是那個剛來的實習生,把子類傳給父類的促銷類型隨便找一個貼上、沒注意到哪些父類的欄位要初始化,我還要加班去處理 bug。

我希望能善用多型,讓各個子類自己負責自己該做的事就好,來看看怎麼做吧。

2. 前置處理

為了讓後續職責拆分比較方便,我先把 switch 中的每個 case 抽成方法。把要抽的程式碼段落選取起來,使用 Extract Method

https://ithelp.ithome.com.tw/upload/images/20251017/20169414BkyENnVhGY.png

到這邊應該是很簡單的操作,為了節省篇幅,就直接跳到執行後的結果吧。

https://ithelp.ithome.com.tw/upload/images/20251017/201694148iqrzkSQsX.png

3. Push Members Down

目前各個 case 的內容都抽成方法了,準備來把職責分給各個子類別。在方法上面選擇 Push Members Down

https://ithelp.ithome.com.tw/upload/images/20251118/20169414CrVJHcdTGY.png

彈窗中的下方代表的是要移動的方法,上方代表的是要移動到哪個子類別。都選好後按下 Next。

https://ithelp.ithome.com.tw/upload/images/20251118/20169414WDkU3ddPKj.png

跳出了一個警告。仔細看原來是因為我的方法宣告為 private,移動到子類別後原本的方法就無法存取了。我先無視警告,往後執行看看會發生什麼事。

https://ithelp.ithome.com.tw/upload/images/20251118/201694142ojw9Z1WtK.png

哇,變成紅色了!想想也是,父類別怎麼能存取子類別呢,得想想其它的策略才行。

https://ithelp.ithome.com.tw/upload/images/20251118/20169414nP0SCBKnCh.png

回到上一步看看,發現這邊有個 Make Abstract 的選項。合理,宣告成 abstract 之後,父類別就能使用子類別的方法了。一路按 Next 下去。

https://ithelp.ithome.com.tw/upload/images/20251118/2016941427Fg71LQka.png

嗯,這次編譯沒有問題,Rider 還很貼心地幫我把方法改成 protected。接下來再逐個分配其它方法到它們各自的子類吧...等等。

https://ithelp.ithome.com.tw/upload/images/20251118/20169414I2I4R62BRW.png

第一個子類別確實沒問題,不過另外兩個子類別,因為有未實作的抽象方法,而導致編譯問題。

https://ithelp.ithome.com.tw/upload/images/20251118/2016941483p4abgmcS.png

我可以回頭把 abstract 改成 virtual,在父類別提供預設實作來解決這個問題。但人改就會有錯,而且跳出編譯問題也不符合我們一開始宣稱要做的安全重構。正如同 TDD 不見得每次都能一步到位,重構也往往需要來回嘗試不同路徑,才能找到最佳的做法。總之先復原回去。

這次我選擇從這個路由 method 執行,一樣選擇 Push Members Down
https://ithelp.ithome.com.tw/upload/images/20251118/20169414bN8MTVUKS0.png

選擇 Make Abstract,並一口氣分派到所有子類別去。

https://ithelp.ithome.com.tw/upload/images/20251118/20169414jQkmvgfBcl.png

這次順順利利完成,沒有任何錯誤。父類別現在長這樣:

https://ithelp.ithome.com.tw/upload/images/20251118/20169414EINCNj9wuH.png

子類別們長這樣:

https://ithelp.ithome.com.tw/upload/images/20251118/20169414PjR0iAkfN0.png

雖然原本的一個方法,突然變成三個,看似多出很多重複的程式碼,但這是重構必經之路。有時候先放棄局部最佳化,反而更能達到全域最佳化。做到這邊眼睛有點痠了,考量到大腦的處理效率,我應該把未完成的篇幅移到下一篇,這也是為了通往全域最佳化嘛!


上一篇
Day 17. if 的世界線之 4:富有既視感的條件式
系列文
沒測試也敢重構?IDE 安全重構 30 日生存指南18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言