はじめに

「動いているコードには触るな」という言葉を聞いたことがあるでしょうか。確かに、テストのないコードを修正するのは恐怖を伴います。どこかが壊れるかもしれない、本番障害につながるかもしれない。その不安から、多くの開発者は問題のあるコードをそのまま放置し、技術的負債が蓄積していきます。

しかし、TDD(テスト駆動開発)を実践していれば、この恐怖から解放されます。テストという安全網があれば、自信を持ってコードを改善できるのです。Martin Fowler氏は著書『Refactoring』で「リファクタリングを安全に行うためには、テストが不可欠である」と述べています。TDDとリファクタリングは、高品質なコードを生み出すための両輪なのです。

本記事では、TDDのRed-Green-Refactorサイクルにおけるリファクタリングの役割から、テストに守られた改善の具体的な進め方、コードの臭い(Code Smell)の発見と対処、そしてリファクタリングカタログの活用方法まで、実践的なテクニックを網羅的に解説します。

前提条件と実行環境

本記事のコード例は以下の環境で動作確認しています。

項目 バージョン
Node.js 20.x LTS
Jest 29.x
Java 21
JUnit 5 5.10.x

コードはJavaScript(Jest)とJava(JUnit 5)の両方で示しますが、考え方自体は言語に依存しません。

Red-Green-Refactorサイクルとリファクタリングの関係

リファクタリングはサイクルの一部

TDDのRed-Green-Refactorサイクルにおいて、リファクタリングは第三のフェーズとして明確に位置づけられています。

flowchart LR
    R["Red<br/>失敗するテストを書く"]
    G["Green<br/>テストを通す最小限のコード"]
    RF["Refactor<br/>コードを改善する"]
    
    R --> G
    G --> RF
    RF --> R
    
    style R fill:#ffcccc,stroke:#cc0000,color:#000000
    style G fill:#ccffcc,stroke:#00cc00,color:#000000
    style RF fill:#cce5ff,stroke:#0066cc,color:#000000

各フェーズの役割を整理すると以下のようになります。

フェーズ 目的 制約
Red 要件を明確にし、失敗するテストを書く まだ実装しない
Green テストを通す最小限のコードを書く 完璧を求めない
Refactor コードの品質を改善する 振る舞いを変えない

重要なのは、Greenフェーズでは完璧なコードを書く必要がないという点です。Greenフェーズの目的はテストを通すことだけであり、コードの美しさはRefactorフェーズで追求します。

なぜRefactorフェーズが必要なのか

Greenフェーズを繰り返すだけでは、以下の問題が発生します。

  1. コードの重複が蓄積する: 同じロジックが複数箇所に散在
  2. 可読性が低下する: 意図が伝わりにくいコード
  3. 変更コストが増大する: 1つの修正が多くの箇所に影響
  4. バグが混入しやすくなる: 複雑で理解困難なコード

Kent Beck氏は「まず動かし、次に正しくし、そして速くする」という格言を残しています。Greenフェーズで「動く」状態を作り、Refactorフェーズで「正しい」状態にするのです。

テストが与える安心感

リファクタリングにおいてテストが果たす役割は計り知れません。

flowchart TB
    subgraph before["テストなしのリファクタリング"]
        B1["コードを変更"] --> B2["動作確認<br/>(手動テスト)"]
        B2 --> B3["本番デプロイ"]
        B3 --> B4["予期せぬバグ発覚"]
        B4 --> B5["緊急対応"]
    end
    
    subgraph after["テストありのリファクタリング"]
        A1["コードを変更"] --> A2["テスト実行"]
        A2 --> A3{"結果"}
        A3 -->|Green| A4["安心してデプロイ"]
        A3 -->|Red| A5["即座に問題を検知"]
        A5 --> A6["変更を修正"]
        A6 --> A2
    end

テストがあれば、変更が既存の振る舞いを壊していないことを即座に確認できます。これが「自信を持ってコードを改善する」ための基盤です。

テストに守られたリファクタリングの進め方

基本原則: 小さなステップで進む

