探險開始(「為什麼」)
我仍記得第一次打開一個 2,000 行的檔案,檔名是 report_generator.py。那是一個怪物,多年來不斷被修補、調整,以及「快速修正」。每當我需要在 CSV 匯出中新增一欄時,就得在無止盡的 if/else 區塊、複製貼上的程式片段,以及命名如 tmp、data2 和 stuff 的變數中捲動。花了一小時尋找日期格式化的地方後,我才發現自己引進了一個 bug,它躲過了測試並損壞了客戶的發票。
那一刻感覺就像在沒有火把的情況下走進漆黑的洞穴。我知道程式能運作——某種程度上——但它很脆弱,讓人害怕去碰觸,每一次修改都像在拆炸彈。我問自己:我們要如何讓這個怪物變得可維護,而不必從頭重寫?
答案不是華麗的框架或新語言,而是一個簡單、可重複的習慣:抽取出小而專注的函式。
啟示(洞見)
做法很簡單:每當你看到一段程式碼在做一件邏輯上的事情——即使只有三行——就把它抽取成自己的函式,並用一個描述「做了什麼」而不是「怎麼做」的名稱來命名。
為什麼這感覺像在廢棄場找到一把光劍?
- 可讀性大幅提升——函式名稱講述故事;函式本體則說明細節。
- 測試變得容易——你可以針對抽取出的片段進行獨立的單元測試。
- 未來的修改可以局部化——如果日期格式改變,你只需要編輯一個函式,而不是十個分散的地方。
- 重複的程式碼減少——一旦抽取出一個片段,你會發現同樣的邏輯出現在其他地方,進而可以重複使用。
如果你跳過這一步,就會產生「義大利麵條程式碼」,單一職責散落在多處。代價是?更多 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 之前就抓到了。
- 上手速度加快——新進人員可以快速閱讀頂層流程,了解模組的功能。
- 重構變得安全——我們可以將日期格式化器替換成支援時區的版本,只需編輯單一函式,所有呼叫者就會自動更新。
總之,抽取方法將一個令人畏懼的舊有程式碼庫,變成我實際上樂於進行修改的地方。這是一種每天都能帶來回報的超能力。
輪到你了
在你目前的專案中,挑選一個感覺像是「神方法」的函式——那個什麼都做一點的函式。花十分鐘將其中一個邏輯區塊抽取成命名良好的輔助函式。執行你的測試,看看清晰度如何改善,然後重複這個步驟。
你想重構的第一個函式是什麼?在留言區分享你的前後對照片段——我很想知道你自己的「新希望」是如何實現的! 🚀
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.