iT邦幫忙

2026 iThome 鐵人賽

DAY 30
0
Software Development

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

Day 30|從文藝復興到日常,把工藝精神留在下一次 commit

  • 分享至 

  • xImage
  •  

文藝復興的畫室,終究會關門

師傅會老去,畫室的招牌,總有一天會摘下來

但工藝精神,沒有跟著招牌一起消失
它活在每一位曾經在那裡磨過顏料的學徒身上
他們有些人,後來自己開了畫室,把同一套判斷力,教給下一代學徒

畫室會關,判斷力不會,這正是這 30 天,想留給你的東西

五個模組,走過的路

模組 隱喻 核心提醒
模組一・臃腫者 布魯內雷斯基的圓頂 比例,先於體積「大,不是問題;失衡的大,才是問題」
模組二・物件導向的濫用者 達文西的透視法 先懂結構,才畫得對形體「型別與繼承要用在對的地方」
模組三・變更的妨礙者 濕壁畫的時間壓力 顏料乾了之後,就改不動了「今天的設計,決定明天改得動、改不動」
模組四・可有可無者 阿伯提的節制美學 多餘的裝飾不是美,是負擔「敢刪,比敢加更需要判斷力」
模組五・過度的耦合者 畫室的分工倫理 師傅與學徒,不該互相代筆「邊界清楚,合作才走得久」

23 種壞味道,看似各自獨立,其實只是同一種能力的不同切面:判斷「這段程式碼,寫得好嗎?」

23 種壞味道,其實只有四種共通病灶

回頭看這一路走過的例子,會發現大部分壞味道,都能歸進幾個更根本的類別:

  • 命名與抽象不足:過長方法、靠註解掩蓋意圖,本質上都是「程式碼,沒有把自己的意圖說清楚」
  • 職責邊界不清:巨大類別、發散式變更、霰彈式修改,本質上都是「一個角色,管了太多事,或一件事,散落在太多角色手上」
  • 耦合過高:不適當的親密關係、訊息鏈,本質上都是「物件之間,知道了太多不該知道的事」
  • 過度設計或沒清乾淨:無用的程式碼、猜測性通用,本質上都是「留著一些,當下用不到、卻沒人敢動的東西」

下次你在 Code Review 裡覺得「這裡怪怪的」,卻說不清楚哪裡怪,先問自己一句:
這段程式碼,落在這四種病灶裡的哪一種?

命名問題,通常一個好名字就能解
邊界問題,需要重新想清楚職責
耦合問題,需要拉開類別之間的距離
清理問題,只需要一份刪除的勇氣

回到 Day 01 那段程式碼:多重警訊,先修哪一個?

Day 01 那段 AI 生成的 OrderProcessor,當時只點出三個警訊
三十天走下來,你手上的詞彙變多了,回頭看同一段程式碼,能認出的警訊,不只三個

// 同一段 Day 01 的程式碼,三十天後,你看得出比第一次更多警訊
public class OrderProcessor
{
    private readonly AppDbContext _db;   // 依賴具體實作而非抽象介面(對照 Day02.DIP)
    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,   // 七個參數(Long Parameter List,Day06)
        string customerType, string couponCode, bool sendEmail, bool sendSms)
    {
        // 驗證、算庫存、算折扣、寫 log、發通知——全部擠在同一個方法裡(Long Method,Day03)
        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")    // 字串代表會員等級(Primitive Obsession,Day05)
            price = price * 0.9m;
        else if (customerType == "REGULAR" && couponCode == "WELCOME10")   // 兩個分支,是 Switch Statement 的早期形狀(Day09)
            price = price * 0.95m;

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

        _log.Info("order processed: customer=" + customerId + " ...");

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

        return price;
        // 庫存規則、折扣規則、通知規則,任何一個改變都要動這個方法(Divergent Change 前兆,Day15)
    }
}

同一段程式碼,這次看出了六個警訊:

警訊 所屬模組/Day 現在會不會算錯結果
字串代表會員等級 模組一.Primitive Obsession(Day 05) 會,打錯字不會編譯錯誤,只會悄悄算錯折扣
方法做五件事 模組一.Long Method(Day 03) 不會,但每加一種需求,改動風險都在往上疊
七個參數 模組一.Long Parameter List(Day 06) 不會,是 Long Method 的副產物
依賴具體 AppDbContext 對照 Day 02.DIP 依賴反轉 不會,但這段邏輯現在幾乎沒辦法寫單元測試
兩個分支的 if-else 模組二.Switch Statements 前兆(Day 09) 不會,目前只有兩種會員等級
只有一種變動理由真的發生過 模組三.Divergent Change 前兆(Day 15) 不會,目前只有折扣規則真的改過

如果照 Day 01 提到的順序修,先拆 Long Method,再處理參數列表,最後才輪到字串比對
你會先花時間重新設計方法結構,而 production 裡的折扣,可能已經因為一次打錯字,悄悄算錯了好幾天

