還記得第一次接手別人寫的程式碼嗎?那種「這是什麼?」的困惑、「為什麼要這樣寫?」的疑問,以及「我該從哪裡開始改?」的無助感。每個開發者都有過這樣的經歷。
經過前九天的學習,我們掌握了 TDD 的基本工具。今天來學習「重構與測試」:在不改變程式外部行為的前提下,改善程式碼內部結構。這就像整理房間一樣,外觀看起來還是同一個房間,但內部變得井然有序、使用起來更方便。
有了測試作為安全網,重構就變得安全而有信心。測試會告訴你:「重構是否保持了原有的行為」。這就像走鋼索時下面有安全網,讓你可以大膽前進。
重構之旅啟程!
├── 第一站:理解重構的本質
├── 第二站:測試驅動的重構實戰
├── 第三站:掌握重構技巧
├── 第四站:重構最佳實踐
└── 終點站:10天學習總結與回顧
今天你將學會:
重構是透過小步驟改善程式碼結構,同時保持程式的外部行為不變。Martin Fowler 在《Refactoring》一書中說:「重構是在不改變軟體可觀察行為的前提下,改善其內部結構」。
很多人常把重構和重寫搞混:
特性 | 重構 | 重寫 |
---|---|---|
改變外部行為 | ❌ 否 | ✅ 可能 |
需要測試保護 | ✅ 必須 | ⚠️ 不一定 |
風險程度 | 低 | 高 |
進行方式 | 小步驟 | 大範圍 |
時間投入 | 持續進行 | 一次性 |
提升可讀性:讓程式碼更容易理解
減少重複:遵循 DRY (Don't Repeat Yourself) 原則
提升維護性:修改和擴展更容易
降低複雜度:簡化複雜的邏輯
三法則(Rule of Three):
重構的黃金法則:在重構之前,你必須有穩固的測試。沒有測試的重構是危險的,就像沒有安全帶就開車一樣。
1. 確認測試都是綠燈 ✅
2. 執行小步驟重構 🔧
3. 執行測試驗證 🧪
4. 如果測試失敗,立即回復 ↩️
5. 重複直到完成 🔄
讓我們透過實際案例來體驗重構的過程:
建立 src/day10/calculator.py
# 重構前:複雜的計算器
class Calculator:
def calculate(self, a: float, b: float, operation: str) -> float:
if operation == 'add':
return a + b
elif operation == 'subtract':
return a - b
elif operation == 'multiply':
return a * b
elif operation == 'divide':
if b == 0:
raise ValueError('Cannot divide by zero')
return a / b
else:
raise ValueError('Unknown operation')
建立 tests/day10/test_calculator.py
import pytest
from src.day10.calculator import Calculator
class TestCalculator:
def setup_method(self):
self.calc = Calculator()
def test_addition(self):
assert self.calc.calculate(5.0, 3.0, 'add') == 8.0
def test_subtraction(self):
assert self.calc.calculate(5.0, 3.0, 'subtract') == 2.0
def test_multiplication(self):
assert self.calc.calculate(5.0, 3.0, 'multiply') == 15.0
def test_division(self):
assert self.calc.calculate(6.0, 2.0, 'divide') == 3.0
def test_division_by_zero_raises_error(self):
with pytest.raises(ValueError, match='Cannot divide by zero'):
self.calc.calculate(5.0, 0.0, 'divide')
def test_unknown_operation_raises_error(self):
with pytest.raises(ValueError, match='Unknown operation'):
self.calc.calculate(5.0, 3.0, 'unknown')
現在我們有了測試安全網,可以安心進行重構:
更新 src/day10/calculator.py
# 重構後:使用字典策略模式
class Calculator:
def calculate(self, a: float, b: float, operation: str) -> float:
operations = {
'add': self._add,
'subtract': self._subtract,
'multiply': self._multiply,
'divide': self._divide
}
if operation not in operations:
raise ValueError('Unknown operation')
return operations[operation](a, b)
def _add(self, a: float, b: float) -> float:
return a + b
def _subtract(self, a: float, b: float) -> float:
return a - b
def _multiply(self, a: float, b: float) -> float:
return a * b
def _divide(self, a: float, b: float) -> float:
if b == 0:
raise ValueError('Cannot divide by zero')
return a / b
當一個方法太長或做太多事情時,可以提取出語義清晰的小方法。
當發現重複的程式碼時,應該提取共同邏輯,遵循 DRY 原則。
複雜的條件判斷應該被簡化或提取成有意義的方法。
將前面學到的技巧應用到訂單處理服務中:
建立 src/day10/order_service.py
# 重構後:職責分離,邏輯清晰
class OrderService:
def process_order(self, order_data: dict) -> dict:
self._validate_order(order_data)
subtotal = self._calculate_subtotal(order_data['items'])
discount = self._calculate_discount(subtotal, order_data.get('customer_type'))
return {
'order_id': f"ORDER_{order_data['customer_id']}",
'subtotal': subtotal,
'discount': discount,
'total': subtotal - discount
}
def _validate_order(self, order_data: dict) -> None:
if not order_data.get('customer_id'):
raise ValueError('Customer ID is required')
if not order_data.get('items'):
raise ValueError('Order must have at least one item')
def _calculate_subtotal(self, items: list) -> float:
return sum(item['price'] * item['quantity'] for item in items)
def _calculate_discount(self, subtotal: float, customer_type: str) -> float:
if customer_type == 'VIP':
return subtotal * 0.1
elif subtotal > 1000:
return subtotal * 0.05
return 0.0
每次只重構一小部分,執行測試確保功能正常,提交版本控制,逐步改善程式碼結構。
$ pytest tests/day10/ # ✅ 確認測試綠燈
$ # 執行重構...
$ pytest tests/day10/ # 🧪 驗證行為未變
$ git commit -m "refactor: extract method for validation"
適合重構:新增功能前、修復 bug 時、Code Review 時
不適合重構:接近截止日期、沒有測試保護時
建立 tests/day10/test_order_service.py
import pytest
from src.day10.order_service import OrderService
class TestOrderService:
def setup_method(self):
self.service = OrderService()
def test_calculate_regular_order(self):
order = {
'customer_id': 'C123',
'customer_type': 'Regular',
'items': [{'price': 100, 'quantity': 2}]
}
result = self.service.process_order(order)
assert result['total'] == 200
def test_calculate_vip_discount(self):
order = {
'customer_id': 'V456',
'customer_type': 'VIP',
'items': [{'price': 500, 'quantity': 2}]
}
result = self.service.process_order(order)
assert result['discount'] == 100 # 10% VIP discount
恭喜你!完成了 TDD 第一階段的學習。讓我們回顧這 10 天的精彩旅程:
Day 01-03:奠定基礎
├── Day 01:環境設定與第一個測試
├── Day 02:理解斷言的藝術
└── Day 03:TDD 紅綠重構循環
Day 04-06:深化理解
├── Day 04:測試結構與組織
├── Day 05:測試生命週期
└── Day 06:參數化測試的威力
Day 07-09:測試技巧
├── Day 07:測試替身基礎
├── Day 08:例外處理測試
└── Day 09:測試覆蓋率分析
Day 10:整合提升
└── 重構與測試的完美搭配
階段 | 核心技能 | 實戰能力 |
---|---|---|
入門 (Day 1-3) | ✅ pytest 測試框架✅ 基本斷言✅ 紅綠重構 | 能寫簡單的單元測試 |
基礎 (Day 4-6) | ✅ 測試組織✅ 生命週期✅ 資料驅動測試 | 能組織大型測試套件 |
深化 (Day 7-9) | ✅ Mock/Stub✅ 例外測試✅ 覆蓋率分析 | 能測試複雜場景 |
整合 (Day 10) | ✅ 安全重構✅ 程式碼品質✅ 持續改進 | 能維護高品質程式碼 |
✅ 能夠設定 Python pytest 測試環境
✅ 熟練使用各種斷言方法
✅ 理解並實踐 TDD 紅綠重構循環
✅ 能夠組織和管理測試程式碼
✅ 掌握測試生命週期鉤子
✅ 會使用參數化測試減少重複
✅ 能夠創建和使用測試替身
✅ 知道如何測試例外情況
✅ 理解測試覆蓋率的意義
✅ 能在測試保護下安全重構
第一階段的基礎訓練圓滿完成!🎉
今天我們學會了測試驅動的重構,這是 TDD 循環中「重構」步驟的深入實踐。有了測試作為安全網,我們可以大膽地改善程式碼結構,讓系統變得更好。
重構不是一次性的大工程,而是持續的小改進。就像園丁修剪花園,每天做一點,最終會有一個美麗的花園。
「寫程式」是為了讓機器理解
「重構」是為了讓人類理解
「測試」是為了讓改變安全
第一階段的學習到此圓滿結束!記住 TDD 的精髓:紅 → 綠 → 重構。測試不只是為了找 bug,更是設計工具和重構的安全網!
恭喜你完成了 TDD 基礎學習的前十天!繼續努力,你將能掌握更多 TDD 技巧!🚀