リファクタリングの最も重要な原則は、小さなステップで進むことです。大きな変更を一度に行うと、問題が発生したときに原因の特定が困難になります。

1
2
3
4
5
6
7
悪い例:
クラス全体を大幅に書き換え → テスト実行 → Red → どこが原因?

良い例:
メソッド名を変更 → テスト実行 → Green
メソッドを抽出 → テスト実行 → Green
変数名を変更 → テスト実行 → Green

実践: 注文計算ロジックのリファクタリング

具体的なリファクタリングの進め方を、注文計算のコードを例に解説します。

初期状態(リファクタリング前):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// orderCalculator.js - リファクタリング前
function calculateOrderTotal(items, customerType, couponCode) {
  let total = 0;
  for (let i = 0; i < items.length; i++) {
    total += items[i].price * items[i].quantity;
  }
  
  // 会員割引
  if (customerType === 'gold') {
    total = total * 0.8;
  } else if (customerType === 'silver') {
    total = total * 0.9;
  }
  
  // クーポン割引
  if (couponCode === 'SAVE10') {
    total = total - 1000;
  } else if (couponCode === 'SAVE20') {
    total = total - 2000;
  }
  
  if (total < 0) {
    total = 0;
  }
  
  return total;
}

module.exports = { calculateOrderTotal };

テストコード(リファクタリング前から存在):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// orderCalculator.test.js
const { calculateOrderTotal } = require('./orderCalculator');

describe('calculateOrderTotal', () => {
  const sampleItems = [
    { price: 1000, quantity: 2 },
    { price: 500, quantity: 3 }
  ];

  test('商品の合計金額を計算する', () => {
    expect(calculateOrderTotal(sampleItems, 'regular', null))
      .toBe(3500);
  });

  test('ゴールド会員は20%割引', () => {
    expect(calculateOrderTotal(sampleItems, 'gold', null))
      .toBe(2800);
  });

  test('シルバー会員は10%割引', () => {
    expect(calculateOrderTotal(sampleItems, 'silver', null))
      .toBe(3150);
  });

  test('SAVE10クーポンで1000円引き', () => {
    expect(calculateOrderTotal(sampleItems, 'regular', 'SAVE10'))
      .toBe(2500);
  });

  test('割引後の金額が0未満にならない', () => {
    const cheapItems = [{ price: 100, quantity: 1 }];
    expect(calculateOrderTotal(cheapItems, 'gold', 'SAVE10'))
      .toBe(0);
  });
});

ステップ1: 商品合計の計算を抽出

まず、商品合計の計算ロジックを別関数に抽出します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// ステップ1: 商品合計の計算を関数として抽出
function calculateSubtotal(items) {
  let subtotal = 0;
  for (let i = 0; i < items.length; i++) {
    subtotal += items[i].price * items[i].quantity;
  }
  return subtotal;
}

function calculateOrderTotal(items, customerType, couponCode) {
  let total = calculateSubtotal(items);
  
  // 会員割引
  if (customerType === 'gold') {
    total = total * 0.8;
  } else if (customerType === 'silver') {
    total = total * 0.9;
  }
  
  // クーポン割引
  if (couponCode === 'SAVE10') {
    total = total - 1000;
  } else if (couponCode === 'SAVE20') {
    total = total - 2000;
  }
  
  if (total < 0) {
    total = 0;
  }
  
  return total;
}

テストを実行して、すべてGreenであることを確認します。

ステップ2: for文をreduceに置き換え

より宣言的な書き方に改善します。

1
2
3
4
5
6
7
// ステップ2: reduceを使用した書き換え
function calculateSubtotal(items) {
  return items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );
}

テストを実行して、すべてGreenであることを確認します。

ステップ3: 会員割引の計算を抽出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ステップ3: 会員割引率を取得する関数を抽出
function getMemberDiscountRate(customerType) {
  const discountRates = {
    gold: 0.8,
    silver: 0.9
  };
  return discountRates[customerType] || 1.0;
}