修的順序,不該照警訊被發現的順序,該照風險排

用 Day 18 那套「現在修或先記一筆」的判斷邏輯,套在這六個警訊上,會排出四層:

  1. 現在就修:字串代表會員等級,這不是「以後改起來貴」,是「現在就可能是錯的」
  2. 排進近期,不用今天:Long Method 與 Long Parameter List,兩者同源,抽出職責分開的方法或類別,會一起解決
  3. 先記一筆技術債,暫不動:依賴具體 AppDbContext,等真的需要幫這段邏輯寫單元測試,或真的要換資料存取方式,再處理
  4. 只需要觀察,先別碰:if-else 的 Switch 前兆、Divergent Change 前兆,兩者目前規模都還小,提前拆分反而可能變成 Day 24 猜測性通用的另一種變形。等真的出現第三種會員等級,或真的出現第二種變動理由,再處理也不遲

六個警訊,只有一個現在真的要修,這正是判斷力要做的事
「不是把每一種味道都聞出來就好,是聞出來之後,還能說清楚哪一個先處理」

5W1H,是拿來用的,不是拿來背的

這系列的每一篇文章,都在重複同一個節奏:

  • 這是什麼:先描述現象,不急著下判斷
  • 為什麼要在意:說清楚代價,難懂、難改、難測試、容易壞
  • 什麼時候該處理:找出警訊,變成 Code Review 時的直覺反應
  • 在哪裡找:知道這種味道,通常藏在專案的哪個角落
  • 怎麼解決:用一個具體、可操作的重構手法,而不是一句空泛的「寫好一點」

這五個問題,不是用來考試的知識點,是拿來在真實 Code Review 裡
把「我覺得怪怪的」翻譯成「這裡是 XX 壞味道,建議用 YY 重構手法處理」的共同語言

有共同語言,團隊才吵得起有意義的架,不是吵「你的風格我不喜歡」
是吵「這個設計,五年後會不會變成一場災難」

2個能立刻帶回團隊的習慣

  • 在 Code Review 留言裡,直接點名壞味道
    • 不說「這裡怪怪的」,說「這裡有點依戀情結,CalculateXxx 好像更該搬到 Order 裡」
    • 具體的詞彙,比模糊的直覺,更容易達成共識
  • 累積團隊自己的 Before / After 範例
    • 這系列示範的都是通用場景,你的團隊一定有自己更貼身的案例,把它們整理下來,比任何外部教材都更有說服力

童子軍軍規:離開畫架前,留下比接手時更乾淨的畫布

有一條簡單到近乎樸素的原則,比任何複雜的規範都更管用:

每次碰到一段程式碼,讓它,比你接手時更乾淨一點。

不用一次重寫整個模組,才叫改善
修掉一個容易誤導人的命名、拆掉一段過長的方法、刪掉一段沒人用的程式碼
每一次微小的動作,都在為下一位路過這裡的人,省下重新理解一次的成本

這正是文藝復興工藝精神的核心
不是靠一次偉大的傑作定義一位工匠,是靠日復一日,對手上這塊畫布的認真程度

AI 時代,這份判斷力,更值錢,不是更沒用

系列開頭提過:AI 讓寫程式碼的速度,前所未有地快

這不會讓 Code Smell 過時,反而讓它更重要,當產出程式碼的成本趨近於零
唯一還需要人類花時間把關的,就是 「這段程式碼,值不值得留下來」 這個判斷

AI 可以幫你把顏料調好、把畫布繃平,但「這幅畫,構圖好不好、比例對不對、有沒有多餘的裝飾」
這份判斷力,還是得靠人

系列自我檢核:你準備好了嗎

  • [ ] 我能不看提示,說出至少 15 種 Code Smell 的名稱與核心症狀
  • [ ] 我能在自己的專案裡,實際找到至少 3 種今天學過的壞味道
  • [ ] 我對其中至少 1 種,已經動手重構過,並且理解重構前後的取捨
  • [ ] 我能在一段同時有好幾種警訊的程式碼裡,排出合理的處理順序,而不是照發現的順序依序修
  • [ ] 我能用「這是哪一種壞味道」這樣的語言,跟同事討論一段程式碼,而不是只說「這裡怪怪的」
  • [ ] 我在下一次 Code Review 裡,準備好把這份判斷力,用出來了

寫在最後

謝謝你陪這個系列,走完 30 天

從布魯內雷斯基的圓頂,到畫室裡收工前的最後一眼
這段旅程談的,始終不是某一種特定的程式語言或框架,是一種可以帶著走的判斷力

下一次你打開一段程式碼,覺得「這裡,有點味道」的時候
希望你已經有了足夠的詞彙,把那股直覺,講清楚,也有了足夠的手法,把它變得更好

畫室的門,會關;但工藝精神,留在每一次你認真對待的 commit 裡


上一篇
Day 29|只負責轉達話語,自己不畫一筆的傳令:中間人 (Middle Man)
系列文
文藝復興:這段程式碼,好像有點味道30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言