目次

この記事の内容

リファクタリングは、ソフトウェアの品質を維持し続けるための不可欠な活動です。本記事では、コードスメルの識別から、具体的なリファクタリング技法、安全性を担保する手順までを体系的に解説します。


リファクタリングの基本概念

リファクタリングとは

**リファクタリング(Refactoring)**とは、外部からの振る舞いを変えずに、コードの内部構造を整理し、理解しやすく、変更しやすい状態にすることです。

【リファクタリングの定義】
・外部振る舞い:不變(テストで保証)
・内部構造:改善(可読性、保守性、拡張性)
・目的:技術的負債の削減、開発速度の維持

リファクタリングと修正の違い

作業目的振る舞いテスト
リファクタリング構造改善不変既存テストで保証
バグ修正不具合修正変更(期待通り)回帰テスト追加
機能追加新機能実装追加新規テスト追加

リファクタリングのメリット

【短期的メリット】
・コードの可読性向上
・バグの発見
・理解しやすくなる

【長期的メリット】
・保守コスト削減
・新機能追加が容易
・開発速度の低下防止
・技術的負債の蓄積抑制

コードスメル——リファクタリングの兆候

コードスメルとは

**コードスメル(Code Smell)**とは、コードの中に漂う「悪臭」であり、潜在的な問題を示す兆候です。バグそのものではありませんが、放置すると問題を引き起こします。

代表的なコードスメル 7 選

1. 重複コード(Duplicated Code)

【症状】
同じようなコードが複数箇所に存在

【問題点】
・修正漏れのリスク
・コード量増加
・一貫性維持の困難

【解決策】
・メソッド抽出
・クラス抽出
・テンプレートメソッドパターン
・ストラテジーパターン
// ❌ 重複コード
function calculateOrderTotal(items) {
  let total = 0;
  for (const item of items) {
    total += item.price * item.quantity;
  }
  const tax = total * 0.1;
  return total + tax;
}

function calculateCartTotal(items) {
  let total = 0;
  for (const item of items) {
    total += item.price * item.quantity;
  }
  const tax = total * 0.1;
  return total + tax;
}

// ✅ 共通メソッドに抽出
function calculateTotal(items) {
  const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const tax = subtotal * 0.1;
  return subtotal + tax;
}

function calculateOrderTotal(items) {
  return calculateTotal(items);
}

function calculateCartTotal(items) {
  return calculateTotal(items);
}

2. 長いメソッド(Long Method)

【症状】
メソッドが 50 行、100 行を超える

【問題点】
・理解に時間がかかる
・デバッグが困難
・再利用できない

【解決策】
・メソッド抽出(Extract Method)
・一時変数の削減
・早期リターン
// ❌ 長いメソッド(100 行超)
function processOrder(order) {
  // 1. 在庫確認
  let hasStock = true;
  for (const item of order.items) {
    const product = getProduct(item.productId);
    if (product.stock < item.quantity) {
      hasStock = false;
      break;
    }
  }
  if (!hasStock) {
    return { success: false, message: '在庫不足' };
  }

  // 2. 価格計算
  let subtotal = 0;
  for (const item of order.items) {
    const product = getProduct(item.productId);
    subtotal += product.price * item.quantity;
  }
  const discount = order.memberId ? subtotal * 0.1 : 0;
  const tax = (subtotal - discount) * 0.1;
  const total = subtotal - discount + tax;

  // 3. ポイント計算
  let points = 0;
  if (order.memberId) {
    const member = getMember(order.memberId);
    points = Math.floor(total * member.pointRate);
  }

  // 4. 在庫更新
  for (const item of order.items) {
    const product = getProduct(item.productId);
    product.stock -= item.quantity;
    updateProduct(product);
  }

  // 5. オーダー保存
  const savedOrder = saveOrder({
    ...order,
    total,
    points,
    status: 'confirmed'
  });

  // 6. メール送信
  sendOrderConfirmationEmail(savedOrder);

  return { success: true, orderId: savedOrder.id };
}

