クリーンコードの原則|可読性・保守性を高める7つの実践ルール

kento_morota 26分で読めます

「自分が3ヶ月前に書いたコードが読めない」「他の人が書いたコードの修正に丸一日かかった」——こうした経験は、多くの開発者が通る道です。コードは書く時間よりも読む時間の方が圧倒的に長く、読みやすいコードは開発チーム全体の生産性を大きく左右します。

本記事では、Robert C. Martin氏の名著『Clean Code』の考え方をベースに、実務で即座に活用できる7つのクリーンコードの実践ルールを、具体的なコード例(Before/After)とともに解説します。

クリーンコードとは何か

クリーンコードとは、「他の開発者が読んで、すぐに理解でき、安心して変更できるコード」のことです。クリーンコードには以下の特徴があります。

読みやすい
コードを読んだだけで、何をしているかが理解できます。ドキュメントや口頭説明がなくても、コード自体が意図を語ります。

変更しやすい
ある部分を変更したとき、予期しない場所に影響が波及しません。変更箇所が局所化されています。

テストしやすい
各部分が独立してテスト可能で、テストコードを書くのに特別な工夫が不要です。

シンプル
必要以上に複雑ではなく、同じ処理を繰り返していません。

では、具体的な7つのルールを見ていきましょう。

ルール1:意図が伝わる名前を付ける

命名は、クリーンコードの最も基本的かつ重要なルールです。良い名前は、コードの可読性を劇的に向上させます。

変数名の改善

// Bad: 何を表す変数かわからない
const d = new Date();
const arr = users.filter(u => u.a > 18);
const flag = true;
const temp = price * 1.1;

// Good: 名前だけで意図が伝わる
const currentDate = new Date();
const adultUsers = users.filter(user => user.age > 18);
const isVerified = true;
const priceWithTax = price * 1.1;

変数名を見ただけで「何を格納しているか」「どういう条件で作られたか」がわかるようにします。1文字の変数名(du)やtemp、flag、dataといった曖昧な名前は避けましょう。

関数名の改善

// Bad: 何をする関数かわからない
function process(data) { ... }
function handle(items) { ... }
function doStuff(user) { ... }
function check(order) { ... }

// Good: 動詞 + 名詞で何をするか明確
function calculateTotalPrice(cartItems) { ... }
function sendVerificationEmail(user) { ... }
function validateOrderQuantity(order) { ... }
function formatPhoneNumber(rawNumber) { ... }

関数名は「動詞 + 名詞」の形式が基本です。「何を」「どうする」関数なのかが名前だけで伝わるようにします。

ブーリアン変数の命名

// Bad: 真偽が曖昧
const login = true;
const admin = false;
const visible = checkDisplay();

// Good: is/has/can/shouldで始める
const isLoggedIn = true;
const hasAdminRole = false;
const shouldDisplayBanner = checkBannerVisibility();
const canEditPost = user.role === 'admin' || user.id === post.authorId;

ブーリアン型の変数にはishascanshouldなどの接頭辞を付け、「はい/いいえ」で答えられる名前にします。

ルール2:関数は小さく、1つのことだけを行う

関数はできるだけ小さくし、1つの責務(タスク)だけを担うように設計します。

巨大な関数を分割する

// Bad: 1つの関数で複数のことをしている(注文処理の例)
async function processOrder(order) {
  // バリデーション
  if (!order.items || order.items.length === 0) {
    throw new Error('注文に商品がありません');
  }
  if (!order.customerId) {
    throw new Error('顧客IDが必要です');
  }

  // 在庫確認
  for (const item of order.items) {
    const stock = await db.query('SELECT stock FROM products WHERE id = ?', [item.productId]);
    if (stock[0].stock < item.quantity) {
      throw new Error(`商品 ${item.productId} の在庫が不足しています`);
    }
  }

  // 金額計算
  let total = 0;
  for (const item of order.items) {
    const product = await db.query('SELECT price FROM products WHERE id = ?', [item.productId]);
    total += product[0].price * item.quantity;
  }
  const tax = total * 0.1;
  const totalWithTax = total + tax;

  // 決済処理
  const paymentResult = await paymentGateway.charge({
    amount: totalWithTax,
    customerId: order.customerId,
  });

  // 在庫更新
  for (const item of order.items) {
    await db.query('UPDATE products SET stock = stock - ? WHERE id = ?',
      [item.quantity, item.productId]);
  }

  // 注文記録
  await db.query('INSERT INTO orders ...');

  // メール送信
  await emailService.send({
    to: order.customerEmail,
    subject: '注文確認',
    body: `ご注文ありがとうございます。合計金額: ${totalWithTax}円`,
  });

  return { orderId: 'xxx', total: totalWithTax };
}
// Good: 各ステップを関数に分離
async function processOrder(order) {
  validateOrder(order);
  await verifyInventory(order.items);

  const totalWithTax = await calculateOrderTotal(order.items);
  await processPayment(order.customerId, totalWithTax);
  await updateInventory(order.items);

  const orderId = await saveOrder(order, totalWithTax);
  await sendOrderConfirmation(order.customerEmail, orderId, totalWithTax);

  return { orderId, total: totalWithTax };
}

