iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
Software Development

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

Day 03|一氣呵成的迷思:過長方法 (Long Method)

  • 分享至 

  • xImage
  •  

昨天,我們把五條規矩刻上了畫室門楣
今天,第一次套進具體場景

1296 年,佛羅倫斯開始蓋聖母百花大教堂,八角形的底座很早就完成了,唯獨中間那個洞
留給圓頂的位置,空了超過一百年

不是沒有錢,也不是沒有工匠,是沒有人知道該怎麼蓋
傳統做法要先搭一整套木造鷹架撐住重量,但這個圓頂的跨度,寬到找不到那麼長的木頭

1418 年,菲利普·布魯內雷斯基(Filippo Brunelleschi)給的答案
不是找到更粗的木頭,而是放棄鷹架這個假設本身

圓頂做成內外兩層,磚塊用人字紋排列,讓每一圈磚在砌上去的當下,就能自己撐住自己

每一圈磚,都是一個獨立完工的單位
砌完這一圈、確認它撐得住自己,才開始下一圈

沒有人想過要「一口氣」把整座圓頂澆築成一塊,那是不可能的事
沒有人扛得住那樣的重量,也沒有人看得懂那樣的結構

圓頂立起來了,不是因為磚頭堆得比較多,是因為每一圈磚
在被放上去的那一刻,就已經知道自己在整體結構裡的位置

這是模組一「臃腫者」要處理的五個警訊:

Day Code Smell 一句話定位
03 過長方法 (Long Method) 一個方法做了五件事,卻只有一個名字
04 巨大類別 (Large Class) 什麼職責都攬在身上的「上帝類別」
05 原始型別執念 (Primitive Obsession) 用一串 string 硬撐一個本該有名字的概念
06 過長參數列表 (Long Parameter List) 呼叫一個方法,要先背下七個參數的順序
07 資料泥團 (Data Clumps) 總是一起出現,卻始終沒被封裝成一個名字的三兄弟

第一站,從最常見的一種開始

一個方法,做了五件事

回到 Day 01 那段「能跑」的 OrderProcessor

public class OrderProcessor
{
    private readonly AppDbContext _db;
    private readonly ILogger _log;
    private readonly IEmailService _emailService;
    private readonly ISmsService _smsService;

    public OrderProcessor(AppDbContext db, ILogger log,
        IEmailService emailService, ISmsService smsService)
    {
        _db = db;
        _log = log;
        _emailService = emailService;
        _smsService = smsService;
    }

    public decimal Process(int customerId, int productId, int qty,
        string customerType, string couponCode, bool sendEmail, bool sendSms)
    {
        var customer = _db.Customers.Find(customerId);
        var product = _db.Products.Find(productId);

        if (customer == null || product == null)
            throw new InvalidOperationException("not found");

        if (product.Stock < qty)
            throw new InvalidOperationException("out of stock");

        decimal price = product.Price * qty;

        if (customerType == "VIP")
            price = price * 0.9m;
        else if (customerType == "REGULAR" && couponCode == "WELCOME10")
            price = price * 0.95m;

        product.Stock = product.Stock - qty;
        _db.SaveChanges();

        _log.Info("order processed: customer=" + customerId +
            " product=" + productId + " qty=" + qty + " price=" + price);

        if (sendEmail)
            _emailService.Send(customer.Email, "Order Confirmed", "...");
        if (sendSms)
            _smsService.Send(customer.Phone, "...");

        return price;
    }
}

一個 Process 方法,同時做了:

  • 找資料、驗證資料
  • 算價格
  • 扣庫存、存檔
  • 寫 log
  • 發通知

五件事,疊在同一個方法裡,像沒分層就想澆築完成的圓頂

為什麼「一氣呵成」是個陷阱

沒有人是故意把方法寫長的

通常的過程是這樣:

  1. 一開始只是「驗證 + 算價格」,20 行,看起來還好
  2. 有人說「順便扣個庫存吧」,加了 5 行
  3. 有人說「要記 log 方便查」,又加了 3 行
  4. 有人說「客戶會想知道訂單成立」,發信、發簡訊都補了進去

每一次加的都不多,加總起來,就是一整座撐不住自己的圓頂

問題不在「這段程式碼寫錯了」——它能編譯、能跑、demo 也會過,問題在於:

半年後,有人要改折扣邏輯,得先讀完全部 60 行,才敢確定改哪一段不會動到別的東西