function calculateOrderTotal(items, customerType, couponCode) {
  let total = calculateSubtotal(items);
  total = total * getMemberDiscountRate(customerType);
  
  // クーポン割引
  if (couponCode === 'SAVE10') {
    total = total - 1000;
  } else if (couponCode === 'SAVE20') {
    total = total - 2000;
  }
  
  if (total < 0) {
    total = 0;
  }
  
  return total;
}

テストを実行して、すべてGreenであることを確認します。

ステップ4: クーポン割引の計算を抽出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// ステップ4: クーポン割引額を取得する関数を抽出
function getCouponDiscountAmount(couponCode) {
  const couponDiscounts = {
    SAVE10: 1000,
    SAVE20: 2000
  };
  return couponDiscounts[couponCode] || 0;
}

function calculateOrderTotal(items, customerType, couponCode) {
  let total = calculateSubtotal(items);
  total = total * getMemberDiscountRate(customerType);
  total = total - getCouponDiscountAmount(couponCode);
  return Math.max(total, 0);
}

テストを実行して、すべてGreenであることを確認します。

最終状態(リファクタリング後):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// orderCalculator.js - リファクタリング後
function calculateSubtotal(items) {
  return items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );
}

function getMemberDiscountRate(customerType) {
  const discountRates = {
    gold: 0.8,
    silver: 0.9
  };
  return discountRates[customerType] || 1.0;
}

function getCouponDiscountAmount(couponCode) {
  const couponDiscounts = {
    SAVE10: 1000,
    SAVE20: 2000
  };
  return couponDiscounts[couponCode] || 0;
}

function calculateOrderTotal(items, customerType, couponCode) {
  const subtotal = calculateSubtotal(items);
  const afterMemberDiscount = subtotal * getMemberDiscountRate(customerType);
  const afterCouponDiscount = afterMemberDiscount - getCouponDiscountAmount(couponCode);
  return Math.max(afterCouponDiscount, 0);
}

module.exports = { calculateOrderTotal };

リファクタリングにより、以下の改善が達成されました。

改善点 ビフォー アフター
関数の責務 1関数に全ロジック 責務ごとに分離
可読性 if文の連続 宣言的なオブジェクト
拡張性 条件追加が煩雑 オブジェクトに追加するだけ
テスタビリティ 結合テストのみ 各関数を個別にテスト可能

コードの臭い(Code Smell)の発見と対処

コードの臭いとは

コードの臭い(Code Smell) は、Martin Fowler氏とKent Beck氏が提唱した概念で、「コードに問題がある可能性を示唆する兆候」を指します。バグではありませんが、放置するとバグを生みやすく、変更を困難にする要因となります。

リファクタリングの第一歩は、これらの臭いを嗅ぎ分けることです。

代表的なコードの臭いと対処法

Long Method(長いメソッド)

50行を超えるメソッドは、複数の責務を持っている可能性が高いです。

発見の兆候:

  • メソッドの全体像を把握するのにスクロールが必要
  • コメントで処理のブロックを区切っている
  • ネストが深い

対処法: Extract Method(メソッドの抽出)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// Before: 長いメソッド
public void processOrder(Order order) {
    // 在庫確認
    for (OrderItem item : order.getItems()) {
        Product product = productRepository.findById(item.getProductId());
        if (product.getStock() < item.getQuantity()) {
            throw new InsufficientStockException(product.getName());
        }
    }
    
    // 合計計算
    double total = 0;
    for (OrderItem item : order.getItems()) {
        total += item.getPrice() * item.getQuantity();
    }
    
    // 割引適用
    if (order.getCouponCode() != null) {
        Coupon coupon = couponRepository.findByCode(order.getCouponCode());
        total = total - coupon.getDiscountAmount();
    }
    
    // 注文保存
    order.setTotal(total);
    orderRepository.save(order);
    
    // 通知送信
    emailService.sendOrderConfirmation(order);
}

// After: メソッドを抽出
public void processOrder(Order order) {
    validateStock(order);
    double total = calculateTotal(order);
    total = applyDiscount(order, total);
    saveOrder(order, total);
    sendConfirmation(order);
}

