還記得第一次接手別人寫的程式碼嗎?那種「這是什麼?」的困惑、「為什麼要這樣寫?」的疑問,以及「我該從哪裡開始改?」的無助感。每個開發者都有過這樣的經歷。
經過前九天的學習,我們掌握了 TDD 的基本工具。今天來學習「重構與測試」:在不改變程式外部行為的前提下,改善程式碼內部結構。這就像整理房間一樣,外觀看起來還是同一個房間,但內部變得井然有序、使用起來更方便。
有了測試作為安全網,重構就變得安全而有信心。測試會告訴你:「重構是否保持了原有的行為」。這就像走鋼索時下面有安全網,讓你可以大膽前進。
重構之旅啟程!
├── 第一站:理解重構的本質
├── 第二站:測試驅動的重構實戰
├── 第三站:掌握重構技巧
├── 第四站:重構最佳實踐
└── 終點站:10天學習總結與回顧
今天你將學會:
重構是透過小步驟改善程式碼結構,同時保持程式的外部行為不變。Martin Fowler 在《Refactoring》一書中說:「重構是在不改變軟體可觀察行為的前提下,改善其內部結構」。
很多人常把重構和重寫搞混:
特性 | 重構 | 重寫 |
---|---|---|
改變外部行為 | ❌ 否 | ✅ 可能 |
需要測試保護 | ✅ 必須 | ⚠️ 不一定 |
風險程度 | 低 | 高 |
進行方式 | 小步驟 | 大範圍 |
時間投入 | 持續進行 | 一次性 |
提升可讀性:讓程式碼更容易理解
減少重複:遵循 DRY (Don't Repeat Yourself) 原則
提升維護性:修改和擴展更容易
降低複雜度:簡化複雜的邏輯
三法則(Rule of Three):
重構的黃金法則:在重構之前,你必須有穩固的測試。沒有測試的重構是危險的,就像沒有安全帶就開車一樣。
1. 確認測試都是綠燈 ✅
2. 執行小步驟重構 🔧
3. 執行測試驗證 🧪
4. 如果測試失敗,立即回復 ↩️
5. 重複直到完成 🔄
讓我們透過實際案例來體驗重構的過程:
建立 src/day10/calculator.ts
export class Calculator {
// 需要重構的複雜函式
calculate(a: number, b: number, operation: string): number {
if (operation === 'add') {
return a + b
} else if (operation === 'subtract') {
return a - b
} else if (operation === 'multiply') {
return a * b
} else if (operation === 'divide') {
if (b === 0) {
throw new Error('Cannot divide by zero')
}
return a / b
} else {
throw new Error('Unknown operation')
}
}
}
建立 tests/day10/calculator-before-refactor.test.ts
import { describe, it, expect } from 'vitest'
import { Calculator } from '../../src/day10/calculator.js'
describe('Calculator - Before Refactor', () => {
const calculator = new Calculator()
it('performs addition correctly', () => {
expect(calculator.calculate(5, 3, 'add')).toBe(8)
})
it('performs subtraction correctly', () => {
expect(calculator.calculate(5, 3, 'subtract')).toBe(2)
})
it('performs multiplication correctly', () => {
expect(calculator.calculate(5, 3, 'multiply')).toBe(15)
})
it('performs division correctly', () => {
expect(calculator.calculate(6, 2, 'divide')).toBe(3)
})
it('throws error when dividing by zero', () => {
expect(() => calculator.calculate(5, 0, 'divide'))
.toThrow('Cannot divide by zero')
})
it('throws error for unknown operation', () => {
expect(() => calculator.calculate(5, 3, 'unknown'))
.toThrow('Unknown operation')
})
})
更新 src/day10/calculator.ts
export class Calculator {
calculate(a: number, b: number, operation: string): number {
const operations: Record<string, () => number> = {
add: () => this.add(a, b),
subtract: () => this.subtract(a, b),
multiply: () => this.multiply(a, b),
divide: () => this.divide(a, b)
}
const operationFn = operations[operation]
if (!operationFn) {
throw new Error('Unknown operation')
}
return operationFn()
}
private add(a: number, b: number): number {
return a + b
}
private subtract(a: number, b: number): number {
return a - b
}
private multiply(a: number, b: number): number {
return a * b
}
private divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Cannot divide by zero')
}
return a / b
}
}
建立 tests/day10/calculator-after-refactor.test.ts
import { describe, it, expect } from 'vitest'
import { Calculator } from '../../src/day10/calculator.js'
describe('Calculator - After Refactor', () => {
const calculator = new Calculator()
it('all operations still work correctly', () => {
expect(calculator.calculate(5, 3, 'add')).toBe(8)
expect(calculator.calculate(5, 3, 'subtract')).toBe(2)
expect(calculator.calculate(5, 3, 'multiply')).toBe(15)
expect(calculator.calculate(6, 2, 'divide')).toBe(3)
})
it('error handling preserved', () => {
expect(() => calculator.calculate(5, 0, 'divide'))
.toThrow('Cannot divide by zero')
expect(() => calculator.calculate(5, 3, 'unknown'))
.toThrow('Unknown operation')
})
})
// 重構前:長方法
function processOrder(order: Order): void {
// 驗證和計算邏輯混在一起
if (!order.customer) throw new Error('Customer required')
if (!order.items?.length) throw new Error('Items required')
let total = 0
for (const item of order.items) {
total += item.price * item.quantity
}
if (order.customer.type === 'VIP') {
total *= 0.9
}
order.total = total
order.status = 'processed'
}
// 重構後:提取方法
function processOrder(order: Order): void {
validateOrder(order)
const total = calculateTotal(order)
const discountedTotal = applyDiscount(total, order.customer)
updateOrder(order, discountedTotal)
}
建立 tests/day10/extract-variable.test.ts
import { describe, it, expect } from 'vitest'
describe('Extract Variable Refactoring', () => {
// 重構前:複雜表達式
function calculatePriceBefore(base: number, type: string, amount: number): number {
return base * (type === 'VIP' ? 0.8 : 1.0) * (amount > 1000 ? 0.95 : 1.0)
}
// 重構後:清晰的變數
function calculatePriceAfter(base: number, type: string, amount: number): number {
const customerDiscount = type === 'VIP' ? 0.8 : 1.0
const volumeDiscount = amount > 1000 ? 0.95 : 1.0
return base * customerDiscount * volumeDiscount
}
it('maintains same behavior after refactoring', () => {
// 測試各種情況,確保行為一致
expect(calculatePriceAfter(100, 'VIP', 500))
.toBe(calculatePriceBefore(100, 'VIP', 500))
expect(calculatePriceAfter(100, 'Regular', 1500))
.toBe(calculatePriceBefore(100, 'Regular', 1500))
expect(calculatePriceAfter(100, 'VIP', 1500))
.toBe(calculatePriceBefore(100, 'VIP', 1500))
})
})
建立 tests/day10/remove-duplication.test.ts
import { describe, it, expect } from 'vitest'
describe('Remove Duplication Refactoring', () => {
// 重構前:重複的驗證邏輯
class UserServiceBefore {
updateEmail(userId: string, email: string): void {
if (!userId || userId.trim() === '') {
throw new Error('Invalid user ID')
}
if (!email || !email.includes('@')) {
throw new Error('Invalid email')
}
// 更新邏輯...
}
updatePassword(userId: string, password: string): void {
if (!userId || userId.trim() === '') {
throw new Error('Invalid user ID')
}
if (!password || password.length < 8) {
throw new Error('Invalid password')
}
// 更新邏輯...
}
}
// 重構後:提取共用驗證
class UserServiceAfter {
private validateUserId(userId: string): void {
if (!userId || userId.trim() === '') {
throw new Error('Invalid user ID')
}
}
updateEmail(userId: string, email: string): void {
this.validateUserId(userId)
if (!email || !email.includes('@')) {
throw new Error('Invalid email')
}
// 更新邏輯...
}
updatePassword(userId: string, password: string): void {
this.validateUserId(userId)
if (!password || password.length < 8) {
throw new Error('Invalid password')
}
// 更新邏輯...
}
}
it('validates user ID consistently', () => {
const serviceBefore = new UserServiceBefore()
const serviceAfter = new UserServiceAfter()
// 測試無效的 userId
expect(() => serviceBefore.updateEmail('', 'test@test.com'))
.toThrow('Invalid user ID')
expect(() => serviceAfter.updateEmail('', 'test@test.com'))
.toThrow('Invalid user ID')
// 測試無效的 email
expect(() => serviceBefore.updateEmail('user123', 'invalid'))
.toThrow('Invalid email')
expect(() => serviceAfter.updateEmail('user123', 'invalid'))
.toThrow('Invalid email')
})
})
不要一次性大重構,而是小步驟進行:
🔴 錯誤示範:
「我要花三天時間重構整個模組」
🟢 正確做法:
「我每次只重構一個方法,確保測試通過後再繼續」
執行步驟:
確保重構前後行為一致:
# 重構工作流程
$ npm test # ✅ 確認測試綠燈
$ # 執行重構...
$ npm test # 🧪 驗證行為未變
$ git commit -m "refactor: extract method for validation"
重構時保持函數介面不變,避免破壞現有程式碼:
// ❌ 破壞性變更
getTotal() // 原本
calculateTotal() // 直接改名
// ✅ 向後相容
getTotal() {
// 標記為過時,但保持可用
console.warn('getTotal() is deprecated, use calculateTotal() instead')
return this.calculateTotal()
}
calculateTotal() {
// 新的實作
}
適合重構的時機:
不適合重構的時機:
恭喜你!完成了 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) | ✅ Vitest 測試框架✅ 基本斷言✅ 紅綠重構 | 能寫簡單的單元測試 |
基礎 (Day 4-6) | ✅ 測試組織✅ 生命週期✅ 資料驅動測試 | 能組織大型測試套件 |
深化 (Day 7-9) | ✅ Mock/Stub✅ 例外測試✅ 覆蓋率分析 | 能測試複雜場景 |
整合 (Day 10) | ✅ 安全重構✅ 程式碼品質✅ 持續改進 | 能維護高品質程式碼 |
測試優先思維:先寫測試,後寫程式碼
小步驟開發:每次只改一點點,保持綠燈
重構信心:有測試保護,重構不再恐懼
品質意識:測試不只是找 bug,更是設計工具
第一階段的基礎訓練圓滿完成!🎉
透過今天的學習,我們掌握了:
重構讓我們能夠:
恭喜你完成了 TDD 基礎訓練的前十天!你已經掌握了 TDD 的核心概念和實踐技巧。
今天我們學會了測試驅動的重構,這是 TDD 循環中「重構」步驟的深入實踐。有了測試作為安全網,我們可以大膽地改善程式碼結構,讓系統變得更好。
重構不是一次性的大工程,而是持續的小改進。就像園丁修剪花園,每天做一點,最終會有一個美麗的花園。
「寫程式」是為了讓機器理解
「重構」是為了讓人類理解
「測試」是為了讓改變安全
第一階段的學習到此圓滿結束!記住 TDD 的精髓:紅 → 綠 → 重構。測試不只是為了找 bug,更是設計工具和重構的安全網!
恭喜你完成了 TDD 基礎學習的前十天!繼續努力,你將能掌握更多 TDD 技巧!🚀