LESSON 30分

コードスメルの検出

ストーリー

「コードスメルって聞いたことあるか?」

松本先輩が別のPRの画面を開きながら聞いた。

「スメル......匂いですか?」

「そうだ。バグではないけど、『何か臭う』コードのことだ。 放置すると技術的負債になり、いずれバグや障害を引き起こす。 レビュアーとして、このスメルを嗅ぎ分ける力が大事なんだ」


コードスメルとは

コードスメルとは、コードの品質に問題がある兆候(サイン)です。即座にバグにはならないが、将来的にメンテナンスが困難になったり、バグを誘発したりする可能性が高いコードのパターンを指します。

Martin Fowler が著書『Refactoring』で体系化した概念です。


代表的なコードスメル

1. 長い関数(Long Method)

typescript
// ❌ コードスメル: 1つの関数が多くのことをやりすぎている
async function processOrder(order: Order) {
  // バリデーション(20行)
  if (!order.items || order.items.length === 0) {
    throw new Error('No items');
  }
  for (const item of order.items) {
    if (item.quantity <= 0) throw new Error('Invalid quantity');
    if (item.price < 0) throw new Error('Invalid price');
    const stock = await getStock(item.productId);
    if (stock < item.quantity) throw new Error('Out of stock');
  }

  // 金額計算(15行)
  let subtotal = 0;
  for (const item of order.items) {
    subtotal += item.price * item.quantity;
  }
  const tax = subtotal * 0.1;
  const shipping = subtotal > 5000 ? 0 : 500;
  const total = subtotal + tax + shipping;

  // DB保存(10行)
  const savedOrder = await db.orders.create({
    ...order,
    subtotal,
    tax,
    shipping,
    total,
    status: 'confirmed',
  });

  // メール送信(15行)
  const template = await loadTemplate('order-confirmation');
  const html = renderTemplate(template, { order: savedOrder });
  await sendEmail(order.customerEmail, 'ご注文確認', html);

  // ログ記録(5行)
  await logActivity('order_created', { orderId: savedOrder.id });

  return savedOrder;
}
// → 60行以上の関数。バリデーション、計算、保存、通知、ログが混在

// ✅ 改善: 責務ごとに分割
async function processOrder(order: Order) {
  validateOrder(order);
  await checkInventory(order.items);
  const pricing = calculatePricing(order.items);
  const savedOrder = await saveOrder(order, pricing);
  await notifyCustomer(savedOrder);
  await logOrderCreated(savedOrder);
  return savedOrder;
}

2. 重複コード(Duplicated Code)

typescript
// ❌ コードスメル: 同じロジックが複数箇所にコピーされている
class UserController {
  async getUser(req: Request, res: Response) {
    try {
      const user = await userService.findById(req.params.id);
      if (!user) {
        res.status(404).json({ error: 'User not found' });
        return;
      }
      res.json(user);
    } catch (error) {
      console.error('Error:', error);
      res.status(500).json({ error: 'Internal server error' });
    }
  }

  async getOrder(req: Request, res: Response) {
    try {
      const order = await orderService.findById(req.params.id);
      if (!order) {
        res.status(404).json({ error: 'Order not found' });
        return;
      }
      res.json(order);
    } catch (error) {
      console.error('Error:', error);
      res.status(500).json({ error: 'Internal server error' });
    }
  }
}

// ✅ 改善: 共通処理を抽出
function asyncHandler(fn: (req: Request, res: Response) => Promise<void>) {
  return async (req: Request, res: Response) => {
    try {
      await fn(req, res);
    } catch (error) {
      console.error('Error:', error);
      res.status(500).json({ error: 'Internal server error' });
    }
  };
}

3. マジックナンバー / マジックストリング

typescript
// ❌ コードスメル: 意味不明な数値や文字列が散在
function calculateShipping(weight: number, zone: string): number {
  if (zone === 'A') {
    return weight * 120;
  } else if (zone === 'B') {
    return weight * 180;
  } else {
    return weight * 250;
  }
}

if (user.loginAttempts > 5) {
  lockAccount(user);
}

if (order.total > 10000) {
  applyDiscount(order, 0.05);
}