private void validateStock(Order order) {
    for (OrderItem item : order.getItems()) {
        Product product = productRepository.findById(item.getProductId());
        if (product.getStock() < item.getQuantity()) {
            throw new InsufficientStockException(product.getName());
        }
    }
}

private double calculateTotal(Order order) {
    return order.getItems().stream()
        .mapToDouble(item -> item.getPrice() * item.getQuantity())
        .sum();
}
// ... 他のメソッドも同様に抽出

Duplicate Code(重複したコード)

同じ、または類似したコードが複数箇所に存在する状態です。

発見の兆候:

  • コピー&ペーストで作られたコード
  • 複数ファイルに似たロジック
  • 修正時に「あそこも直さなければ」と思う

対処法: Extract Method / Pull Up Method

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Before: 重複したバリデーション
function validateEmail(email) {
  if (!email) return false;
  if (!email.includes('@')) return false;
  if (email.length > 255) return false;
  return true;
}

function validateUserEmail(user) {
  if (!user.email) return false;
  if (!user.email.includes('@')) return false;
  if (user.email.length > 255) return false;
  return true;
}

// After: 共通化
function isValidEmail(email) {
  if (!email) return false;
  if (!email.includes('@')) return false;
  if (email.length > 255) return false;
  return true;
}

function validateUserEmail(user) {
  return isValidEmail(user.email);
}

Feature Envy(特性の横恋慕)

あるクラスのメソッドが、自分自身のデータよりも他のクラスのデータを多く使用している状態です。

発見の兆候:

  • 他のオブジェクトのgetterを連続して呼び出している
  • 他のクラスのデータを加工するロジックが自クラスにある

対処法: Move Method(メソッドの移動)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Before: OrderServiceがOrderItemのデータを羨ましがっている
public class OrderService {
    public double calculateItemTotal(OrderItem item) {
        return item.getPrice() * item.getQuantity() * 
               (1 - item.getDiscountRate());
    }
}

// After: ロジックをOrderItemに移動
public class OrderItem {
    private double price;
    private int quantity;
    private double discountRate;
    
    public double calculateTotal() {
        return price * quantity * (1 - discountRate);
    }
}

Primitive Obsession(基本型への執着)

ドメインの概念を表現するために、StringやIntegerなどの基本型を過度に使用している状態です。

発見の兆候:

  • 電話番号やメールアドレスがStringのまま
  • 金額計算にdoubleを直接使用
  • バリデーションロジックが散在

対処法: Replace Primitive with Object(基本型をオブジェクトに置換)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Before: 基本型を使用
public class User {
    private String email;
    private String phoneNumber;
    private double accountBalance;
}

// After: 値オブジェクトを導入
public class User {
    private Email email;
    private PhoneNumber phoneNumber;
    private Money accountBalance;
}

public class Email {
    private final String value;
    
    public Email(String value) {
        if (!isValid(value)) {
            throw new IllegalArgumentException(
                "Invalid email format: " + value);
        }
        this.value = value;
    }
    
    private boolean isValid(String email) {
        return email != null && 
               email.matches("^[\\w.-]+@[\\w.-]+\\.[a-z]{2,}$");
    }
}

Switch Statements(switch文の乱用)

同じswitchやif-else連鎖が複数箇所に現れている状態です。

発見の兆候:

  • 型による分岐が複数箇所に存在
  • 新しい型を追加するたびに複数箇所を修正

対処法: Replace Conditional with Polymorphism(条件分岐をポリモーフィズムに置換)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Before: switch文による分岐
public class ShippingCalculator {
    public double calculate(Order order) {
        switch (order.getShippingType()) {
            case "standard":
                return order.getWeight() * 100;
            case "express":
                return order.getWeight() * 200 + 500;
            case "overnight":
                return order.getWeight() * 300 + 1000;
            default:
                throw new IllegalArgumentException();
        }
    }
}

// After: ポリモーフィズムを使用
public interface ShippingStrategy {
    double calculate(Order order);
}