// ✅ 短いメソッドに分割
function processOrder(order) {
  if (!checkStock(order.items)) {
    return { success: false, message: '在庫不足' };
  }

  const { subtotal, discount, tax, total } = calculateOrderTotals(order);
  const points = calculatePoints(order, total);
  updateStock(order.items);

  const savedOrder = saveOrder({
    ...order,
    total,
    points,
    status: 'confirmed'
  });

  sendOrderConfirmationEmail(savedOrder);

  return { success: true, orderId: savedOrder.id };
}

function checkStock(items) {
  return items.every(item => {
    const product = getProduct(item.productId);
    return product.stock >= item.quantity;
  });
}

function calculateOrderTotals(order) {
  const subtotal = order.items.reduce((sum, item) => {
    const product = getProduct(item.productId);
    return sum + product.price * item.quantity;
  }, 0);

  const discount = order.memberId ? subtotal * 0.1 : 0;
  const tax = (subtotal - discount) * 0.1;
  const total = subtotal - discount + tax;

  return { subtotal, discount, tax, total };
}

function calculatePoints(order, total) {
  if (!order.memberId) return 0;
  const member = getMember(order.memberId);
  return Math.floor(total * member.pointRate);
}

function updateStock(items) {
  items.forEach(item => {
    const product = getProduct(item.productId);
    product.stock -= item.quantity;
    updateProduct(product);
  });
}

3. 巨大なクラス(Large Class)

【症状】
・1 つのクラスが 1000 行を超える
・責任が複数ある(神オブジェクト)

【問題点】
・理解が困難
・修正の影響範囲が広い
・テストが書きにくい

【解決策】
・クラス抽出(Extract Class)
・責務の分離(Single Responsibility Principle)
・コンポジションへの置き換え

4. 長すぎるパラメータリスト(Long Parameter List)

【症状】
メソッドのパラメータが 4-5 個を超える

【問題点】
・呼び出しが煩雑
・順序を間違えやすい
・変更に弱い

【解決策】
・パラメータオブジェクトの導入
・オブジェクト全体の受け渡し
// ❌ 長すぎるパラメータリスト
function createUser(email, password, firstName, lastName, dateOfBirth, phoneNumber, address) {
  // ...
}

// ✅ パラメータオブジェクト
function createUser(userData) {
  const { email, password, firstName, lastName, dateOfBirth, phoneNumber, address } = userData;
  // ...
}

// 使用例
createUser({
  email: 'test@example.com',
  password: 'secure123',
  firstName: 'Taro',
  lastName: 'Yamada',
  dateOfBirth: '1990-01-01',
  phoneNumber: '090-1234-5678',
  address: '東京都...'
});

5. データへの執着(Data Clumps)

【症状】
常に一緒に登場するデータのかたまり

【解決策】
・専用クラスにまとめる
// ❌ データの塊
function calculateShipping(weight, width, height, depth) {
  // ...
}

// ✅ クラスにまとめる
class Dimensions {
  constructor(weight, width, height, depth) {
    this.weight = weight;
    this.width = width;
    this.height = height;
    this.depth = depth;
  }

  get volumetricWeight() {
    return (this.width * this.height * this.depth) / 5000;
  }

  get billableWeight() {
    return Math.max(this.weight, this.volumetricWeight);
  }
}

function calculateShipping(dimensions) {
  const rate = getShippingRate(dimensions.billableWeight);
  return dimensions.billableWeight * rate;
}

6. 重複した条件ロジック(Duplicate Conditional Logic)

【症状】
同じ条件式が複数箇所に出現

【解決策】
・条件のメソッド化
・ポリモーフィズムの活用
// ❌ 重複した条件
function getCharge(customer) {
  if (customer.type === 'premium') {
    return baseCharge * 0.8;
  }
  if (customer.type === 'business') {
    return baseCharge * 0.9;
  }
  return baseCharge;
}

function getShippingRate(customer) {
  if (customer.type === 'premium') {
    return 0; // 送料無料
  }
  if (customer.type === 'business') {
    return baseRate * 0.5;
  }
  return baseRate;
}

// ✅ ポリモーフィズム
class Customer {
  getDiscountRate() { return 1.0; }
  getShippingMultiplier() { return 1.0; }
}

class PremiumCustomer extends Customer {
  getDiscountRate() { return 0.8; }
  getShippingMultiplier() { return 0.0; }
}

