在現代程式設計中,善用物件導的特性,可以解決很多不必要的判斷。接下來要來探討各種在物件導向的演化路上,會用到的重構。
今天先從簡單的開始。
底下是一個處理促銷套用的父類別和它的子類別們。子類別會進行一些欄位初始化,並告訴父類別它是屬於哪一種促銷;而父類別的 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;
}
}
這樣的實作會引起幾個疑問:
我希望能善用多型,讓各個子類自己負責自己該做的事就好,來看看怎麼做吧。
為了讓後續職責拆分比較方便,我先把 switch 中的每個 case 抽成方法。把要抽的程式碼段落選取起來,使用 Extract Method。

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

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

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

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

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

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

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

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

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

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

子類別們長這樣:

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