function validateOrder(order) {
  if (!order.items || order.items.length === 0) {
    throw new OrderValidationError('注文に商品がありません');
  }
  if (!order.customerId) {
    throw new OrderValidationError('顧客IDが必要です');
  }
}

async function verifyInventory(items) {
  for (const item of items) {
    const available = await inventoryService.getStock(item.productId);
    if (available < item.quantity) {
      throw new InsufficientStockError(item.productId, item.quantity, available);
    }
  }
}

async function calculateOrderTotal(items) {
  const subtotal = await calculateSubtotal(items);
  const tax = calculateTax(subtotal);
  return subtotal + tax;
}

分割後のprocessOrder関数は、処理の全体像を一目で把握できます。各ステップの詳細を知りたい場合は、対応する関数を読めばよいのです。この「抽象度の階層化」がクリーンコードの核心です。

関数の引数は少なく

// Bad: 引数が多すぎる
function createUser(name, email, age, role, department, phone, address) { ... }

// Good: オブジェクトにまとめる
function createUser({ name, email, age, role, department, phone, address }) { ... }

// 呼び出し側も読みやすくなる
createUser({
  name: '田中太郎',
  email: 'tanaka@example.com',
  age: 30,
  role: 'member',
  department: '開発部',
});

関数の引数は理想的には0〜2個、最大でも3個までに抑えます。それ以上必要な場合はオブジェクトにまとめます。

ルール3:コメントはコードで表現できないことだけに書く

コメントは「なぜ」を説明するために書き、「何を」しているかはコード自体で表現します。

悪いコメントと良いコメント

// Bad: コードを繰り返しているだけのコメント
// ユーザーの年齢を取得する
const age = user.age;

// カウンターを1増やす
counter++;

// ユーザーが管理者かチェックする
if (user.role === 'admin') { ... }

// Bad: 古くなって嘘になっているコメント
// 最大3回までリトライする(実際のコードは5回リトライしている)
const MAX_RETRIES = 5;
// Good: 「なぜ」を説明するコメント
// 消費税法改正(2026年施行)に対応するため、税率を10%から12%に変更
const TAX_RATE = 0.12;

// レガシーシステムのAPIが UTC+0 で返してくるため、JST に変換が必要
const jstDate = convertToJST(apiResponse.timestamp);

// 同時実行数を5に制限。これ以上増やすと外部APIのレート制限に抵触する
const CONCURRENCY_LIMIT = 5;

// パフォーマンス最適化:N+1クエリを避けるためバッチ取得
const userMap = await fetchUsersByIds(userIds);

コメントの代わりにコードを改善する

// Bad: コメントで説明が必要な条件分岐
// 休日で、かつ勤務時間外で、かつ緊急でない場合
if ((day === 0 || day === 6) && (hour < 9 || hour > 18) && priority !== 'critical') {
  // 通知を送らない
}

// Good: 条件を関数として抽出
function shouldSuppressNotification(day, hour, priority) {
  const isWeekend = day === 0 || day === 6;
  const isOutsideBusinessHours = hour < 9 || hour > 18;
  const isNonCritical = priority !== 'critical';

  return isWeekend && isOutsideBusinessHours && isNonCritical;
}

if (shouldSuppressNotification(day, hour, priority)) {
  return;
}