public class StandardShipping implements ShippingStrategy {
    public double calculate(Order order) {
        return order.getWeight() * 100;
    }
}

public class ExpressShipping implements ShippingStrategy {
    public double calculate(Order order) {
        return order.getWeight() * 200 + 500;
    }
}

public class OvernightShipping implements ShippingStrategy {
    public double calculate(Order order) {
        return order.getWeight() * 300 + 1000;
    }
}

コードの臭いの分類

コードの臭いは、その性質によって以下のカテゴリに分類できます。

カテゴリ 代表的な臭い 主な原因
肥大化(Bloaters) Long Method, Large Class 機能追加の蓄積
OOの誤用(OO Abusers) Switch Statements, Refused Bequest 設計の不備
変更の障害(Change Preventers) Divergent Change, Shotgun Surgery 責務の分離不足
不要なもの(Dispensables) Dead Code, Duplicate Code メンテナンス不足
結合度(Couplers) Feature Envy, Message Chains 依存関係の問題

リファクタリングカタログの活用方法

リファクタリングカタログとは

リファクタリングカタログは、Martin Fowler氏が著書『Refactoring』で体系化した、コード改善のパターン集です。各リファクタリングには、目的、手順、適用前後のコード例が記載されています。

現在はオンラインでも参照可能です。

代表的なリファクタリングパターン

Extract Method(メソッドの抽出)

コードの断片をメソッドとして抽出し、意図が伝わる名前を付けます。

適用場面:

  • 長いメソッドを分割したい
  • コメントで説明が必要なコードブロックがある
  • 同じ処理が複数箇所にある
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Before
function printOwing(invoice) {
  let outstanding = 0;
  
  console.log('***********************');
  console.log('**** Customer Owes ****');
  console.log('***********************');
  
  for (const order of invoice.orders) {
    outstanding += order.amount;
  }
  
  console.log(`name: ${invoice.customer}`);
  console.log(`amount: ${outstanding}`);
}

// After
function printOwing(invoice) {
  printBanner();
  const outstanding = calculateOutstanding(invoice);
  printDetails(invoice, outstanding);
}

function printBanner() {
  console.log('***********************');
  console.log('**** Customer Owes ****');
  console.log('***********************');
}

function calculateOutstanding(invoice) {
  return invoice.orders.reduce((sum, order) => sum + order.amount, 0);
}

function printDetails(invoice, outstanding) {
  console.log(`name: ${invoice.customer}`);
  console.log(`amount: ${outstanding}`);
}

Inline Method(メソッドのインライン化)

メソッドの本体が名前と同じくらい明確な場合、メソッドを展開します。Extract Methodの逆操作です。

適用場面:

  • メソッドの中身が自明
  • 過度に細分化されたメソッド群を整理したい
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Before: 過度に細分化されたメソッド
function getRating(driver) {
  return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}

function moreThanFiveLateDeliveries(driver) {
  return driver.numberOfLateDeliveries > 5;
}

// After: インライン化
function getRating(driver) {
  return driver.numberOfLateDeliveries > 5 ? 2 : 1;
}

Replace Temp with Query(一時変数をクエリに置換)

一時変数をメソッド呼び出しに置き換えます。

適用場面:

  • 一時変数が複数回使用されている
  • 計算ロジックを再利用したい
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Before
function calculateTotal(order) {
  const basePrice = order.quantity * order.itemPrice;
  const discountFactor = order.quantity > 100 ? 0.9 : 1;
  return basePrice * discountFactor;
}

// After
function calculateTotal(order) {
  return getBasePrice(order) * getDiscountFactor(order);
}

function getBasePrice(order) {
  return order.quantity * order.itemPrice;
}

function getDiscountFactor(order) {
  return order.quantity > 100 ? 0.9 : 1;
}

Decompose Conditional(条件分岐の分解)

複雑な条件式をメソッドに抽出します。

