LESSON 30分

ストーリー

高橋アーキテクト
SOLID原則とデザインパターンを学んだ。でも、既存のコードベースにいきなりパターンを適用するのは危険だ
高橋アーキテクト
リファクタリングは、外部の振る舞いを変えずに内部構造を改善する技術。マーティン・ファウラーが体系化した、安全にコードを良くするための手順書のようなものだ

リファクタリングとは

リファクタリング(Refactoring): 外部から見た振る舞いを変えずに、コードの内部構造を改善する作業。

重要なポイント:

  • 振る舞いは変えない — テストが通り続けることで保証する
  • 小さなステップで行う — 一度に大きく変えない
  • 頻繁にテストを実行する — 各ステップの後にテストを回す

Extract Method(メソッドの抽出)

最も頻繁に使うリファクタリング技法です。長いメソッドから意味のあるまとまりを新しいメソッドに抽出します。

Before

class InvoiceGenerator {
  generate(order: Order): string {
    // 小計の計算
    let subtotal = 0;
    for (const item of order.items) {
      subtotal += item.price * item.quantity;
    }

    // 割引の計算
    let discount = 0;
    if (order.customer.membershipLevel === 'gold') {
      discount = subtotal * 0.1;
    } else if (order.customer.membershipLevel === 'silver') {
      discount = subtotal * 0.05;
    }

    // 税金の計算
    const taxableAmount = subtotal - discount;
    const tax = taxableAmount * 0.1;

    // 請求書のフォーマット
    let invoice = `Invoice for ${order.customer.name}\n`;
    invoice += `---\n`;
    for (const item of order.items) {
      invoice += `${item.name}: ${item.price} x ${item.quantity}\n`;
    }
    invoice += `---\n`;
    invoice += `Subtotal: ${subtotal}\n`;
    invoice += `Discount: -${discount}\n`;
    invoice += `Tax: ${tax}\n`;
    invoice += `Total: ${taxableAmount + tax}\n`;

    return invoice;
  }
}

After

class InvoiceGenerator {
  generate(order: Order): string {
    const subtotal = this.calculateSubtotal(order.items);
    const discount = this.calculateDiscount(subtotal, order.customer.membershipLevel);
    const tax = this.calculateTax(subtotal - discount);
    const total = subtotal - discount + tax;

    return this.formatInvoice(order, subtotal, discount, tax, total);
  }

  private calculateSubtotal(items: OrderItem[]): number {
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }

  private calculateDiscount(subtotal: number, membershipLevel: string): number {
    const rates: Record<string, number> = { gold: 0.1, silver: 0.05 };
    return subtotal * (rates[membershipLevel] ?? 0);
  }

  private calculateTax(taxableAmount: number): number {
    return taxableAmount * 0.1;
  }

  private formatInvoice(
    order: Order, subtotal: number, discount: number, tax: number, total: number
  ): string {
    const lines = [
      `Invoice for ${order.customer.name}`,
      '---',
      ...order.items.map(item => `${item.name}: ${item.price} x ${item.quantity}`),
      '---',
      `Subtotal: ${subtotal}`,
      `Discount: -${discount}`,
      `Tax: ${tax}`,
      `Total: ${total}`,
    ];
    return lines.join('\n');
  }
}

Rename(名前の変更)

コードの意図を正確に伝える名前に変更します。

// Before
function calc(d: any[]): number {
  let r = 0;
  for (const x of d) {
    r += x.p * x.q;
  }
  return r;
}

// After
function calculateOrderTotal(orderItems: OrderItem[]): number {
  let total = 0;
  for (const item of orderItems) {
    total += item.price * item.quantity;
  }
  return total;
}

Rename のコツ

対象ルール
変数中身を表す名詞dorderItems
関数動詞 + 目的語calccalculateOrderTotal
ブーリアンis/has/can/shouldflagisActive
定数UPPER_SNAKE_CASExTAX_RATE

Move Method / Move Field(メソッド/フィールドの移動)

メソッドやフィールドが、本来あるべきクラスに存在しない場合に移動します。

Before

// 特性の横恋慕:OrderPrinter が Order のデータを直接操作
class OrderPrinter {
  printSummary(order: Order): string {
    const total = order.items.reduce((s, i) => s + i.price * i.quantity, 0);
    const tax = total * order.taxRate;
    return `Order #${order.id}: ${total + tax}`;
  }
}

class Order {
  id: string;
  items: OrderItem[];
  taxRate: number;
}

After

// 計算ロジックをデータを持つクラスに移動
class Order {
  id: string;
  items: OrderItem[];
  taxRate: number;

  getSubtotal(): number {
    return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }

  getTax(): number {
    return this.getSubtotal() * this.taxRate;
  }

  getTotal(): number {
    return this.getSubtotal() + this.getTax();
  }
}

class OrderPrinter {
  printSummary(order: Order): string {
    return `Order #${order.id}: ${order.getTotal()}`;
  }
}

Inline Method / Inline Variable(メソッド/変数のインライン化)

Extract の逆。メソッドや変数が単純すぎて、そのまま展開した方がわかりやすい場合に使います。

// Before:変数が不必要に多い
function isEligible(user: User): boolean {
  const age = user.getAge();
  const isAdult = age >= 18;
  const hasAccount = user.getAccountStatus() === 'active';
  const isNotBanned = !user.isBanned();
  const result = isAdult && hasAccount && isNotBanned;
  return result;
}

// After:シンプルに
function isEligible(user: User): boolean {
  return user.getAge() >= 18
    && user.getAccountStatus() === 'active'
    && !user.isBanned();
}

リファクタリングの安全な進め方

高橋アーキテクトのアドバイス:

「リファクタリングの鉄則は”小さく、頻繁に”。1つの変更をしたらテストを実行する。テストが通ったら次の変更。テストが落ちたら、直前の変更を戻して原因を調べる。この繰り返しだ」

ステップ内容
1. テスト確認既存のテストがすべて通ることを確認
2. 小さな変更1つのリファクタリングテクニックを適用
3. テスト実行すべてのテストが通ることを確認
4. コミット動作する状態でコミット
5. 繰り返し次のリファクタリングへ

まとめ

ポイント内容
Extract Method長いメソッドから意味のある単位を抽出
Rename意図を正確に伝える名前に変更
Move Methodメソッドをデータを持つクラスに移動
Inline不要な中間変数やメソッドを除去
安全性小さなステップ + テスト実行の繰り返し

チェックリスト

  • Extract Method を使って長いメソッドを分割できる
  • Rename で意図の伝わる名前に変更できる
  • リファクタリングの安全な進め方を理解した

次のステップへ

次は「条件分岐の整理」です。複雑な if/else の連鎖をポリモーフィズムで置き換える方法を学びます。


推定読了時間: 30分