class BusinessCustomer extends Customer {
  getDiscountRate() { return 0.9; }
  getShippingMultiplier() { return 0.5; }
}

7. コメントへの依存(Comments as Code Smell)

【症状】
・コードではなくコメントで説明している
・「なぜ」ではなく「何を」を説明している

【問題点】
・コメントとコードの乖離
・コメントのメンテナンスコスト

【解決策】
・コード自体を分かりやすくする
・メソッド名、変数名で意図を表現
// ❌ コメントで説明
// 未成年なら false を返す
function checkAge(user) {
  if (user.age < 18) {
    return false;
  }
  return true;
}

// ✅ コード自体を明確に
function isAdult(user) {
  return user.age >= 18;
}

リファクタリング技法

1. メソッド抽出(Extract Method)

【目的】
長いメソッドを小さなメソッドに分割する

【手順】
1. 抽出するコードブロックを選択
2. 意味のある名前を付ける
3. ローカル変数の使用を確認
4. メソッドとして抽出
5. 呼び出し元に置き換え
// Before
function printOwing(invoice) {
  let outstanding = 0;

  // 明細の出力
  console.log('*******************************');
  console.log('**** 顧客のご請求額 ***********');
  console.log('*******************************');

  // 合計の計算
  for (const o of invoice.outstanding) {
    outstanding += o.amount;
  }

  // 期日の記録
  const today = new Date();
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);

  // 最終出力
  console.log(`名前:${invoice.customer}`);
  console.log(`金額:${outstanding}`);
  console.log(`期日:${invoice.dueDate}`);
}

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

function printBanner() {
  console.log('*******************************');
  console.log('**** 顧客のご請求額 ***********');
  console.log('*******************************');
}

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

function recordDueDate(invoice) {
  const today = new Date();
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
}

function printDetails(invoice, outstanding) {
  console.log(`名前:${invoice.customer}`);
  console.log(`金額:${outstanding}`);
  console.log(`期日:${invoice.dueDate}`);
}

2. 変数の置き換え(Replace Variable)

【目的】
一時変数を削減し、コードをシンプルにする

【技法】
・一時変数の置き換え
・ループ変数の分離
・中間変数の削除
// Before
function getPrice(order) {
  const basePrice = order.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const discount = basePrice * order.discountRate;
  const afterDiscount = basePrice - discount;
  const tax = afterDiscount * 0.1;
  const total = afterDiscount + tax;
  return total;
}

// After
function getPrice(order) {
  const basePrice = order.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const afterDiscount = basePrice * (1 - order.discountRate);
  return afterDiscount * 1.1; // 税込価格
}

3. クラスの抽出(Extract Class)

【目的】
巨大なクラスを責務ごとに分割する

【手順】
1. 責務を識別
2. 関連するフィールド・メソッドをグループ化
3. 新しいクラスを作成
4. 元のクラスから委譲
// Before
class Person {
  constructor(name, email, phone, address, city, zip, country) {
    this.name = name;
    this.email = email;
    this.phone = phone;
    this.address = address;
    this.city = city;
    this.zip = zip;
    this.country = country;
  }

  getFullAddress() {
    return `${this.address}, ${this.city}, ${this.zip}, ${this.country}`;
  }

  sendEmail(message) {
    // メール送信ロジック
  }

  sendSMS(message) {
    // SMS 送信ロジック
  }
}

// After
class ContactInfo {
  constructor(email, phone, address, city, zip, country) {
    this.email = email;
    this.phone = phone;
    this.address = address;
    this.city = city;
    this.zip = zip;
    this.country = country;
  }

  getFullAddress() {
    return `${this.address}, ${this.city}, ${this.zip}, ${this.country}`;
  }
}

class Person {
  constructor(name, contactInfo) {
    this.name = name;
    this.contact = contactInfo;
  }

  sendEmail(message) {
    // メール送信ロジック
  }

  sendSMS(message) {
    // SMS 送信ロジック
  }
}

4. 継承から委譲へ(Replace Inheritance with Delegation)

【目的】
不適切な継承をコンポジションに置き換える

