コードスメルの検出
ストーリー
「コードスメルって聞いたことあるか?」
松本先輩が別の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分