コメントが必要だと感じたら、まずコードを改善してコメントなしで伝わるようにできないか考えましょう。それでも伝えきれない「なぜ」がある場合にコメントを書きます。

ルール4:適切な抽象度でコードを整理する

1つの関数やクラスの中で、抽象度を統一することが重要です。

抽象度の不統一を解消する

// Bad: 高レベルの処理と低レベルの処理が混在
async function registerUser(userData) {
  // 高レベル: バリデーション
  if (!isValidEmail(userData.email)) {
    throw new Error('無効なメールアドレスです');
  }

  // 低レベル: SQLの直接操作
  const existingUser = await db.query(
    'SELECT id FROM users WHERE email = $1',
    [userData.email]
  );
  if (existingUser.rows.length > 0) {
    throw new Error('このメールアドレスは既に登録されています');
  }

  // 低レベル: パスワードのハッシュ化
  const salt = await bcrypt.genSalt(10);
  const hashedPassword = await bcrypt.hash(userData.password, salt);

  // 低レベル: SQL INSERT
  const result = await db.query(
    'INSERT INTO users (name, email, password_hash) VALUES ($1, $2, $3) RETURNING id',
    [userData.name, userData.email, hashedPassword]
  );

  // 高レベル: メール送信
  await sendWelcomeEmail(userData.email, userData.name);

  return result.rows[0].id;
}
// Good: 抽象度を統一
async function registerUser(userData) {
  validateUserData(userData);
  await ensureEmailNotTaken(userData.email);

  const userId = await createUser(userData);
  await sendWelcomeEmail(userData.email, userData.name);

  return userId;
}

// 低レベルの詳細は別の関数に委譲
function validateUserData(userData) {
  if (!isValidEmail(userData.email)) {
    throw new ValidationError('無効なメールアドレスです');
  }
  if (userData.password.length < 8) {
    throw new ValidationError('パスワードは8文字以上必要です');
  }
}

async function ensureEmailNotTaken(email) {
  const exists = await userRepository.existsByEmail(email);
  if (exists) {
    throw new ConflictError('このメールアドレスは既に登録されています');
  }
}

async function createUser(userData) {
  const hashedPassword = await hashPassword(userData.password);
  return userRepository.create({
    name: userData.name,
    email: userData.email,
    passwordHash: hashedPassword,
  });
}

リファクタリング後のregisterUserは、すべてのステップが同じ抽象度(「何をするか」のレベル)で記述されています。SQLの操作やハッシュ化の詳細は、それぞれ専用の関数に閉じ込められています。

ルール5:エラーハンドリングを適切に行う

エラーハンドリングは、堅牢なコードの基盤です。

カスタムエラークラスの活用

// Bad: 汎用的なErrorだけを使う
throw new Error('ユーザーが見つかりません');
throw new Error('権限がありません');
throw new Error('入力値が不正です');

// catch側で文字列比較が必要になり、脆弱
try {
  await getUser(id);
} catch (error) {
  if (error.message === 'ユーザーが見つかりません') { // 文字列の比較は危険
    res.status(404).json({ ... });
  }
}
// Good: カスタムエラークラスを定義
class AppError extends Error {
  constructor(message, statusCode, code) {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
    this.name = this.constructor.name;
  }
}

class NotFoundError extends AppError {
  constructor(resource, id) {
    super(`${resource}(ID: ${id})が見つかりません`, 404, 'NOT_FOUND');
  }
}

class ForbiddenError extends AppError {
  constructor(action) {
    super(`${action}の権限がありません`, 403, 'FORBIDDEN');
  }
}

class ValidationError extends AppError {
  constructor(message, details = []) {
    super(message, 400, 'VALIDATION_ERROR');
    this.details = details;
  }
}

// 使用側
async function getUser(id) {
  const user = await userRepository.findById(id);
  if (!user) {
    throw new NotFoundError('ユーザー', id);
  }
  return user;
}

// catch側はinstanceofで型判定
try {
  const user = await getUser(id);
} catch (error) {
  if (error instanceof NotFoundError) {
    res.status(404).json({ code: error.code, message: error.message });
  } else if (error instanceof ForbiddenError) {
    res.status(403).json({ code: error.code, message: error.message });
  } else {
    // 予期しないエラー
    res.status(500).json({ code: 'INTERNAL_ERROR', message: '内部エラーが発生しました' });
  }
}