// ✅ 改善: 定数として定義
const SHIPPING_RATE = {
  ZONE_A: 120,
  ZONE_B: 180,
  ZONE_OTHER: 250,
} as const;

const MAX_LOGIN_ATTEMPTS = 5;
const FREE_SHIPPING_THRESHOLD = 10000;
const BULK_ORDER_DISCOUNT_RATE = 0.05;

4. 神クラス(God Class)

typescript
// ❌ コードスメル: 1つのクラスが全てを知っている
class ApplicationManager {
  // ユーザー管理
  async createUser() { /* ... */ }
  async deleteUser() { /* ... */ }
  async updateUser() { /* ... */ }

  // 注文管理
  async createOrder() { /* ... */ }
  async cancelOrder() { /* ... */ }

  // 在庫管理
  async checkInventory() { /* ... */ }
  async updateStock() { /* ... */ }

  // メール送信
  async sendEmail() { /* ... */ }

  // レポート生成
  async generateReport() { /* ... */ }
}
// → このクラスは変更理由が多すぎる(SRP違反)

5. Feature Envy(機能の横恋慕)

typescript
// ❌ コードスメル: 別のオブジェクトのデータに依存しすぎ
class OrderPrinter {
  printOrderSummary(order: Order) {
    const subtotal = order.items.reduce(
      (sum, item) => sum + item.price * item.quantity, 0
    );
    const tax = subtotal * order.taxRate;
    const total = subtotal + tax - order.discount;
    console.log(`Subtotal: ${subtotal}, Tax: ${tax}, Total: ${total}`);
  }
}
// → OrderPrinter が Order の内部データを使って計算している
// → この計算は Order クラス自体が行うべき

// ✅ 改善: 計算ロジックを Order に移動
class Order {
  getSubtotal(): number { /* ... */ }
  getTax(): number { /* ... */ }
  getTotal(): number { /* ... */ }
}

class OrderPrinter {
  printOrderSummary(order: Order) {
    console.log(
      `Subtotal: ${order.getSubtotal()}, Tax: ${order.getTax()}, Total: ${order.getTotal()}`
    );
  }
}

6. プリミティブ執着(Primitive Obsession)

typescript
// ❌ コードスメル: プリミティブ型で全てを表現
function createUser(
  name: string,
  email: string,          // 形式チェックは?
  phone: string,          // 形式チェックは?
  zipCode: string,        // 形式チェックは?
  birthDate: string,      // ISO形式?和暦?
  role: string,           // どんな値が有効?
) { /* ... */ }

// ✅ 改善: Value Object で意味のある型を定義
function createUser(input: {
  name: UserName,
  email: Email,
  phone: PhoneNumber,
  address: Address,
  birthDate: BirthDate,
  role: UserRole,
}) { /* ... */ }

コードスメルの一覧表

スメル兆候リスク対策
長い関数20行以上の関数理解困難、テスト困難関数分割
重複コードコピペされたロジック修正漏れ共通化、抽象化
マジックナンバー意味不明な数値意図不明、修正困難定数化
神クラス巨大なクラス変更影響が大クラス分割
Feature Envy他オブジェクトのデータに依存カプセル化違反ロジックを適切なクラスに移動
プリミティブ執着全てが string / numberバリデーション漏れValue Object
深いネストif/forが3段以上可読性低下早期リターン、関数分割
長いパラメータリスト引数が5つ以上呼び出し側の混乱オブジェクト引数

まとめ

項目ポイント
コードスメルの定義バグではないが品質に問題がある兆候
主なスメル長い関数、重複コード、マジックナンバー、神クラスなど
検出のポイント可読性・保守性・テスタビリティの観点
レビューでの指摘[Should Fix] として改善提案を添えて指摘

チェックリスト

  • コードスメルの概念を説明できる
  • 6つ以上のコードスメルを識別できる
  • 各コードスメルに対する改善策を提案できる
  • レビューでコードスメルを指摘できる

次のステップへ

コードスメルを検出できるようになったら、次はリファクタリングの提案方法を学びます。スメルを発見するだけでなく、具体的な改善案を提示できるようになりましょう。


推定読了時間: 30分