適用場面:

  • 条件式が複雑で読みにくい
  • 条件の意図を明確にしたい
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Before
function calculateCharge(date, quantity) {
  let charge;
  if (date.isBefore(SUMMER_START) || date.isAfter(SUMMER_END)) {
    charge = quantity * winterRate + winterServiceCharge;
  } else {
    charge = quantity * summerRate;
  }
  return charge;
}

// After
function calculateCharge(date, quantity) {
  if (isWinter(date)) {
    return winterCharge(quantity);
  }
  return summerCharge(quantity);
}

function isWinter(date) {
  return date.isBefore(SUMMER_START) || date.isAfter(SUMMER_END);
}

function winterCharge(quantity) {
  return quantity * winterRate + winterServiceCharge;
}

function summerCharge(quantity) {
  return quantity * summerRate;
}

リファクタリングカタログの使い方

リファクタリングカタログを効果的に活用するための手順を紹介します。

手順1: コードの臭いを特定する

まず、コードのどこに問題があるかを特定します。前述のコードの臭いのカテゴリを参考にしてください。

手順2: 対応するリファクタリングを探す

カタログから、その臭いに対応するリファクタリングを探します。

コードの臭い 適用するリファクタリング
Long Method Extract Method
Duplicate Code Extract Method, Pull Up Method
Feature Envy Move Method
Primitive Obsession Replace Primitive with Object
Switch Statements Replace Conditional with Polymorphism
Long Parameter List Introduce Parameter Object

手順3: 手順に従って適用する

カタログに記載された手順に従い、小さなステップでリファクタリングを適用します。各ステップ後にテストを実行し、振る舞いが変わっていないことを確認します。

TDDとリファクタリングの実践ワークフロー

日常的なリファクタリングのタイミング

リファクタリングは、以下のタイミングで日常的に行います。

1. Red-Green-Refactorサイクルの一部として

新機能を追加するたびにRefactorフェーズでコードを改善します。

2. 機能追加の前準備として

新しい機能を追加する前に、追加しやすい形にコードを整理します。Martin Fowler氏はこれを「準備のためのリファクタリング」と呼んでいます。

3. コードレビュー時に

レビューで発見された問題点を修正する際に、リファクタリングを適用します。

リファクタリングを行うべきでないタイミング

以下の状況では、リファクタリングを避けるべきです。

状況 理由
テストがない 安全性を担保できない
期限直前 リスクが高い
コードを捨てる予定 投資対効果が悪い
動作を理解していない 意図せず振る舞いを変えてしまう

リファクタリングのDo / Don’t

Do(推奨):

  • 小さなステップで進める
  • 各ステップ後にテストを実行する
  • 振る舞いを変えずに構造だけを変える
  • バージョン管理でこまめにコミットする
  • リファクタリングと機能追加を分けてコミットする

Don’t(禁止):

  • テストなしでリファクタリングする
  • 複数の変更を一度に行う
  • リファクタリングと機能追加を同時に行う
  • 「ついでに」別の問題を直す

まとめ

TDDにおけるリファクタリングは、単なるコード整理ではありません。テストという安全網に守られながら、継続的にコードの品質を向上させる技術です。

本記事で解説した内容を振り返ります。

  1. Red-Green-Refactorサイクルとリファクタリングの関係: リファクタリングはTDDの不可欠な第三フェーズであり、Greenフェーズで「動く」コードを、Refactorフェーズで「正しい」コードにする
  2. テストに守られたリファクタリングの進め方: 小さなステップで進み、各ステップ後にテストを実行して振る舞いが変わっていないことを確認する
  3. コードの臭いの発見と対処: Long Method、Duplicate Code、Feature Envyなどの臭いを嗅ぎ分け、適切なリファクタリングで対処する
  4. リファクタリングカタログの活用: Extract Method、Replace Temp with Queryなどのパターンを状況に応じて適用する

リファクタリングは一度で完璧にする必要はありません。ボーイスカウトの規則「来たときよりも美しく」を心がけ、コードに触れるたびに少しずつ改善していきましょう。テストがあれば、その改善は安全に行えます。

参考リンク