早期リターンパターン

// Bad: ネストが深くなるif文の連鎖
function processPayment(order, user) {
  if (order) {
    if (order.items.length > 0) {
      if (user) {
        if (user.isActive) {
          if (user.balance >= order.total) {
            // ここでやっと本来の処理
            return executePayment(order, user);
          } else {
            throw new Error('残高不足');
          }
        } else {
          throw new Error('アカウントが無効');
        }
      } else {
        throw new Error('ユーザーが不正');
      }
    } else {
      throw new Error('注文が空');
    }
  } else {
    throw new Error('注文が不正');
  }
}

// Good: ガード節(早期リターン)でフラットに
function processPayment(order, user) {
  if (!order) throw new ValidationError('注文が不正です');
  if (order.items.length === 0) throw new ValidationError('注文が空です');
  if (!user) throw new ValidationError('ユーザーが不正です');
  if (!user.isActive) throw new ForbiddenError('アカウントが無効です');
  if (user.balance < order.total) throw new InsufficientBalanceError(user.balance, order.total);

  return executePayment(order, user);
}

ガード節を使うことで、ネストが解消され、異常系の処理と正常系の処理が明確に分離されます。

ルール6:DRY原則を意識しつつ、過度な共通化を避ける

DRY(Don't Repeat Yourself)原則は重要ですが、誤った適用は逆効果になります。

適切なDRYの適用

// Bad: 同じロジックが複数箇所に散在
function calculateEmployeeSalary(employee) {
  const baseSalary = employee.baseSalary;
  const bonus = baseSalary * 0.1; // ボーナス計算が重複
  const tax = (baseSalary + bonus) * 0.2; // 税計算が重複
  return baseSalary + bonus - tax;
}

function calculateContractorPayment(contractor) {
  const basePayment = contractor.hourlyRate * contractor.hours;
  const bonus = basePayment * 0.1; // ボーナス計算が重複
  const tax = (basePayment + bonus) * 0.2; // 税計算が重複
  return basePayment + bonus - tax;
}

// Good: 共通ロジックを抽出
function calculateBonus(amount, rate = 0.1) {
  return amount * rate;
}

function calculateTax(amount, taxRate = 0.2) {
  return amount * taxRate;
}

function calculateEmployeeSalary(employee) {
  const baseSalary = employee.baseSalary;
  const bonus = calculateBonus(baseSalary);
  const tax = calculateTax(baseSalary + bonus);
  return baseSalary + bonus - tax;
}

function calculateContractorPayment(contractor) {
  const basePayment = contractor.hourlyRate * contractor.hours;
  const bonus = calculateBonus(basePayment);
  const tax = calculateTax(basePayment + bonus);
  return basePayment + bonus - tax;
}

過度な共通化の罠

// Bad: 異なる概念を無理に共通化している
function formatEntity(entity, type) {
  if (type === 'user') {
    return `${entity.firstName} ${entity.lastName} (${entity.email})`;
  } else if (type === 'product') {
    return `${entity.name} - ¥${entity.price}`;
  } else if (type === 'order') {
    return `注文#${entity.id} (${entity.status})`;
  }
  // typeが増えるたびにif文が増殖する
}

// Good: それぞれの概念に固有の関数を持つ
function formatUserDisplay(user) {
  return `${user.firstName} ${user.lastName} (${user.email})`;
}

function formatProductDisplay(product) {
  return `${product.name} - ¥${product.price.toLocaleString()}`;
}

function formatOrderDisplay(order) {
  return `注文#${order.id} (${order.status})`;
}

「似ている」だけで共通化すると、後から要件が分岐したときに条件分岐だらけの複雑な関数になります。「同じ知識の重複」を排除するのがDRYの本質であり、「たまたま似ているコード」の共通化とは異なります。

ルール7:テストしやすいコードを書く

テストしやすいコードは、自然とクリーンなコードになります。

依存関係の注入(Dependency Injection)

// Bad: 外部依存が内部にハードコードされている
class OrderService {
  async createOrder(orderData) {
    // データベースに直接依存
    const order = await db.query('INSERT INTO orders ...');

    // メールサービスに直接依存
    await sendgrid.send({
      to: orderData.email,
      subject: '注文確認',
    });

    // 決済サービスに直接依存
    await stripe.charges.create({
      amount: orderData.total,
    });

    return order;
  }
}
// テスト時に実際のDB、メール送信、決済が実行されてしまう
// Good: 依存関係を外部から注入
class OrderService {
  constructor(orderRepository, emailService, paymentService) {
    this.orderRepository = orderRepository;
    this.emailService = emailService;
    this.paymentService = paymentService;
  }

  async createOrder(orderData) {
    await this.paymentService.charge(orderData.customerId, orderData.total);
    const order = await this.orderRepository.create(orderData);
    await this.emailService.sendOrderConfirmation(orderData.email, order.id);
    return order;
  }
}

// テスト時はモックを注入できる
describe('OrderService', () => {
  it('注文を作成し、メールを送信すること', async () => {
    const mockRepo = { create: jest.fn().mockResolvedValue({ id: '1' }) };
    const mockEmail = { sendOrderConfirmation: jest.fn().mockResolvedValue(true) };
    const mockPayment = { charge: jest.fn().mockResolvedValue(true) };

    const service = new OrderService(mockRepo, mockEmail, mockPayment);
    const result = await service.createOrder({
      email: 'test@example.com',
      total: 1000,
      customerId: 'cust_1',
    });

    expect(result.id).toBe('1');
    expect(mockPayment.charge).toHaveBeenCalledWith('cust_1', 1000);
    expect(mockEmail.sendOrderConfirmation).toHaveBeenCalledWith('test@example.com', '1');
  });
});

純粋関数を増やす

// Bad: 外部状態に依存し、副作用がある
let discountRate = 0.1;

function calculateDiscount(price) {
  const discount = price * discountRate; // 外部変数に依存
  console.log(`割引額: ${discount}`);   // 副作用(コンソール出力)
  return discount;
}

// Good: 純粋関数(同じ入力に対して常に同じ出力)
function calculateDiscount(price, discountRate) {
  return price * discountRate;
}

// テストが簡単
expect(calculateDiscount(1000, 0.1)).toBe(100);
expect(calculateDiscount(2000, 0.2)).toBe(400);

純粋関数は入力だけに依存し、外部状態を変更しないため、テストが非常に簡単です。ビジネスロジックはできるだけ純粋関数として実装し、外部とのやり取り(DB、API、ファイル)は境界層に押し出しましょう。

まとめ

クリーンコードは、チーム全体の開発生産性を長期的に向上させる投資です。本記事で紹介した7つのルールを振り返りましょう。

ルール1:意図が伝わる名前を付ける
変数名、関数名、クラス名は「読むだけで意図がわかる」ように命名します。略語や曖昧な名前は避けましょう。

ルール2:関数は小さく、1つのことだけを行う
1つの関数は1つの責務に限定し、抽象度を階層化して全体像を把握しやすくします。

ルール3:コメントはコードで表現できないことだけに書く
「何をしているか」はコードで表現し、「なぜそうしているか」をコメントで補足します。

ルール4:適切な抽象度でコードを整理する
1つの関数内の処理は同じ抽象レベルに統一し、詳細は下位の関数に委譲します。

ルール5:エラーハンドリングを適切に行う
カスタムエラークラスと早期リターンにより、堅牢で読みやすいエラー処理を実現します。

ルール6:DRY原則を意識しつつ、過度な共通化を避ける
「同じ知識」の重複は排除しますが、「たまたま似ているコード」を無理に共通化しないようにします。

ルール7:テストしやすいコードを書く
依存関係の注入と純粋関数の活用により、テスト容易性と保守性を同時に向上させます。

クリーンコードは一朝一夕では身に付きません。日々のコーディングで少しずつ意識し、コードレビューでチームメンバーと議論を重ねることで、チーム全体のコード品質が徐々に向上していきます。まずは今日書くコードから、1つのルールを意識してみてください。

#クリーンコード#設計#リファクタリング
共有:
無料メルマガ

週1回、最新の技術記事をお届け

AI・クラウド・開発の最新記事を毎週月曜にメールでお届けします。登録は無料、いつでも解除できます。

プライバシーポリシーに基づき管理します

AI活用のヒントをお探しですか?お気軽にご相談ください。

まずは話だけ聞いてもらう