【継承が問題になる場合】
・実装の流用だけのために継承している
・スーパークラスの実装詳細に依存したくない
// ❌ 不適切な継承
class ArrayStack extends Array {
  push(item) {
    // 追加のバリデーション
    if (!item) throw new Error('null は追加できません');
    super.push(item);
  }
}

// ✅ 委譲
class Stack {
  constructor() {
    this.items = [];
  }

  push(item) {
    if (!item) throw new Error('null は追加できません');
    this.items.push(item);
  }

  pop() {
    return this.items.pop();
  }

  peek() {
    return this.items[this.items.length - 1];
  }

  get size() {
    return this.items.length;
  }
}

5. 条件ロジックの単純化

【目的】
複雑な条件を分かりやすくする

【技法】
・ガード節(早期リターン)
・ポリモーフィズム
・テーブル駆動

ガード節

// ❌ ネストが深い
function calculateBonus(employee) {
  if (employee.performance === 'excellent') {
    if (!employee.hasWarnings) {
      if (employee.years >= 5) {
        return employee.salary * 0.3;
      } else {
        return employee.salary * 0.2;
      }
    } else {
      return employee.salary * 0.1;
    }
  } else {
    return 0;
  }
}

// ✅ ガード節
function calculateBonus(employee) {
  if (employee.performance !== 'excellent') return 0;
  if (employee.hasWarnings) return employee.salary * 0.1;
  if (employee.years >= 5) return employee.salary * 0.3;
  return employee.salary * 0.2;
}

テーブル駆動

// ❌ 複雑な条件分岐
function getShippingCost(country, weight) {
  if (country === 'JP') {
    if (weight <= 1) return 500;
    if (weight <= 5) return 800;
    return 1200;
  } else if (country === 'US') {
    if (weight <= 1) return 1500;
    if (weight <= 5) return 2500;
    return 4000;
  } else if (country === 'EU') {
    if (weight <= 1) return 1800;
    if (weight <= 5) return 3000;
    return 5000;
  }
  throw new Error(`Unknown country: ${country}`);
}

// ✅ テーブル駆動
const SHIPPING_RATES = {
  JP: [{ max: 1, cost: 500 }, { max: 5, cost: 800 }, { max: Infinity, cost: 1200 }],
  US: [{ max: 1, cost: 1500 }, { max: 5, cost: 2500 }, { max: Infinity, cost: 4000 }],
  EU: [{ max: 1, cost: 1800 }, { max: 5, cost: 3000 }, { max: Infinity, cost: 5000 }],
};

function getShippingCost(country, weight) {
  const rates = SHIPPING_RATES[country];
  if (!rates) throw new Error(`Unknown country: ${country}`);

  const rate = rates.find(r => weight <= r.max);
  return rate.cost;
}

6. API の単純化

【目的】
メソッドの使いやすさを向上させる

【技法】
・パラメータの追加・削除
・関数の名前変更
・オブジェクトの統合
// Before
function setAlert(customerId, alertType, isEmail, isSMS, isPush) {
  // ...
}

// After
function setAlert(customerId, alertType, channels = ['email']) {
  // ...
}

// 使用例
setAlert('cust123', 'promotion', ['email', 'sms', 'push']);

安全なリファクタリングの手順

2 つの帽子

【ケント・ベックの「2 つの帽子」】
・リファクタリング帽子:構造改善のみ(振る舞い不変)
・機能追加帽子:振る舞い変更(新機能・バグ修正)

→ 同時に被らない
・リファクタリング中:テストがすべて通っている状態を維持
・機能追加中:テストを追加・更新

リファクタリングの 3 ステップ

【ステップ 1: 準備】
1. 対象コードを特定
2. 既存テストの確認(カバレッジが十分か)
3. テストがなければ追加
4. 現在のテストをパスさせる

【ステップ 2: 実行】
1. 小さなステップで変更
2. 各ステップ後にテスト実行
3. 失敗したら即座に元に戻す

【ステップ 3: 検証】
1. 全テストを再実行
2. パフォーマンス確認(必要に応じて)
3. コードレビュー

リファクタリングチェックリスト

