iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Software Development

文藝復興:這段程式碼,好像有點味道系列 第 10

Day 10|畫布上只在某幾筆才用到的顏料槽:暫時欄位 (Temporary Field)

  • 分享至 

  • xImage
  •  

畫室裡的工作台,是共用的
今天甲師傅接了一件濕壁畫委託,把灰泥、顏料、刮刀擺上工作台,開始動工
如果乙師傅在甲師傅還沒收工前,也搬了自己的材料上同一張工作台

兩人的顏料混在一起,誰的刮刀屬於誰都分不清楚

工作台是共用的,但材料應該是各自獨立的,混在一起,就是災難的開始

一個為了「省參數」而加的欄位

Day 06 教會我們,參數太多要包成物件。 但這個教訓,被人用歪了

團隊要幫 OrderProcessor 加一個「運費試算」功能,計算過程要分三步:基本費、偏遠地區附加費、快遞附加費

為了不要讓每個私有方法都重複傳 weightzoneisExpress,有人想到一個「聰明」的辦法
爭什麼啊,「參在一起做撒尿牛丸啊」
「把它們存成類別的欄位」

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 物件的時候,它們是 0null
  • 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),看起來很像,差別在於:

  • 參數物件:把一堆相關參數包起來,讓方法簽名變乾淨,但物件本身不做事,只是資料的容器
  • 方法物件:不只包住資料,還把用到這些資料的演算法一起搬進來,讓這個物件自己完成整件事

如果你發現自己「借」了類別的欄位來存放暫時性的計算資料
該想的不是怎麼把欄位清乾淨,是這段演算法該不該有自己的家

自我檢查清單

  1. 這個欄位,是不是只有在特定方法執行時才有值,其他時候都是預設值或 null
  2. 這個欄位存在的理由,是不是只是為了讓內部的幾個私有方法少傳幾個參數?
  3. 這個類別,會不會在 DI 容器裡被註冊成 Singleton 或長生命週期的服務?
  4. 如果兩個執行緒同時呼叫用到這個欄位的方法,結果還會是對的嗎?
  5. 這組暫時欄位,加上用到它們的邏輯,是不是其實該搬去一個獨立的類別?

明日預告

明天我們回到繼承的世界,看一個子類別繼承了父類別的招牌,卻用不上父類別教的技法,甚至得刻意把某個方法改成「拒絕使用」

模組二第三站:拒絕的遺贈(Refused Bequest)


上一篇
Day 09|每加一種類型就多畫一筆的素描:Switch 陳述式 (Switch Statements)
下一篇
Day 11|繼承了畫室招牌,卻不用畫室的技法:拒絕的遺贈 (Refused Bequest)
系列文
文藝復興:這段程式碼,好像有點味道11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言