探險開始(「為什麼」)

我仍記得第一次打開一個 2,000 行的檔案,檔名是 report_generator.py。那是一個怪物,多年來不斷被修補、調整,以及「快速修正」。每當我需要在 CSV 匯出中新增一欄時,就得在無止盡的 if/else 區塊、複製貼上的程式片段,以及命名如 tmpdata2stuff 的變數中捲動。花了一小時尋找日期格式化的地方後,我才發現自己引進了一個 bug,它躲過了測試並損壞了客戶的發票。

那一刻感覺就像在沒有火把的情況下走進漆黑的洞穴。我知道程式能運作——某種程度上——但它很脆弱,讓人害怕去碰觸,每一次修改都像在拆炸彈。我問自己:我們要如何讓這個怪物變得可維護,而不必從頭重寫?

答案不是華麗的框架或新語言,而是一個簡單、可重複的習慣:抽取出小而專注的函式

啟示(洞見)

做法很簡單:每當你看到一段程式碼在做一件邏輯上的事情——即使只有三行——就把它抽取成自己的函式,並用一個描述「做了什麼」而不是「怎麼做」的名稱來命名。

為什麼這感覺像在廢棄場找到一把光劍?

  1. 可讀性大幅提升——函式名稱講述故事;函式本體則說明細節。
  2. 測試變得容易——你可以針對抽取出的片段進行獨立的單元測試。
  3. 未來的修改可以局部化——如果日期格式改變,你只需要編輯一個函式,而不是十個分散的地方。
  4. 重複的程式碼減少——一旦抽取出一個片段,你會發現同樣的邏輯出現在其他地方,進而可以重複使用。

如果你跳過這一步,就會產生「義大利麵條程式碼」,單一職責散落在多處。代價是?更多 bug、更長的上手時間,以及團隊害怕碰觸該檔案。我曾見過團隊因為一個小修改就需要完整的回歸測試套件才能放心,而浪費了數週的時間。

運用力量(程式碼與範例)

之前——掙扎

def generate_report(data):
    # 1. Filter active users
    active_users = []
    for u in data['users']:
        if u['status'] == 'active' and u['last_login'] > datetime.now() - timedelta(days=30):
            active_users.append(u)

    # 2. Compute totals
    total_sales = 0
    for u in active_users:
        for o in u['orders']:
            if o['date'].year == datetime.now().year:
                total_sales += o['amount']

    # 3. Format date for header
    header_date = datetime.now().strftime('%B %d, %Y')# 4. Build CSV lines
    lines = [f"Report generated on {header_date}"]
    lines.append("User ID, Name, Total Spent")
    for u in active_users:
        user_total = sum(o['amount'] for o in u['orders'] if o['date'].year == datetime.now().year)
        lines.append(f"{u['id']}, {u['name']}, {user_total:.2f}")

    lines.append(f"Grand Total Sales: {total_sales:.2f}")
    return "\n".join(lines)

Enter fullscreen mode Exit fullscreen mode

這裡發生了什麼事?

  • 這個函式做了四件不同的事情:篩選、加總、格式化日期,以及組裝 CSV 輸出。
  • 相同的年份檢查邏輯出現了兩次(一次用於銷售,一次用於每個使用者)。
  • 如果業務決定改變「active」的定義(例如包含試用使用者),你必須在兩個迴圈中搜尋,並可能遺漏某處。

之後——勝利

def _filter_active_users(users):
    """Return users who are active and logged in within the last 30 days."""
    cutoff = datetime.now() - timedelta(days=30)
    return [u for u in users if u['status'] == 'active' and u['last_login'] > cutoff]

def _yearly_total(orders):
    """Sum order amounts that fall in the current year."""
    now = datetime.now()
    return sum(o['amount'] for o in o['orders'] if o['date'].year == now.year)

def _format_header_date():
    return datetime.now().strftime('%B %d, %Y')

def generate_report(data):
    active_users = _filter_active_users(data['users'])
    header_date = _format_header_date()
    total_sales = _yearly_total([u['orders'] for u in active_users])  # flatten for simplicity

    lines = [f"Report generated on {header_date}"]
    lines.append("User ID, Name, Total Spent")
    for u in active_users:
        user_total = _yearly_total(u['orders'])
        lines.append(f"{u['id']}, {u['name']}, {user_total:.2f}")

    lines.append(f"Grand Total Sales: {total_sales:.2f}")
    return "\n".join(lines)

Enter fullscreen mode Exit fullscreen mode

有什麼改變?

  • 每個輔助函式都做一件事,並以該件事命名。
  • 年份篩選邏輯放在 _yearly_total 中,因此如果規則改變,我們只需要修改一處。
  • 主函式現在看起來像一份高階食譜:篩選使用者、取得標頭、計算總計、建立行。
  • 新增一欄(例如「平均訂單金額」)只需要再呼叫一個小輔助函式——不需要深入巢狀迴圈。

常見陷阱要避免

  • 抽取過多:不要只因為可以就抽取一行;輔助函式應該增加清晰度,而不是噪音。
  • 在名稱中洩漏實作細節get_data() 告訴我們什麼都沒有;filter_active_users() 告訴我們到底發生了什麼。
  • 忘記回傳純值:輔助函式應該避免副作用(如修改外部狀態),除非那是明確的目的。

當你遵守這些規範時,抽取出的函式就會變成可重複使用的建構模組——就像樂高積木,你可以在不破壞現有模型的情況下,以新方式將它們組裝在一起。

為什麼這個新能力很重要

在我開始認真抽取方法之後,report_generator.py 檔案從 2,000 行縮減到約 650 行,團隊的信心也隨之高漲。

  • Bug 率下降——因為每個部分都可以獨立進行單元測試,我們在將邏輯錯誤送進 CI 之前就抓到了。
  • 上手速度加快——新進人員可以快速閱讀頂層流程,了解模組的功能。
  • 重構變得安全——我們可以將日期格式化器替換成支援時區的版本,只需編輯單一函式,所有呼叫者就會自動更新。

總之,抽取方法將一個令人畏懼的舊有程式碼庫,變成我實際上樂於進行修改的地方。這是一種每天都能帶來回報的超能力。


輪到你了

在你目前的專案中,挑選一個感覺像是「神方法」的函式——那個什麼都做一點的函式。花十分鐘將其中一個邏輯區塊抽取成命名良好的輔助函式。執行你的測試,看看清晰度如何改善,然後重複這個步驟。

你想重構的第一個函式是什麼?在留言區分享你的前後對照片段——我很想知道你自己的「新希望」是如何實現的! 🚀