474 lines
7.6 KiB
Markdown
474 lines
7.6 KiB
Markdown
---
|
||
description: 重構消除重複(Refactor 階段)。在綠燈的保護下,改善程式碼品質。
|
||
---
|
||
|
||
# TDD Refactor - 重構消除重複(Refactor)
|
||
|
||
在測試通過後,消除重複、改善設計。測試是你的安全網。
|
||
|
||
**【功能名】**:{{feature_name}}
|
||
|
||
## Kent Beck 的 Refactor 理念
|
||
|
||
> "重構的核心是消除重複。"
|
||
> "測試通過後,就是重構的時機。"
|
||
|
||
### 什麼是重複?
|
||
|
||
Kent Beck 定義的重複包括:
|
||
|
||
#### 1. 明顯的程式碼重複
|
||
|
||
```javascript
|
||
// ❌ 重複
|
||
function processOrderA(order) {
|
||
if (!order) throw new Error('Invalid order');
|
||
return order.total * 1.1;
|
||
}
|
||
|
||
function processOrderB(order) {
|
||
if (!order) throw new Error('Invalid order');
|
||
return order.total * 0.9;
|
||
}
|
||
|
||
// ✅ 消除重複
|
||
function validateOrder(order) {
|
||
if (!order) throw new Error('Invalid order');
|
||
}
|
||
|
||
function processOrderA(order) {
|
||
validateOrder(order);
|
||
return order.total * 1.1;
|
||
}
|
||
```
|
||
|
||
#### 2. 測試和實作之間的重複
|
||
|
||
```javascript
|
||
// 測試
|
||
test('5 * 2 = 10', () => {
|
||
expect(multiply(5, 2)).toBe(10);
|
||
});
|
||
|
||
// ❌ 實作中的重複(硬編碼)
|
||
function multiply(a, b) {
|
||
return 10; // 和測試中的 10 重複!
|
||
}
|
||
|
||
// ✅ 消除重複(真正的實作)
|
||
function multiply(a, b) {
|
||
return a * b;
|
||
}
|
||
```
|
||
|
||
#### 3. 概念上的重複
|
||
|
||
```javascript
|
||
// ❌ 概念重複(魔術數字)
|
||
function applyDiscount(price) {
|
||
return price * 0.9;
|
||
}
|
||
|
||
function calculateTax(price) {
|
||
return price * 0.1;
|
||
}
|
||
|
||
// ✅ 消除重複(提取常數)
|
||
const DISCOUNT_RATE = 0.9;
|
||
const TAX_RATE = 0.1;
|
||
|
||
function applyDiscount(price) {
|
||
return price * DISCOUNT_RATE;
|
||
}
|
||
|
||
function calculateTax(price) {
|
||
return price * TAX_RATE;
|
||
}
|
||
```
|
||
|
||
## 重構步驟
|
||
|
||
### 1. 確認測試通過
|
||
|
||
```bash
|
||
npm test
|
||
# 必須是綠燈!🟢
|
||
```
|
||
|
||
**如果是紅燈**:
|
||
- 不要重構
|
||
- 先讓測試通過
|
||
|
||
### 2. 識別重複或異味
|
||
|
||
問自己:
|
||
- 有重複的程式碼嗎?
|
||
- 有硬編碼的值嗎?
|
||
- 有不清楚的命名嗎?
|
||
- 有太長的函式嗎?
|
||
- 有不必要的複雜度嗎?
|
||
|
||
### 3. 小步重構
|
||
|
||
**一次只改一個地方**
|
||
|
||
```
|
||
改一小步 → 執行測試 → 通過
|
||
↓
|
||
改下一步 → 執行測試 → 通過
|
||
↓
|
||
...
|
||
```
|
||
|
||
### 4. 每次都執行測試
|
||
|
||
**頻率**:每改一個地方就測試一次
|
||
|
||
```bash
|
||
# 改善命名
|
||
npm test
|
||
|
||
# 提取函式
|
||
npm test
|
||
|
||
# 提取常數
|
||
npm test
|
||
```
|
||
|
||
### 5. 如果測試失敗,立即回退
|
||
|
||
```bash
|
||
# 紅燈!
|
||
git checkout . # 回退
|
||
# 或手動 Undo
|
||
```
|
||
|
||
## 常見重構技巧
|
||
|
||
### 技巧 1:消除硬編碼
|
||
|
||
**Before**
|
||
```javascript
|
||
class Dollar {
|
||
times(multiplier) {
|
||
return new Dollar(10); // 硬編碼
|
||
}
|
||
}
|
||
```
|
||
|
||
**After**
|
||
```javascript
|
||
class Dollar {
|
||
times(multiplier) {
|
||
return new Dollar(this.amount * multiplier);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 技巧 2:提取函式
|
||
|
||
**Before**
|
||
```javascript
|
||
function processOrder(order) {
|
||
// 驗證
|
||
if (!order.items || order.items.length === 0) {
|
||
throw new Error('No items');
|
||
}
|
||
|
||
// 計算
|
||
let total = 0;
|
||
for (const item of order.items) {
|
||
total += item.price * item.quantity;
|
||
}
|
||
|
||
return total;
|
||
}
|
||
```
|
||
|
||
**After**
|
||
```javascript
|
||
function processOrder(order) {
|
||
validateOrder(order);
|
||
return calculateTotal(order.items);
|
||
}
|
||
|
||
function validateOrder(order) {
|
||
if (!order.items || order.items.length === 0) {
|
||
throw new Error('No items');
|
||
}
|
||
}
|
||
|
||
function calculateTotal(items) {
|
||
let total = 0;
|
||
for (const item of items) {
|
||
total += item.price * item.quantity;
|
||
}
|
||
return total;
|
||
}
|
||
```
|
||
|
||
### 技巧 3:提取常數
|
||
|
||
**Before**
|
||
```javascript
|
||
function calculateShipping(weight) {
|
||
if (weight > 1000) return 100;
|
||
return 50;
|
||
}
|
||
```
|
||
|
||
**After**
|
||
```javascript
|
||
const HEAVY_WEIGHT_THRESHOLD = 1000;
|
||
const SHIPPING_HEAVY = 100;
|
||
const SHIPPING_STANDARD = 50;
|
||
|
||
function calculateShipping(weight) {
|
||
if (weight > HEAVY_WEIGHT_THRESHOLD) {
|
||
return SHIPPING_HEAVY;
|
||
}
|
||
return SHIPPING_STANDARD;
|
||
}
|
||
```
|
||
|
||
### 技巧 4:改善命名
|
||
|
||
**Before**
|
||
```javascript
|
||
function calc(x) {
|
||
return x * 0.9;
|
||
}
|
||
```
|
||
|
||
**After**
|
||
```javascript
|
||
function applyDiscount(price) {
|
||
const DISCOUNT_RATE = 0.9;
|
||
return price * DISCOUNT_RATE;
|
||
}
|
||
```
|
||
|
||
### 技巧 5:簡化條件
|
||
|
||
**Before**
|
||
```javascript
|
||
function canPurchase(user, product) {
|
||
if (user.age >= 18) {
|
||
if (user.balance >= product.price) {
|
||
if (product.stock > 0) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
```
|
||
|
||
**After**
|
||
```javascript
|
||
function canPurchase(user, product) {
|
||
return user.age >= 18
|
||
&& user.balance >= product.price
|
||
&& product.stock > 0;
|
||
}
|
||
```
|
||
|
||
## 何時停止重構?
|
||
|
||
Kent Beck 的判斷標準:
|
||
|
||
### 停止的信號
|
||
|
||
✅ **可以停止了**:
|
||
- 沒有明顯的重複
|
||
- 程式碼清楚易懂
|
||
- 命名恰當
|
||
- 函式簡短
|
||
|
||
✅ **也可以停止**:
|
||
- 想不到明顯的改善
|
||
- 繼續重構的收益不大
|
||
- 想寫下一個測試了
|
||
|
||
### 不要過度重構
|
||
|
||
```javascript
|
||
// ✅ 夠好了
|
||
function calculateTotal(items) {
|
||
let total = 0;
|
||
for (const item of items) {
|
||
total += item.price * item.quantity;
|
||
}
|
||
return total;
|
||
}
|
||
|
||
// ❌ 過度了(除非真的需要)
|
||
class TotalCalculator {
|
||
constructor(strategy) {
|
||
this.strategy = strategy;
|
||
}
|
||
|
||
calculate(items) {
|
||
return this.strategy.compute(items);
|
||
}
|
||
}
|
||
|
||
class StandardCalculationStrategy {
|
||
compute(items) {
|
||
return items.reduce((sum, item) =>
|
||
sum + this.calculateItemTotal(item), 0);
|
||
}
|
||
|
||
calculateItemTotal(item) {
|
||
return new Money(item.price)
|
||
.multiply(item.quantity)
|
||
.getAmount();
|
||
}
|
||
}
|
||
// ... 太複雜了!
|
||
```
|
||
|
||
## 重構的節奏
|
||
|
||
Kent Beck 建議的節奏:
|
||
|
||
```
|
||
快速寫測試(幾分鐘)
|
||
↓
|
||
快速讓測試通過(幾分鐘)
|
||
↓
|
||
快速重構(幾分鐘)← 現在在這裡
|
||
↓
|
||
回到寫測試
|
||
```
|
||
|
||
**每個階段都要快**
|
||
- 不要花太久在重構
|
||
- 幾分鐘就好
|
||
- 保持節奏
|
||
|
||
## 更新 journey.md
|
||
|
||
```markdown
|
||
#### 🔵 Refactor - 重構
|
||
|
||
重構內容:
|
||
1. {改善項目 1}
|
||
- 重構前:{簡述}
|
||
- 重構後:{簡述}
|
||
- 原因:{為什麼}
|
||
|
||
2. {改善項目 2}
|
||
...
|
||
|
||
測試狀態:✅ 持續通過
|
||
|
||
#### 📝 下一步
|
||
執行 /kb-review 回顧下一步
|
||
```
|
||
|
||
## 範例:Money 的重構
|
||
|
||
### 重構前
|
||
|
||
```javascript
|
||
class Dollar {
|
||
constructor(amount) {
|
||
this.amount = amount;
|
||
}
|
||
|
||
times(multiplier) {
|
||
return new Dollar(this.amount * multiplier);
|
||
}
|
||
}
|
||
|
||
class Franc {
|
||
constructor(amount) {
|
||
this.amount = amount;
|
||
}
|
||
|
||
times(multiplier) {
|
||
return new Franc(this.amount * multiplier);
|
||
}
|
||
}
|
||
```
|
||
|
||
**發現重複**:Dollar 和 Franc 幾乎一樣!
|
||
|
||
### 重構步驟
|
||
|
||
**Step 1:提取父類別**
|
||
```javascript
|
||
class Money {
|
||
constructor(amount) {
|
||
this.amount = amount;
|
||
}
|
||
}
|
||
|
||
class Dollar extends Money {
|
||
times(multiplier) {
|
||
return new Dollar(this.amount * multiplier);
|
||
}
|
||
}
|
||
|
||
class Franc extends Money {
|
||
times(multiplier) {
|
||
return new Franc(this.amount * multiplier);
|
||
}
|
||
}
|
||
```
|
||
|
||
測試:✅ 通過
|
||
|
||
**Step 2:將 times 提升到父類別**
|
||
```javascript
|
||
class Money {
|
||
constructor(amount) {
|
||
this.amount = amount;
|
||
}
|
||
|
||
times(multiplier) {
|
||
return new Money(this.amount * multiplier);
|
||
}
|
||
}
|
||
|
||
class Dollar extends Money {}
|
||
class Franc extends Money {}
|
||
```
|
||
|
||
測試:✅ 通過
|
||
|
||
**消除了重複!**
|
||
|
||
## 重構的勇氣來自哪裡?
|
||
|
||
Kent Beck:**測試給你重構的勇氣**
|
||
|
||
```
|
||
沒有測試:
|
||
😰 不敢改程式碼
|
||
😰 怕改壞東西
|
||
😰 程式碼越來越爛
|
||
|
||
有測試:
|
||
😎 隨時可以重構
|
||
😎 測試會告訴你有沒有改壞
|
||
😎 程式碼越來越好
|
||
```
|
||
|
||
## 下一步
|
||
|
||
重構完成?執行:
|
||
```
|
||
/kb-review
|
||
```
|
||
|
||
回顧這一輪,計劃下一步!
|
||
|
||
## 記住 Kent Beck 的話
|
||
|
||
> "重構的時機是在測試通過之後。"
|
||
> "重複是設計的敵人。"
|
||
> "小步重構,頻繁測試,永遠保持綠燈。"
|
||
> "測試讓你有勇氣重構,重構讓程式碼保持清潔。"
|