做到 Day 19 之後,目前專案功能都已經可以正常使用:
Task CRUD
MySQL 儲存
狀態更新
狀態篩選
分類篩選
日期格式錯誤處理
功能雖然正常,但 app.py 裡開始出現一些重複程式。
尤其是:
new_task()
edit_task()
裡面,都有相同的分類查詢和日期解析。
所以 Day 20 不加新功能。
今天的目標是:
在不改變原本行為的前提下,把重複程式整理成比較好維護的寫法。
Refactor 就是:
整理程式內部結構
但不改變外部功能
也就是說,使用者看網站時,不應該感覺有任何不同。
例如 Day 19 已經確認:
錯誤日期 → 400
合法日期 → 正常
空白日期 → NULL
Day 20 重構之後,這些結果都必須保持一樣。
所以可以把 Refactor 理解成:
程式裡面變乾淨
網站外面不變
今天只做小範圍整理。
預計只修改:
app.py
不做:
新增功能
分類 CRUD
搜尋
分頁
services/
routes/
Application Factory
資料庫調整
Claude API
也不會因為看到可以整理的地方,就一次把整個專案拆掉。
我先切到 Plan Mode,讓 Claude Code 檢查目前的 app.py。
Prompt:
目前專案功能都正常。
Day 20 不新增任何功能,
今天只做小範圍 Refactor。
請檢查目前 app.py,
特別看 new_task() 和 edit_task()。
找出明顯重複的程式碼,
並判斷哪些值得抽成小型 helper function。
限制:
1. 不改變任何現有功能
2. 不改 URL
3. 不改 HTTP Method
4. 不改資料庫結構
5. 不建立 services/
6. 不建立 routes/
7. 不新增其他架構
8. 優先維持 app.py 單一檔案
9. 不順便新增功能或驗證規則
10. 修改後原本測試結果必須保持一致
請先分析:
- 哪些重複值得整理
- 建議抽成什麼 helper
- helper 的參數和回傳值
- 哪些地方不要動
- 修改後怎麼驗證
先不要直接修改程式。
Claude Code 找到兩段幾乎一樣的程式。
第一段是分類查詢:
category = Category.query.filter_by(
name=request.form.get("category")
).first()
接著再取得:
category.id if category else None
這一段同時出現在:
new_task()
edit_task()
Day 19 修完之後,新增和編輯都有:
due_date_text = request.form.get("due_date", "")
if due_date_text:
try:
due_date = datetime.strptime(
due_date_text,
"%Y-%m-%d"
).date()
except ValueError:
abort(400)
else:
due_date = None
這兩段程式完全一樣。
所以 Claude 建議抽成兩個 helper:
resolve_category_id()
parse_due_date()
Claude 建議:
def resolve_category_id(category_name):
category = Category.query.filter_by(
name=category_name
).first()
return category.id if category else None
這個 helper 的工作很單純:
分類名稱
↓
查 Category
↓
回傳 category_id
例如:
專案
→ 找到 Category
→ 回傳 id
如果查不到:
不存在的分類
→ None
這跟原本新增和編輯的行為完全一樣。
日期則整理成:
def parse_due_date(due_date_text):
if not due_date_text:
return None
try:
return datetime.strptime(
due_date_text,
"%Y-%m-%d"
).date()
except ValueError:
abort(400)
這個 helper 保留 Day 19 的所有規則。
2026-12-25
會回傳日期物件。
""
會回:
None
not-a-date
會:
abort(400)
跟重構前完全一樣。
Claude Code 還特別保留了一些地方沒有動。
例如 index() 裡也有分類查詢:
category = Category.query.filter_by(
name=category_filter
).first()
但它沒有跟:
resolve_category_id()
合併。
因為兩邊雖然都在查分類,行為其實不一樣。
新增和編輯:
查不到分類
→ category_id = None
首頁篩選:
查不到分類
→ category_id = -1
→ 查詢結果為空
所以不能只因為「程式長得像」,就硬抽成同一個函式。
這也是今天很重要的一點:
重構要看邏輯是不是一樣,不只是看程式碼像不像。
Claude 也沒有把:
new_task()
edit_task()
整個合併。
因為:
new_task
→ 建立新的 Task
edit_task
→ 修改既有 Task
兩者責任本來就不同。
所以這次只抽真正重複的:
分類解析
日期解析
沒有過度整理。
我確認 Plan 沒問題後,才讓 Claude Code 執行:
這版 Refactor 規劃可以。
請照規劃執行,只修改 app.py:
1. 新增 resolve_category_id()
2. 新增 parse_due_date()
3. new_task() 和 edit_task() 改成呼叫這兩個 helper
不要修改 index() 的分類篩選邏輯。
不要合併 new_task() 和 edit_task()。
不要新增 validation、route、service 或其他功能。
完成後請列出:
- 實際修改內容
- 修改後 new_task() / edit_task() 關鍵程式碼
- Day 19 六組回歸測試結果
最後真的只修改:
app.py
並在:
db.init_app(app)
後面加入兩個 helper。
def resolve_category_id(category_name):
category = Category.query.filter_by(
name=category_name
).first()
return category.id if category else None
以及:
def parse_due_date(due_date_text):
if not due_date_text:
return None
try:
return datetime.strptime(
due_date_text,
"%Y-%m-%d"
).date()
except ValueError:
abort(400)
原本 new_task() 裡要自己處理分類和日期。
重構後變成:
category_id = resolve_category_id(
request.form.get("category")
)
due_date = parse_due_date(
request.form.get("due_date", "")
)
建立 Task:
task = Task(
title=request.form.get("title", ""),
description=request.form.get("description", ""),
status=request.form.get("status", "todo"),
category_id=category_id,
due_date=due_date,
)
Route 裡就不用再塞日期格式處理細節。
編輯任務則變成:
task.title = request.form.get("title", "")
task.description = request.form.get("description", "")
task.status = request.form.get("status", "todo")
task.category_id = resolve_category_id(
request.form.get("category")
)
task.due_date = parse_due_date(
request.form.get("due_date", "")
)
這樣:
new_task()
edit_task()
都使用同一套分類與日期規則。
Refactor 最重要的是:
功能不能變
所以我直接沿用 Day 19 的六組測試重新驗證。
輸入:
due_date = not-a-date
結果:
400
沒有 Traceback
跟 Day 19 完全一樣。
輸入:
due_date = 2026-12-25
結果:
302
任務正確建立
正常日期沒有被重構改壞。
輸入:
due_date =
結果:
302
due_date = NULL
原本允許空日期的行為也沒有改變。
對既有任務送:
due_date = bad-date
結果:
400
沒有 Traceback
而且:
原本 Task 資料完全沒有被修改
輸入:
due_date = 2026-09-26
結果:
302
更新成功
輸入:
due_date =
結果:
302
due_date 更新為 NULL
| 測試 | 輸入 | 結果 |
|---|---|---|
| new_task 不合法日期 | not-a-date |
400,無 Traceback |
| new_task 合法日期 | 2026-12-25 |
302,建立成功 |
| new_task 空日期 | 空白 | 302,NULL |
| edit_task 不合法日期 | bad-date |
400,資料未改動 |
| edit_task 合法日期 | 2026-09-26 |
302,更新成功 |
| edit_task 空日期 | 空白 | 302,更新為 NULL |
六組結果和 Day 19 完全一致。
測試過程產生的暫時資料和修改,也都已經清除或還原。
重構前:
new_task()
→ 自己查分類
→ 自己解析日期
edit_task()
→ 再查一次分類
→ 再解析一次日期
重構後:
new_task ─────┐
├→ resolve_category_id()
└→ parse_due_date()
edit_task ────┐
├→ resolve_category_id()
└→ parse_due_date()
功能沒有變,但重複程式少了。
假設之後日期格式的處理規則要改。
以前可能要記得修改:
new_task()
edit_task()
兩個地方。
現在只需要改:
parse_due_date()
就可以同時影響兩邊。
分類也是一樣。
這可以降低:
改了新增
忘了修改編輯
這種同步問題。
這次我也開始理解,看到重複程式不能馬上就說:
抽函式
還要先問:
它們的責任真的一樣嗎?
行為真的完全一樣嗎?
抽完會比較清楚嗎?
像 index() 的分類查詢雖然看起來很像,但語意不同,所以保留原本寫法反而比較安全。
Day 19 是:
原本有 Bug
↓
修改程式
↓
改變錯誤行為
例如:
500 → 400
Day 20 則是:
原本就正常
↓
整理內部程式
↓
結果完全不變
所以這次最重要的驗證不是:
有沒有新功能
而是:
六組測試結果有沒有跟重構前完全一致
今天沒有新增任何功能,也沒有把專案拆成複雜架構。
只是把目前已經出現的重複程式:
分類查詢
日期解析
整理成:
resolve_category_id()
parse_due_date()
最後六組回歸測試全部跟 Day 19 一樣。
這讓我對 Refactor 有一個比較具體的理解:
重構不是把程式改得越複雜越專業,而是在不改功能的情況下,讓重複減少、責任更清楚、之後更容易維護。
Day 20 到這裡完成。
下一步要開始處理一個對 Claude Code 很重要的檔案:
CLAUDE.md
讓 Claude Code 進入專案之後,不需要每次重新猜:
這個專案在做什麼
目前做到哪裡
有哪些規則不能亂動