Files
2025-11-29 18:03:43 +08:00

7.6 KiB
Raw Permalink Blame History

description
description
重構消除重複Refactor 階段)。在綠燈的保護下,改善程式碼品質。

TDD Refactor - 重構消除重複Refactor

在測試通過後,消除重複、改善設計。測試是你的安全網。

【功能名】{{feature_name}}

Kent Beck 的 Refactor 理念

"重構的核心是消除重複。" "測試通過後,就是重構的時機。"

什麼是重複?

Kent Beck 定義的重複包括:

1. 明顯的程式碼重複

// ❌ 重複
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. 測試和實作之間的重複

// 測試
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. 概念上的重複

// ❌ 概念重複(魔術數字)
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. 確認測試通過

npm test
# 必須是綠燈!🟢

如果是紅燈

  • 不要重構
  • 先讓測試通過

2. 識別重複或異味

問自己:

  • 有重複的程式碼嗎?
  • 有硬編碼的值嗎?
  • 有不清楚的命名嗎?
  • 有太長的函式嗎?
  • 有不必要的複雜度嗎?

3. 小步重構

一次只改一個地方

改一小步 → 執行測試 → 通過
    ↓
改下一步 → 執行測試 → 通過
    ↓
...

4. 每次都執行測試

頻率:每改一個地方就測試一次

# 改善命名
npm test

# 提取函式
npm test

# 提取常數
npm test

5. 如果測試失敗,立即回退

# 紅燈!
git checkout .  # 回退
# 或手動 Undo

常見重構技巧

技巧 1消除硬編碼

Before

class Dollar {
  times(multiplier) {
    return new Dollar(10); // 硬編碼
  }
}

After

class Dollar {
  times(multiplier) {
    return new Dollar(this.amount * multiplier);
  }
}

技巧 2提取函式

Before

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

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

function calculateShipping(weight) {
  if (weight > 1000) return 100;
  return 50;
}

After

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

function calc(x) {
  return x * 0.9;
}

After

function applyDiscount(price) {
  const DISCOUNT_RATE = 0.9;
  return price * DISCOUNT_RATE;
}

技巧 5簡化條件

Before

function canPurchase(user, product) {
  if (user.age >= 18) {
    if (user.balance >= product.price) {
      if (product.stock > 0) {
        return true;
      }
    }
  }
  return false;
}

After

function canPurchase(user, product) {
  return user.age >= 18
    && user.balance >= product.price
    && product.stock > 0;
}

何時停止重構?

Kent Beck 的判斷標準:

停止的信號

可以停止了

  • 沒有明顯的重複
  • 程式碼清楚易懂
  • 命名恰當
  • 函式簡短

也可以停止

  • 想不到明顯的改善
  • 繼續重構的收益不大
  • 想寫下一個測試了

不要過度重構

// ✅ 夠好了
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

#### 🔵 Refactor - 重構

重構內容:
1. {改善項目 1}
   - 重構前:{簡述}
   - 重構後:{簡述}
   - 原因:{為什麼}

2. {改善項目 2}
   ...

測試狀態:✅ 持續通過

#### 📝 下一步
執行 /kb-review 回顧下一步

範例Money 的重構

重構前

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提取父類別

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 提升到父類別

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 的話

"重構的時機是在測試通過之後。" "重複是設計的敵人。" "小步重構,頻繁測試,永遠保持綠燈。" "測試讓你有勇氣重構,重構讓程式碼保持清潔。"