畫室裡的工作台,是共用的
今天甲師傅接了一件濕壁畫委託,把灰泥、顏料、刮刀擺上工作台,開始動工
如果乙師傅在甲師傅還沒收工前,也搬了自己的材料上同一張工作台兩人的顏料混在一起,誰的刮刀屬於誰都分不清楚
工作台是共用的,但材料應該是各自獨立的,混在一起,就是災難的開始
Day 06 教會我們,參數太多要包成物件。 但這個教訓,被人用歪了
團隊要幫 OrderProcessor 加一個「運費試算」功能,計算過程要分三步:基本費、偏遠地區附加費、快遞附加費
為了不要讓每個私有方法都重複傳 weight、zone、isExpress,有人想到一個「聰明」的辦法爭什麼啊,「參在一起做撒尿牛丸啊」
「把它們存成類別的欄位」
public class OrderProcessor
{
// ...既有欄位...
// 為了讓三個私有方法都能存取,暫時借放在這裡
private decimal _pendingWeight;
private string _pendingZone;
private bool _pendingIsExpress;
public decimal CalculateShippingFee(decimal weight, string zone, bool isExpress)
{
_pendingWeight = weight;
_pendingZone = zone;
_pendingIsExpress = isExpress;
decimal fee = BaseRate() + ZoneSurcharge() + ExpressSurcharge();
return fee;
}
private decimal BaseRate() => _pendingWeight * 20m;
private decimal ZoneSurcharge() =>
ZoneSurcharges.GetValueOrDefault(_pendingZone, 30m);
private decimal ExpressSurcharge() =>
_pendingIsExpress ? _pendingWeight * 5m : 0m;
private static readonly Dictionary<string, decimal> ZoneSurcharges = new()
{
["北"] = 0m,
["中"] = 10m,
["南"] = 20m
};
}
有了三個私有方法確實不用再傳參數了,看起來乾淨了不少
問題是 _pendingWeight、_pendingZone、_pendingIsExpress 這三個欄位
只有在 CalculateShippingFee 執行的那幾毫秒裡,才有意義
在其他任何時候:
OrderProcessor 物件的時候,它們是 0 或 null
CalculateShippingFee 執行完之後,它們還留著上一次計算的舊值,沒有人清空這就像工作台上,留著上一位師傅沒收拾乾淨的顏料
下一個人看到,不知道這是還在用的材料,還是該丟掉的殘留物
如果 OrderProcessor 只有一個實例、一次只服務一個人,這幾個欄位頂多是「看起來混亂」
但如果 OrderProcessor 在 DI 容器裡被註冊成 Singleton,多個請求會共用同一個實例
想像兩個客戶,幾乎同時呼叫 CalculateShippingFee:
// Thread A:客戶甲,重量 5 公斤,北區
// Thread B:客戶乙,重量 20 公斤,南區
兩個執行緒,同時寫入同一組 _pendingWeight、_pendingZone
客戶甲算出來的運費,可能用了客戶乙的重量
這不是理論上的風險,這是共用工作台一定會發生的事,只是時間早晚(想到就覺得刺激)
解法的核心思想就是,這三個暫時欄位,連同用到它們的演算法,其實共同代表著「一次獨立的運費試算任務」
它們該有自己的家,而不是寄居在 OrderProcessor 身上
換成程式碼,這是以方法物件取代方法 (Replace Method with Method Object):
public class ShippingFeeJob
{
private readonly decimal _weight;
private readonly string _zone;
private readonly bool _isExpress;
public ShippingFeeJob(decimal weight, string zone, bool isExpress)
{
_weight = weight;
_zone = zone;
_isExpress = isExpress;
}
public decimal Execute() => BaseRate() + ZoneSurcharge() + ExpressSurcharge();
private decimal BaseRate() => _weight * 20m;
private decimal ZoneSurcharge() =>
ZoneSurcharges.GetValueOrDefault(_zone, 30m);
private decimal ExpressSurcharge() =>
_isExpress ? _weight * 5m : 0m;
private static readonly Dictionary<string, decimal> ZoneSurcharges = new()
{
["北"] = 0m,
["中"] = 10m,
["南"] = 20m
};
}
OrderProcessor 恢復乾淨,不再持有任何暫時狀態:
public decimal CalculateShippingFee(decimal weight, string zone, bool isExpress) =>
new ShippingFeeJob(weight, zone, isExpress).Execute();
每一次呼叫,都會建立一個全新的 ShippingFeeJob
每個執行緒,都在自己專屬的工作台上作業,材料不會再混在一起
Day 06 學到的參數物件(OrderRequest),跟今天的方法物件(ShippingFeeJob),看起來很像,差別在於:
如果你發現自己「借」了類別的欄位來存放暫時性的計算資料
該想的不是怎麼把欄位清乾淨,是這段演算法該不該有自己的家
null?明天我們回到繼承的世界,看一個子類別繼承了父類別的招牌,卻用不上父類別教的技法,甚至得刻意把某個方法改成「拒絕使用」
模組二第三站:拒絕的遺贈(Refused Bequest)