這才是 Long Method 真正的代價:不是跑不動,是沒有人敢改

一圈一圈砌:Extract Method

布魯內雷斯基的做法,換成程式碼語言,就是提煉方法 (Extract Method)

把每一個「獨立完工的單位」抽出來,各自負責一件事:

public class OrderProcessor
{
    private readonly AppDbContext _db;
    private readonly ILogger _log;
    private readonly IEmailService _emailService;
    private readonly ISmsService _smsService;

    public OrderProcessor(AppDbContext db, ILogger log,
        IEmailService emailService, ISmsService smsService)
    {
        _db = db;
        _log = log;
        _emailService = emailService;
        _smsService = smsService;
    }

    public decimal Process(int customerId, int productId, int qty,
        string customerType, string couponCode, bool sendEmail, bool sendSms)
    {
        var (customer, product) = LoadAndValidate(customerId, productId, qty);
        decimal price = CalculatePrice(product, qty, customerType, couponCode);

        DeductStock(product, qty);
        LogOrder(customerId, productId, qty, price);
        NotifyCustomer(customer, sendEmail, sendSms);

        return price;
    }

    private (Customer, Product) LoadAndValidate(int customerId, int productId, int qty)
    {
        var customer = _db.Customers.Find(customerId);
        var product = _db.Products.Find(productId);

        if (customer == null || product == null)
            throw new InvalidOperationException("not found");

        if (product.Stock < qty)
            throw new InvalidOperationException("out of stock");

        return (customer, product);
    }

    private decimal CalculatePrice(Product product, int qty, string customerType, string couponCode)
    {
        decimal price = product.Price * qty;

        if (customerType == "VIP")
            price *= 0.9m;
        else if (customerType == "REGULAR" && couponCode == "WELCOME10")
            price *= 0.95m;

        return price;
    }

    private void DeductStock(Product product, int qty)
    {
        product.Stock -= qty;
        _db.SaveChanges();
    }

    private void LogOrder(int customerId, int productId, int qty, decimal price)
    {
        _log.Info($"order processed: customer={customerId} product={productId} qty={qty} price={price}");
    }

    private void NotifyCustomer(Customer customer, bool sendEmail, bool sendSms)
    {
        if (sendEmail)
            _emailService.Send(customer.Email, "Order Confirmed", "...");
        if (sendSms)
            _smsService.Send(customer.Phone, "...");
    }
}

Process 現在讀起來,像一份施工順序表:
驗證 → 算價 → 扣庫存 → 記錄 → 通知

每一段獨立完工,也各自可以單獨測試

這不是終點,是下一站的起點

眼尖的人可能注意到,CalculatePrice 裡還藏著一句:

if (customerType == "VIP")

這句話本身,就是 Day 05 要談的另一種壞味道 「原始型別執念(Primitive Obsession)」

不過在那之前,我們還要先去看看,一個類別如果把太多職責攬在身上,會變成什麼樣子

那就是明天的 「巨大類別(Large Class)」

什麼時候該動手拆?

不是所有方法都要拆到只剩三行,判斷的標準比較像這樣:

  • 這個方法的名字,能不能只用一個動詞就講完它在做的事?
  • 讀這個方法的時候,會不會需要先跳去看某一段的「意圖」才看得懂?
  • 如果要幫其中一小段寫測試,是不是得連帶把整個方法都跑一遍?

只要有一題答案是「不行」,就是該分層砌磚的時候了

自我檢查清單

  1. 這個方法,我能用一句話(一個動詞)說完它在做什麼嗎?
  2. 方法裡有沒有可以被一句註解概括的連續程式碼區塊?
  3. 如果只想測試「折扣怎麼算」,我需要連帶跑過整個流程嗎?
  4. 這個方法的長度,是設計出來的,還是一次次「順便加一下」疊出來的?
  5. 拆出來的每一小段,是不是都能取一個精準的動詞名字?

明日預告

明天我們把鏡頭拉遠,看一整間畫室
如果一個工地主任身兼建築師、監工、會計、採購,所有事情都攬在自己身上,會發生什麼事?

模組一第二站:巨大類別(Large Class)


上一篇
Day 02|行會規範:畫室開工前,刻在門楣上的五條規矩(SOLID)
下一篇
Day 04|身兼一切的工地主任:巨大類別 (Large Class)
系列文
文藝復興:這段程式碼,好像有點味道4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言