【リファクタリング前のチェックリスト】
□ 対象コードのテストは存在するか
□ テストは網羅的か(エッジケース含む)
□ テストは安定しているか(flaky ではない)
□ バージョン管理の準備はできているか
□ 影響範囲は把握したか

【リファクタリング中のチェックリスト】
□ 各ステップは小さいか(5-10 分以内)
□ テストは頻繁に実行しているか
□ 1 ステップで複数の変更をしていないか
□ 命名は適切か

【リファクタリング後のチェックリスト】
□ 全テストがパスしているか
□ コードは改善されたか
□ パフォーマンスは劣化していないか
□ ドキュメントは更新したか

テストとの関係

テストがリファクタリングを可能にする

【テストの重要性】
・テストがない = リファクタリングできない
・テストがある = 自信を持って変更できる

【テスト戦略】
1. ユニットテスト:各関数・メソッドの動作を保証
2. 統合テスト:コンポーネント間の連携を保証
3. 回帰テスト:既存機能が壊れていないことを保証

テストファーストのリファクタリング

【Red-Green-Refactor】
1. Red: 失敗するテストを書く
2. Green: テストをパスさせる(汚くても OK)
3. Refactor: コードを整理する

このサイクルで常にクリーンなコードを維持

リファクタリングに強いテストの書き方

// ❌ 実装詳細に依存したテスト
test('calculateTotal は reduce を使用する', () => {
  // 実装方法に依存 → リファクタリングで失敗
});

// ✅ 振る舞いに焦点
test('calculateTotal は合計金額を計算する', () => {
  const items = [
    { price: 100, quantity: 2 },
    { price: 50, quantity: 3 }
  ];
  expect(calculateTotal(items)).toBe(350);
});

test('calculateTotal は空配列なら 0 を返す', () => {
  expect(calculateTotal([])).toBe(0);
});

test('calculateTotal は負の数量を拒否する', () => {
  expect(() => calculateTotal([{ price: 100, quantity: -1 }]))
    .toThrow('数量は 0 以上である必要があります');
});

リファクタリングのタイミング

ルール 3: リファクタリングのタイミング

【ロバート・マーティンの「3 つのルール」】
1. 汚いコードを見つけたら、すぐきれいにする
2. 自分が入った時より少しきれいにして去る
3. 3 回同じことをしたら、リファクタリングする

【実践的なタイミング】
・コードレビュー中
・バグ修正時(関連箇所も整理)
・機能追加時(既存コードの整理)
・テスト追加時(テストしにくい = リファクタリング必要)

リファクタリングすべき時

【即座にリファクタリング】
・バグの原因がコードの複雑さ
・新規機能追加が困難
・理解に 30 分以上かかる

【計画的にリファクタリング】
・技術的負債が蓄積
・チーム全体の生産性低下
・パフォーマンスボトルネック

リファクタリングすべきでない時

【避けるべき状況】
・締め切り直前
・テストがないコード(まずテストを書く)
・仕様が確定していない
・パフォーマンスクリティカルな箇所(計測前に最適化しない)

【判断基準】
・ビジネス価値 > リファクタリングコスト
・緊急度が高い場合は後回し(技術的負債として記録)

リファクタリングの実践パターン

パターン 1: 機能移動に伴うリファクタリング

【状況】
新機能追加時に、既存コードが整理されていない

【アプローチ】
1. 既存コードをリファクタリング
2. 新機能を追加
3. テストで保証
// Before: 既存コードが複雑
function checkout(cart, user, paymentMethod, shippingAddress) {
  // 100 行の複雑なロジック
}

// After: リファクタリング後
function checkout(cart, user, paymentMethod, shippingAddress) {
  validateCart(cart);
  validateUser(user);

  const order = createOrder(cart, user);
  const payment = processPayment(order, paymentMethod);
  const shipment = createShipment(order, shippingAddress);

  sendConfirmation(order, user);

  return { order, payment, shipment };
}

// 新機能:ギフトオプション
function checkoutWithGift(cart, user, paymentMethod, shippingAddress, giftOptions) {
  validateCart(cart);
  validateUser(user);

  const order = createOrder(cart, user);
  const payment = processPayment(order, paymentMethod);
  const shipment = createShipment(order, shippingAddress);

  if (giftOptions) {
    applyGiftWrapping(shipment, giftOptions);
    addGiftMessage(shipment, giftOptions.message);
  }

  sendConfirmation(order, user);

  return { order, payment, shipment };
}

パターン 2: 並行開発との調整

【状況】
チームメンバーが同じファイルを編集している

【アプローチ】
1. 機能ブランチでリファクタリング
2. 短いサイクルでマージ
3. チームに周知

パターン 3: 段階的リファクタリング

【状況】
大規模なリファクタリングが必要

【アプローチ】
1. 依存関係を分析
2. 小さなステップに分割
3. 段階的に適用
4. 各ステップでテスト

アンチパターン

❌ 1. 完璧主義リファクタリング

【症状】
・全てを完璧にしようとする
・リファクタリング中に機能追加を始める
・「ついでに」他の箇所も修正

【対策】
・範囲を限定
・1 つの変更で 1 つの目的
・「ついでに」を禁止

❌ 2. テストなしリファクタリング

【症状】
・テストを書かずにリファクタリング
・手動テストのみで確認

【リスク】
・回帰バグの発見が遅れる
・自信を持って変更できない

【対策】
・まずテスト
・カバレッジを可視化

❌ 3. パフォーマンスと混同

【症状】
・最適化をリファクタリングと呼ぶ
・計測せずに「遅そう」で変更

【違い】
・リファクタリング:構造改善(速度不変)
・最適化:速度改善(構造変更)

【対策】
・計測してボトルネックを特定
・目的を明確に分ける

❌ 4. 大規模リファクタリング

【症状】
・数週間の大規模リファクタリング
・他の機能開発を停止

【リスク】
・コンフリクト
・ビジネス機会の損失
・挫折

【対策】
・小さなステップに分割
・継続的にマージ
・ビジネス価値とバランス

ツール活用

自動リファクタリングツール

言語ツール機能
JavaScript/TypeScriptVS Code, WebStormメソッド抽出、名前変更、移動
JavaIntelliJ IDEA強力なリファクタリング機能
PythonPyCharm, Rope名前変更、抽出、移動
Gogofmt, goplsフォーマット、整理
Rustrust-analyzer名前変更、抽出

静的解析ツール

// ESLint(JavaScript)
module.exports = {
  rules: {
    'complexity': ['error', { max: 10 }], // 循環的複雑性
    'max-lines-per-function': ['error', { max: 50 }],
    'max-params': ['error', { max: 4 }],
    'no-duplicate-imports': 'error',
  }
};

コードメトリクス

【循環的複雑性(Cyclomatic Complexity)】
・if, else, for, while, case などの分岐数
・目安:10 以下
・10 超:リファクタリング検討
・20 超:必須

【認知複雑性(Cognitive Complexity)】
・コードの理解しやすさ
・目安:15 以下

まとめ

リファクタリングの核心:

  1. 定義の理解: 外部振る舞いを変えずに内部構造を改善
  2. コードスメルの識別: 重複、長さ、複雑さの兆候をキャッチ
  3. 技法の習得: 抽出、移動、単純化の定石
  4. 安全性の確保: テストで保証、小さなステップ
  5. タイミングの判断: 即座に、計画的に、バランスよく

「リファクタリングは、今日できる最も重要な投資の一つである」

リファクタリングは、一見「生産的でない」ように見えるかもしれません。しかし、リファクタリングを怠ったコードは、時間とともに理解しにくくなり、変更が困難になり、最終的には開発速度を著しく低下させます。

小さなリファクタリングを毎日続けることが、長期的な開発速度の維持、バグの減少、チームの満足度向上につながります。


参考資料

  • 「リファクタリング——既存のコードを安全に改善する——」Martin Fowler, Kent Beck 著
  • 「クリーンコード——達人プログラマーの技術」Robert C. Martin 著
  • 「リファクタリング・ウェットウェア」Staffan Nöteberg 著
  • Refactoring.guru: https://refactoring.guru/
  • Martin Fowler’s Refactoring: https://martinfowler.com/tags/refactoring.html

免責事項 — 掲載情報は執筆時点のものです。料金・機能は変更される場合があります。最新情報は各公式サイトをご確認ください。