Skip to content
Published on

クリーンコード実践原則2025:読みやすいコードを書くための30のルール

Authors

はじめに

「動(うご)くコード」と「読(よ)みやすいコード」の間(あいだ)には、越(こ)えられないような隔(へだ)たりがあります。多(おお)くの開発者(かいはつしゃ)は機能(きのう)実装(じっそう)に集中(しゅうちゅう)するあまりコード品質(ひんしつ)を後回(あとまわ)しにし、結局(けっきょく)メンテナンスコストが雪(ゆき)だるま式(しき)に膨(ふく)れ上(あ)がる経験(けいけん)をします。

Robert C. Martin(Uncle Bob)の「Clean Code」が出版(しゅっぱん)されてから17年(ねん)が経(た)ちましたが、その原則(げんそく)は今(いま)でも有効(ゆうこう)です。ただし、TypeScript、Python、Rustなどのモダンな言語(げんご)やツールの登場(とうじょう)により、実践(じっせん)方法(ほうほう)は進化(しんか)しています。

この記事(きじ)では、30の実践(じっせん)ルールを6つのカテゴリに分(わ)けて、それぞれ**Bad/Goodコード比較(ひかく)**で説明(せつめい)します。すべての例(れい)はTypeScriptとPythonで提供(ていきょう)されます。

ボーイスカウトルール(Boy Scout Rule): コードを見(み)つけた時(とき)よりもきれいな状態(じょうたい)で残(のこ)せ。


カテゴリ1:命名(めいめい)(Naming) — 6つのルール

ルール1:意図(いと)を伝(つた)える名前(なまえ)を使(つか)え

変数(へんすう)、関数(かんすう)、クラスの名前(なまえ)だけで「何(なに)をするか」「なぜ存在(そんざい)するか」が分(わ)かるべきです。

Bad:

const d: number = 86400;
const list1: number[] = [1, 2, 3];

function calc(a: number, b: number): number {
  return a * b * 0.1;
}

Good:

const SECONDS_PER_DAY: number = 86400;
const activeUserIds: number[] = [1, 2, 3];

function calculateDiscountedPrice(originalPrice: number, quantity: number): number {
  const DISCOUNT_RATE = 0.1;
  return originalPrice * quantity * DISCOUNT_RATE;
}

Python版(ばん):

# Bad
d = 86400
lst = [1, 2, 3]

def calc(a, b):
    return a * b * 0.1

# Good
SECONDS_PER_DAY = 86400
active_user_ids = [1, 2, 3]

def calculate_discounted_price(original_price: float, quantity: int) -> float:
    DISCOUNT_RATE = 0.1
    return original_price * quantity * DISCOUNT_RATE

ルール2:略語(りゃくご)を避(さ)けよ

略語(りゃくご)は作成者(さくせいしゃ)にのみ明確(めいかく)で、他(ほか)の開発者(かいはつしゃ)には謎(なぞ)です。

Bad:

const usrMgr = new UserManager();
const btnClkHndlr = () => {};
const genRpt = (dt: Date) => {};

Good:

const userManager = new UserManager();
const handleButtonClick = () => {};
const generateReport = (reportDate: Date) => {};

許容(きょよう)される略語(りゃくご): 業界(ぎょうかい)標準(ひょうじゅん)のみ — id, url, api, db, io, html, css, http

ルール3:検索(けんさく)可能(かのう)な名前(なまえ)を使(つか)え

マジックナンバーや1文字(もじ)変数(へんすう)はプロジェクト全体(ぜんたい)で検索(けんさく)できません。

Bad:

if (user.role === 5) {
  setTimeout(callback, 86400000);
}

Good:

const ADMIN_ROLE_ID = 5;
const ONE_DAY_IN_MILLISECONDS = 86400000;

if (user.role === ADMIN_ROLE_ID) {
  setTimeout(callback, ONE_DAY_IN_MILLISECONDS);
}

ルール4:発音(はつおん)できる名前(なまえ)を使(つか)え

コードレビューで名前(なまえ)を声(こえ)に出(だ)して読(よ)めるべきです。

Bad:

class DtaRcrd102 {
  private genymdhms: Date;
  private modymdhms: Date;
  private pszqint: number;
}

Good:

class Customer {
  private generationTimestamp: Date;
  private modificationTimestamp: Date;
  private recordId: number;
}

ルール5:一貫(いっかん)した語彙(ごい)を使(つか)え

同(おな)じ概念(がいねん)に複数(ふくすう)の単語(たんご)を混用(こんよう)しないでください。

Bad:

class UserService {
  fetchUser(id: number) {}
  retrieveAccount(id: number) {}
  getUserProfile(id: number) {}
  obtainUserSettings(id: number) {}
}

Good:

class UserService {
  getUser(id: number) {}
  getAccount(id: number) {}
  getUserProfile(id: number) {}
  getUserSettings(id: number) {}
}

チーム命名(めいめい)規約(きやく)の例(れい):

アクション規約(きやく)例(れい)
取得(しゅとく)(単件(たんけん))get + 名詞(めいし)getUser, getOrder
取得(しゅとく)(一覧(いちらん))list + 名詞(めいし)複数形(ふくすうけい)listUsers, listOrders
作成(さくせい)create + 名詞(めいし)createUser, createOrder
更新(こうしん)update + 名詞(めいし)updateUser, updateOrder
削除(さくじょ)delete + 名詞(めいし)deleteUser, deleteOrder
存在(そんざい)確認(かくにん)has/is + 形容詞(けいようし)/名詞(めいし)hasPermission, isActive
変換(へんかん)to + 目的形態(もくてきけいたい)toJSON, toString

ルール6:スコープの長(なが)さに比例(ひれい)する名前(なまえ)の長(なが)さ

短(みじか)いスコープでは短(みじか)い名前(なまえ)を、広(ひろ)いスコープでは説明的(せつめいてき)な名前(なまえ)を使(つか)います。

Bad:

for (let arrayIndex = 0; arrayIndex < items.length; arrayIndex++) {
  process(items[arrayIndex]);
}

const T = 3000;

Good:

for (let i = 0; i < items.length; i++) {
  process(items[i]);
}

const API_REQUEST_TIMEOUT_MS = 3000;

カテゴリ2:関数(かんすう)(Functions) — 6つのルール

ルール7:関数(かんすう)は小(ちい)さく作(つく)れ

関数(かんすう)の理想的(りそうてき)な長(なが)さは20行(ぎょう)以内(いない)です。

Bad:

function processOrder(order: Order): OrderResult {
  // 検証 (20行)
  if (!order.items || order.items.length === 0) throw new Error('No items');
  if (!order.customer) throw new Error('No customer');

  // 価格計算 (30行)
  let total = 0;
  for (const item of order.items) {
    total += item.price * item.quantity;
  }
  const tax = total * 0.1;
  const shipping = total > 100 ? 0 : 10;

  // 決済処理 (20行)
  const paymentResult = chargeCard(order.customer.cardId, total + tax + shipping);

  // 通知送信 (15行)
  sendEmail(order.customer.email, 'Order confirmed');

  return { orderId: generateId(), total, status: 'completed' };
}

Good:

function processOrder(order: Order): OrderResult {
  validateOrder(order);
  const pricing = calculatePricing(order.items);
  const paymentResult = processPayment(order.customer, pricing.total);
  notifyCustomer(order.customer, paymentResult);

  return createOrderResult(paymentResult, pricing);
}

function validateOrder(order: Order): void {
  if (!order.items?.length) throw new InvalidOrderError('No items');
  if (!order.customer) throw new InvalidOrderError('No customer');
}

function calculatePricing(items: OrderItem[]): Pricing {
  const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const tax = subtotal * TAX_RATE;
  const shipping = subtotal > FREE_SHIPPING_THRESHOLD ? 0 : SHIPPING_FEE;
  return { subtotal, tax, shipping, total: subtotal + tax + shipping };
}

ルール8:一(ひと)つのことだけをせよ(Single Responsibility)

関数(かんすう)は一(ひと)つのことだけをすべきで、それをうまくやるべきです。

Bad:

function emailClients(clients: Client[]): void {
  for (const client of clients) {
    const record = database.lookup(client.id);
    if (record.isActive()) {
      email(client.email, 'Welcome!');
    }
  }
}

Good:

function emailActiveClients(clients: Client[]): void {
  const activeClients = getActiveClients(clients);
  activeClients.forEach(client => sendWelcomeEmail(client));
}

function getActiveClients(clients: Client[]): Client[] {
  return clients.filter(client => isClientActive(client.id));
}

function isClientActive(clientId: number): boolean {
  return database.lookup(clientId).isActive();
}

function sendWelcomeEmail(client: Client): void {
  email(client.email, 'Welcome!');
}

ルール9:パラメータは少(すく)なく保(たも)て(理想(りそう)は2つ以下(いか))

パラメータが3つ以上(いじょう)の場合(ばあい)はオブジェクトにまとめて渡(わた)しましょう。

Bad:

function createUser(
  name: string, email: string, age: number,
  role: string, department: string, isActive: boolean
): User { /* ... */ }

createUser('Alice', 'alice@example.com', 30, 'admin', 'engineering', true);

Good:

interface CreateUserParams {
  name: string;
  email: string;
  age: number;
  role: UserRole;
  department: string;
  isActive: boolean;
}

function createUser(params: CreateUserParams): User { /* ... */ }

createUser({
  name: 'Alice',
  email: 'alice@example.com',
  age: 30,
  role: UserRole.ADMIN,
  department: 'engineering',
  isActive: true,
});

ルール10:副作用(ふくさよう)を避(さ)けよ

関数(かんすう)が予想外(よそうがい)の動作(どうさ)をしてはいけません。

Bad:

let currentUser: User | null = null;

function checkPassword(userName: string, password: string): boolean {
  const user = database.findUser(userName);
  if (user && user.password === hash(password)) {
    // 副作用!パスワード確認関数がセッションを初期化する
    currentUser = user;
    initializeSession(user);
    return true;
  }
  return false;
}

Good:

function verifyPassword(userName: string, password: string): boolean {
  const user = database.findUser(userName);
  return user !== null && user.password === hash(password);
}

function login(userName: string, password: string): LoginResult {
  if (!verifyPassword(userName, password)) {
    return { success: false };
  }
  const user = database.findUser(userName);
  const session = initializeSession(user);
  return { success: true, user, session };
}

ルール11:コマンド・クエリ分離(ぶんり)(Command-Query Separation)

関数(かんすう)は「何(なに)かを実行(じっこう)する」か「何(なに)かを返(かえ)す」かのどちらかで、両方(りょうほう)してはいけません。

Bad:

function setAndCheckAttribute(name: string, value: string): boolean {
  const attribute = findAttribute(name);
  if (attribute) {
    attribute.value = value;
    return true;
  }
  return false;
}

if (setAndCheckAttribute('username', 'alice')) { /* ??? */ }

Good:

function attributeExists(name: string): boolean {
  return findAttribute(name) !== null;
}

function setAttribute(name: string, value: string): void {
  const attribute = findAttribute(name);
  if (!attribute) throw new AttributeNotFoundError(name);
  attribute.value = value;
}

if (attributeExists('username')) {
  setAttribute('username', 'alice');
}

ルール12:DRY(Don't Repeat Yourself)

重複(じゅうふく)コードはバグの温床(おんしょう)です。

Bad:

function getAdminUsers(): User[] {
  const users = database.query('SELECT * FROM users');
  return users
    .filter(u => u.role === 'admin')
    .map(u => ({ ...u, displayName: `${u.firstName} ${u.lastName}` }))
    .sort((a, b) => a.displayName.localeCompare(b.displayName));
}

function getModeratorUsers(): User[] {
  const users = database.query('SELECT * FROM users');
  return users
    .filter(u => u.role === 'moderator')
    .map(u => ({ ...u, displayName: `${u.firstName} ${u.lastName}` }))
    .sort((a, b) => a.displayName.localeCompare(b.displayName));
}

Good:

function getUsersByRole(role: UserRole): User[] {
  const users = database.query('SELECT * FROM users WHERE role = ?', [role]);
  return users.map(addDisplayName).sort(byDisplayName);
}

function addDisplayName(user: User): User {
  return { ...user, displayName: `${user.firstName} ${user.lastName}` };
}

function byDisplayName(a: User, b: User): number {
  return a.displayName.localeCompare(b.displayName);
}

カテゴリ3:コメント — 4つのルール

ルール13:「なぜ」を説明(せつめい)し、「何(なに)」はコードに任(まか)せよ

Bad:

// ユーザー名を取得する
const userName = user.getName();

// 配列を反復する
for (const item of items) {
  // アイテムを処理する
  processItem(item);
}

Good:

// 外部API制限により最大100件までリクエスト可能 (API docs: https://...)
const MAX_BATCH_SIZE = 100;

// レガシーシステム互換のためISO-8859-1でエンコードが必要
// 新システム移行後UTF-8に変更予定 (JIRA-4521)
const encodedData = encode(data, 'ISO-8859-1');

ルール14:ノイズコメントを避(さ)けよ

情報(じょうほう)を追加(ついか)しないコメントはコードを読(よ)みにくくします。

Bad:

/**
 * コンストラクタ
 */
constructor() {}

/**
 * idを返す
 * @returns id
 */
getId(): number {
  return this.id;
}

Good:

class User {
  constructor(
    private readonly id: number,
    private readonly name: string,
  ) {}

  getId(): number {
    return this.id;
  }
}

ルール15:TODOコメントは標準(ひょうじゅん)形式(けいしき)に従(したが)え

Bad:

// TODO あとで直す
// FIXME これ動かない
// HACK 一時的にこうした

Good:

// TODO(team-auth): OAuth2トークン更新ロジック追加 [JIRA-1234] @2025-Q2
// FIXME(alice): 同時リクエスト時にrace condition発生。分散ロック導入必要 [JIRA-5678]
// HACK: Safari 17.2 flexboxバグワークアラウンド。Safari 18修正後に削除

ルール16:JSDoc/Docstringを活用(かつよう)せよ

TypeScript (JSDoc):

/**
 * 指定期間のユーザー活動統計を計算します。
 *
 * @param userId - 統計を照会するユーザーID
 * @param startDate - 照会開始日(含む)
 * @param endDate - 照会終了日(含む)
 * @returns 活動統計オブジェクト
 * @throws {UserNotFoundError} ユーザーが存在しない場合
 * @throws {InvalidDateRangeError} 開始日が終了日より後の場合
 *
 * @example
 * const stats = await getUserActivityStats('user-123', '2025-01-01', '2025-03-31');
 * console.log(stats.totalLogins); // 45
 */
async function getUserActivityStats(
  userId: string,
  startDate: string,
  endDate: string,
): Promise<ActivityStats> {
  // ...
}

カテゴリ4:エラー処理(しょり) — 4つのルール

ルール17:エラーコードの代(か)わりに例外(れいがい)を使(つか)え

Bad:

function withdraw(account: Account, amount: number): number {
  if (amount > account.balance) return -1;
  if (amount < 0) return -2;
  if (account.isFrozen) return -3;
  account.balance -= amount;
  return 0;
}

Good:

class InsufficientFundsError extends Error {
  constructor(public readonly balance: number, public readonly amount: number) {
    super(`Insufficient funds: balance ${balance}, requested ${amount}`);
    this.name = 'InsufficientFundsError';
  }
}

function withdraw(account: Account, amount: number): void {
  if (amount < 0) throw new InvalidAmountError(amount);
  if (account.isFrozen) throw new AccountFrozenError(account.id);
  if (amount > account.balance) throw new InsufficientFundsError(account.balance, amount);
  account.balance -= amount;
}

ルール18:早(はや)く失敗(しっぱい)せよ(Fail Fast)

不正(ふせい)な状態(じょうたい)をできるだけ早(はや)く検出(けんしゅつ)し、即座(そくざ)に報告(ほうこく)すべきです。

Bad:

function processPayment(orderId: string, amount: number): PaymentResult {
  // ... 100行のロジック ...
  if (!orderId) return { success: false, error: 'Invalid order ID' };
  if (amount < 0) return { success: false, error: 'Invalid amount' };
}

Good:

function processPayment(orderId: string, amount: number): PaymentResult {
  // ガード節:最初に検証
  if (!orderId) throw new InvalidArgumentError('orderId is required');
  if (amount <= 0) throw new InvalidArgumentError('amount must be positive');

  const order = orderRepository.findById(orderId);
  if (!order) throw new OrderNotFoundError(orderId);

  return paymentGateway.charge(order, amount);
}

ルール19:カスタムエラーを定義(ていぎ)せよ

abstract class AppError extends Error {
  abstract readonly statusCode: number;
  abstract readonly isOperational: boolean;

  constructor(message: string, public readonly cause?: Error) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  readonly statusCode = 404;
  readonly isOperational = true;
}

class ValidationError extends AppError {
  readonly statusCode = 400;
  readonly isOperational = true;

  constructor(message: string, public readonly field: string, public readonly value: unknown) {
    super(message);
  }
}

class DatabaseError extends AppError {
  readonly statusCode = 500;
  readonly isOperational = false;  // システムエラー — 運用チームに通知
}

ルール20:汎用(はんよう)例外(れいがい)をcatchするな

Bad:

try {
  const data = await fetchUserData(userId);
  await processData(data);
  await saveToDatabase(data);
} catch (error) {
  console.log('Something went wrong');  // すべて飲み込む
}

Good:

try {
  const data = await fetchUserData(userId);
  await processData(data);
  await saveToDatabase(data);
} catch (error) {
  if (error instanceof NetworkError) {
    logger.warn('Network error', { userId, error });
    await retryWithBackoff(() => fetchUserData(userId));
  } else if (error instanceof ValidationError) {
    logger.error('Invalid user data', { userId, field: error.field });
    throw error;
  } else {
    throw error;  // 予期しないエラーは再スロー
  }
}

カテゴリ5:コード構造化(こうぞうか) — 5つのルール

ルール21:垂直(すいちょく)フォーマットを守(まも)れ

関連(かんれん)するコードは近(ちか)くに、無関係(むかんけい)なコードは空行(くうぎょう)で区切(くぎ)ります。

ルール22:新聞記事(しんぶんきじ)のメタファー

ファイルを上(うえ)から下(した)に読(よ)む時(とき)、抽象度(ちゅうしょうど)が徐々(じょじょ)に下(さ)がるべきです。

// 1. 最初: パブリックインターフェース(見出し)
export class OrderProcessor {
  async processOrder(orderId: string): Promise<OrderResult> {
    const order = await this.validateAndFetchOrder(orderId);
    const pricing = this.calculatePricing(order);
    return this.executePayment(order, pricing);
  }

  // 2. 中間: ビジネスロジック(本文)
  private async validateAndFetchOrder(orderId: string): Promise<Order> {
    const order = await this.orderRepo.findById(orderId);
    if (!order) throw new OrderNotFoundError(orderId);
    this.validateOrderState(order);
    return order;
  }

  private calculatePricing(order: Order): Pricing {
    const subtotal = this.sumItems(order.items);
    const discount = this.applyDiscounts(subtotal, order.coupons);
    const tax = this.calculateTax(subtotal - discount);
    return { subtotal, discount, tax, total: subtotal - discount + tax };
  }

  // 3. 最後: 実装の詳細
  private validateOrderState(order: Order): void { /* ... */ }
  private sumItems(items: OrderItem[]): number { /* ... */ }
  private applyDiscounts(amount: number, coupons: Coupon[]): number { /* ... */ }
  private calculateTax(amount: number): number { /* ... */ }
}

ルール23:Import整理(せいり)

// 1. Node.js組み込みモジュール
import { readFile } from 'fs/promises';
import path from 'path';

// 2. 外部ライブラリ (node_modules)
import express from 'express';
import { z } from 'zod';

// 3. 内部モジュール — 絶対パス
import { AppConfig } from '@/config';
import { Logger } from '@/utils/logger';

// 4. 内部モジュール — 相対パス
import { UserService } from './services/user';

// 5. 型Import (TypeScript)
import type { User, Order } from './types';

ルール24:ファイルサイズを制限(せいげん)せよ

基準(きじゅん)推奨(すいしょう)警告(けいこく)危険(きけん)
ファイル行数(ぎょうすう)200行(ぎょう)以下(いか)200〜400行(ぎょう)400行(ぎょう)以上(いじょう)
関数(かんすう)行数(ぎょうすう)20行(ぎょう)以下(いか)20〜50行(ぎょう)50行(ぎょう)以上(いじょう)
クラスメソッド数(すう)10個(こ)以下(いか)10〜20個(こ)20個(こ)以上(いじょう)
パラメータ数(すう)2個(こ)以下(いか)3〜4個(こ)5個(こ)以上(いじょう)
ネスト深度(しんど)2段階(だんかい)以下(いか)3段階(だんかい)4段階(だんかい)以上(いじょう)

ルール25:モジュール境界(きょうかい)を明確(めいかく)にせよ

src/
├── modules/
│   ├── user/
│   │   ├── index.ts          # パブリックAPIのみexport
│   │   ├── user.service.ts
│   │   ├── user.repository.ts
│   │   └── __tests__/
│   ├── order/
│   │   ├── index.ts
│   │   └── ...
│   └── payment/
│       ├── index.ts
│       └── ...
├── shared/
│   ├── errors/
│   └── utils/
└── infrastructure/
    ├── database/
    └── cache/
// modules/user/index.ts — パブリックAPIのみ公開
export { UserService } from './user.service';
export type { User, CreateUserDTO } from './user.types';
// UserRepositoryはexportしない — 内部実装

カテゴリ6:テスト — 5つのルール

ルール26:FIRST原則(げんそく)

原則(げんそく)説明(せつめい)
Fastテストは高速(こうそく)に実行(じっこう)される(ミリ秒(びょう)単位(たんい))
Independentテスト間(かん)に依存(いぞん)関係(かんけい)がない
Repeatableどの環境(かんきょう)でも同(おな)じ結果(けっか)
Self-validatingPass/Failが自動(じどう)で判定(はんてい)される
Timelyプロダクションコードの直前(ちょくぜん)/直後(ちょくご)に作成(さくせい)

ルール27:AAAパターン(Arrange-Act-Assert)

describe('UserService', () => {
  describe('createUser', () => {
    it('should create user with valid data', async () => {
      // Arrange — テストデータ準備
      const userRepo = new InMemoryUserRepository();
      const service = new UserService(userRepo);
      const userData: CreateUserDTO = {
        name: 'Alice',
        email: 'alice@example.com',
      };

      // Act — テスト対象実行
      const result = await service.createUser(userData);

      // Assert — 結果検証
      expect(result.name).toBe('Alice');
      expect(result.email).toBe('alice@example.com');
      expect(result.id).toBeDefined();
    });
  });
});

ルール28:1つのテストに1つのAssert概念(がいねん)

Bad:

it('should handle user lifecycle', async () => {
  const user = await service.createUser(data);
  expect(user.id).toBeDefined();
  const fetched = await service.getUser(user.id);
  expect(fetched.name).toBe(data.name);
  await service.updateUser(user.id, { name: 'Bob' });
  const updated = await service.getUser(user.id);
  expect(updated.name).toBe('Bob');
  await service.deleteUser(user.id);
  await expect(service.getUser(user.id)).rejects.toThrow();
});

Good:

describe('UserService', () => {
  it('should create user and return with generated id', async () => {
    const user = await service.createUser(validUserData);
    expect(user.id).toBeDefined();
    expect(user.name).toBe(validUserData.name);
  });

  it('should retrieve existing user by id', async () => {
    const created = await service.createUser(validUserData);
    const fetched = await service.getUser(created.id);
    expect(fetched).toEqual(created);
  });

  it('should throw NotFoundError when getting deleted user', async () => {
    const user = await service.createUser(validUserData);
    await service.deleteUser(user.id);
    await expect(service.getUser(user.id)).rejects.toThrow(NotFoundError);
  });
});

ルール29:読(よ)みやすいテストを書(か)け

Bad:

it('test1', async () => {
  const r = await s.calc({ a: 100, b: 2, c: 0.1, d: true });
  expect(r).toBe(180);
});

Good:

it('should apply 10% discount when premium member orders 2 items at 100 each', async () => {
  // Given
  const order = createOrder({
    items: [{ price: 100, quantity: 2 }],
    isPremiumMember: true,
    discountRate: 0.1,
  });

  // When
  const totalPrice = await pricingService.calculateTotal(order);

  // Then
  expect(totalPrice).toBe(180); // (100 * 2) - 10% discount = 180
});

ルール30:テスト名(めい)はシナリオを説明(せつめい)せよ

命名(めいめい)パターン: should [期待結果] when [条件]

describe('PasswordValidator', () => {
  it('should return valid when password has 8+ chars with mixed case and numbers', () => {});
  it('should return invalid when password is shorter than 8 characters', () => {});
  it('should return invalid when password lacks uppercase letters', () => {});
  it('should throw EmptyPasswordError when password is empty string', () => {});
});

コードスメルカタログ

Martin Fowlerのリファクタリングで定義(ていぎ)された代表的(だいひょうてき)なコードスメルとその解決策(かいけつさく)です。

コードスメル症状(しょうじょう)解決策(かいけつさく)
Long Method関数(かんすう)が50行(ぎょう)以上(いじょう)Extract Method
Large Classクラスの責任(せきにん)が3つ以上(いじょう)Extract Class
Primitive Obsessionプリミティブ型(がた)のみでドメインを表現(ひょうげん)Value Object導入(どうにゅう)
Long Parameter Listパラメータ4つ以上(いじょう)Parameter Object
Data Clumps複数箇所(ふくすうかしょ)で同(おな)じデータグループExtract Class
Feature Envy他(ほか)のクラスのデータを過度(かど)に使用(しよう)Move Method
Shotgun Surgery1つの変更(へんこう)が複数(ふくすう)クラスに影響(えいきょう)Move Method
Dead Code使(つか)われていないコード削除(さくじょ)
Duplicated Code同(おな)じコードが2箇所(かしょ)以上(いじょう)Extract Method

リファクタリング技法(ぎほう) Before/After

技法(ぎほう)1:ガード節(せつ)でネスト除去(じょきょ)

Before:

function getPaymentAmount(employee: Employee): number {
  let result: number;
  if (employee.isSeparated) {
    result = separatedAmount();
  } else {
    if (employee.isRetired) {
      result = retiredAmount();
    } else {
      result = normalPayAmount();
    }
  }
  return result;
}

After:

function getPaymentAmount(employee: Employee): number {
  if (employee.isSeparated) return separatedAmount();
  if (employee.isRetired) return retiredAmount();
  return normalPayAmount();
}

技法(ぎほう)2:ストラテジーパターンで条件文(じょうけんぶん)除去(じょきょ)

Before:

function calculateShippingCost(order: Order): number {
  if (order.shippingMethod === 'standard') return order.weight * 1.5;
  else if (order.shippingMethod === 'express') return order.weight * 3.0 + 5;
  else if (order.shippingMethod === 'overnight') return order.weight * 5.0 + 15;
  throw new Error('Unknown shipping method');
}

After:

interface ShippingStrategy {
  calculate(weight: number): number;
}

const shippingStrategies: Record<string, ShippingStrategy> = {
  standard: { calculate: (w) => w * 1.5 },
  express: { calculate: (w) => w * 3.0 + 5 },
  overnight: { calculate: (w) => w * 5.0 + 15 },
};

function calculateShippingCost(order: Order): number {
  const strategy = shippingStrategies[order.shippingMethod];
  if (!strategy) throw new UnknownShippingMethodError(order.shippingMethod);
  return strategy.calculate(order.weight);
}

技法(ぎほう)3:パイプラインでループリファクタリング

Before:

function getActiveAdminEmails(users: User[]): string[] {
  const result: string[] = [];
  for (let i = 0; i < users.length; i++) {
    if (users[i].isActive) {
      if (users[i].role === 'admin') {
        result.push(users[i].email.toLowerCase());
      }
    }
  }
  result.sort();
  return result;
}

After:

function getActiveAdminEmails(users: User[]): string[] {
  return users
    .filter(user => user.isActive)
    .filter(user => user.role === 'admin')
    .map(user => user.email.toLowerCase())
    .sort();
}

認知的(にんちてき)複雑度(ふくざつど)(Cognitive Complexity)

SonarQubeで測定(そくてい)される認知的(にんちてき)複雑度(ふくざつど)は、コードを「理解(りかい)するために必要(ひつよう)な精神的(せいしんてき)な努力(どりょく)」を数値化(すうちか)します。

推奨(すいしょう)しきい値(ち)

レベル認知的複雑度(にんちてきふくざつど)アクション
良好(りょうこう)0〜5維持(いじ)
警告(けいこく)6〜10リファクタリング検討(けんとう)
危険(きけん)11〜15必(かなら)ずリファクタリング
深刻(しんこく)16以上(いじょう)即座(そくざ)にリファクタリング

SOLID原則(げんそく)クイックリファレンス

S — 単一責任(たんいつせきにん)の原則(げんそく)

// Bad: 1つのクラスが複数の責任
class UserService {
  createUser(data: CreateUserDTO) {}
  sendEmail(to: string, body: string) {}
  generatePDF(user: User) {}
  logAction(action: string) {}
}

// Good: 各クラスが1つの責任
class UserService { createUser(data: CreateUserDTO) {} }
class EmailService { send(to: string, body: string) {} }
class PDFGenerator { generate(user: User) {} }
class AuditLogger { log(action: string) {} }

O — 開放(かいほう)閉鎖(へいさ)の原則(げんそく)

interface PaymentProcessor {
  process(amount: number): Promise<PaymentResult>;
}

class StripeProcessor implements PaymentProcessor {
  async process(amount: number) { /* Stripe API */ }
}

class PayPalProcessor implements PaymentProcessor {
  async process(amount: number) { /* PayPal API */ }
}

// 新しい決済手段の追加は既存コードの修正なしに新クラスを追加するだけ
class CryptoProcessor implements PaymentProcessor {
  async process(amount: number) { /* Crypto API */ }
}

L — リスコフの置換(ちかん)原則(げんそく)

// Bad: ペンギンは飛べないのでBirdを継承すべきでない
class Bird { fly(): void { /* 飛ぶ */ } }
class Penguin extends Bird {
  fly(): void { throw new Error('Cannot fly!'); }
}

// Good: インターフェースを分離
interface FlyingBird { fly(): void; }
interface SwimmingBird { swim(): void; }

class Eagle implements FlyingBird { fly() {} }
class Penguin implements SwimmingBird { swim() {} }

I — インターフェース分離(ぶんり)の原則(げんそく)

// Bad: 太いインターフェース
interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
  attendMeeting(): void;
}

// Good: 分離されたインターフェース
interface Workable { work(): void; }
interface Feedable { eat(): void; }
interface Meetable { attendMeeting(): void; }

class Developer implements Workable, Feedable, Meetable {
  work() {} eat() {} attendMeeting() {}
}

class Robot implements Workable {
  work() {}
}

D — 依存性(いぞんせい)逆転(ぎゃくてん)の原則(げんそく)

// Bad: 高レベルモジュールが低レベルモジュールに直接依存
class OrderService {
  private mysqlDb = new MySQLDatabase();
  save(order: Order) { this.mysqlDb.insert('orders', order); }
}

// Good: 抽象化に依存
interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: string): Promise<Order | null>;
}

class OrderService {
  constructor(private readonly repo: OrderRepository) {}
  async save(order: Order) { await this.repo.save(order); }
}

class MySQLOrderRepository implements OrderRepository { /* ... */ }
class MongoOrderRepository implements OrderRepository { /* ... */ }
class InMemoryOrderRepository implements OrderRepository { /* ... */ }

実践(じっせん)クイズ

クイズ1:このコードの問題点(もんだいてん)は何(なん)ですか?
function proc(lst: any[], flg: boolean): any[] {
  let res: any[] = [];
  for (let i = 0; i < lst.length; i++) {
    if (flg) {
      if (lst[i].a > 0) {
        res.push(lst[i].a * 2);
      }
    } else {
      if (lst[i].b !== null) {
        res.push(lst[i].b);
      }
    }
  }
  return res;
}

問題点(もんだいてん):

  1. 命名(めいめい): proc, lst, flg, res, a, b すべて意味(いみ)のない名前(なまえ)
  2. 型(かた): any型(がた)の乱用(らんよう)
  3. 関数(かんすう)の責任(せきにん): フラグでパラメータの動作(どうさ)が変(か)わるのは2つの関数(かんすう)が必要(ひつよう)というシグナル
  4. ネスト深度(しんど): 3段階(だんかい)のネスト
  5. 命令型(めいれいがた)ループ: filter/mapパイプラインで置(お)き換(か)え可能(かのう)

リファクタリング:

function getDoubledPositiveAmounts(transactions: Transaction[]): number[] {
  return transactions
    .filter(t => t.amount > 0)
    .map(t => t.amount * 2);
}

function getNonNullBalances(accounts: Account[]): number[] {
  return accounts
    .filter(a => a.balance !== null)
    .map(a => a.balance);
}
クイズ2:このエラー処理(しょり)コードを改善(かいぜん)してください。
async function getData(id: string) {
  try {
    const res = await fetch(`/api/data/${id}`);
    const data = await res.json();
    return data;
  } catch (e) {
    console.log(e);
    return null;
  }
}

リファクタリング:

class ApiError extends Error {
  constructor(
    message: string,
    public readonly statusCode: number,
    public readonly endpoint: string,
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

async function fetchDataById(id: string): Promise<UserData> {
  const endpoint = `/api/data/${id}`;
  const response = await fetch(endpoint);

  if (!response.ok) {
    throw new ApiError(
      `Failed to fetch data: ${response.statusText}`,
      response.status,
      endpoint,
    );
  }

  return response.json() as Promise<UserData>;
}
クイズ3:このクラスのSOLID違反(いはん)を見(み)つけてください。
class ReportGenerator {
  generateReport(data: any[]): string {
    if (!data.length) throw new Error('No data');
    let html = '<table>';
    for (const row of data) {
      html += `<tr><td>${row.name}</td><td>${row.value}</td></tr>`;
    }
    html += '</table>';
    fs.writeFileSync('/tmp/report.html', html);
    sendEmail('admin@company.com', 'Report', html);
    return html;
  }
}

違反(いはん)事項(じこう):

  • SRP違反(いはん): 検証(けんしょう)、HTML生成(せいせい)、ファイル保存(ほぞん)、メール送信(そうしん)の4つの責任(せきにん)
  • OCP違反(いはん): PDF形式(けいしき)の追加(ついか)にはクラスの修正(しゅうせい)が必要(ひつよう)
  • DIP違反(いはん): fs、sendEmailに直接(ちょくせつ)依存(いぞん)

リファクタリング:

interface ReportFormatter { format(data: ReportData[]): string; }
interface ReportExporter { export(content: string): Promise<void>; }

class ReportService {
  constructor(
    private formatter: ReportFormatter,
    private exporters: ReportExporter[],
  ) {}

  async generate(data: ReportData[]): Promise<string> {
    if (!data.length) throw new EmptyDataError();
    const content = this.formatter.format(data);
    await Promise.all(this.exporters.map(e => e.export(content)));
    return content;
  }
}
クイズ4:この関数(かんすう)の認知的(にんちてき)複雑度(ふくざつど)を計算(けいさん)してください。
function calculatePrice(product: Product, user: User): number {
  let price = product.basePrice;

  if (user.isPremium) {                         // +1
    if (product.category === 'electronics') {   // +2
      price *= 0.8;
    } else {                                    // +1
      price *= 0.9;
    }
  } else {                                      // +1
    if (user.hasCoupon) {                       // +2
      price *= 0.95;
    }
  }

  if (product.isOnSale && user.isPremium) {     // +1
    price *= 0.9;
  }

  return price;
}

認知的(にんちてき)複雑度(ふくざつど): 8(警告(けいこく)レベル、リファクタリング検討(けんとう)が必要(ひつよう))

リファクタリング:

function calculatePrice(product: Product, user: User): number {
  const basePrice = product.basePrice;
  const memberDiscount = getMemberDiscount(user, product.category);
  const couponDiscount = getCouponDiscount(user);
  const saleDiscount = getSaleDiscount(product, user);

  return basePrice * memberDiscount * couponDiscount * saleDiscount;
}

function getMemberDiscount(user: User, category: string): number {
  if (!user.isPremium) return 1;
  return category === 'electronics' ? 0.8 : 0.9;
}

function getCouponDiscount(user: User): number {
  return !user.isPremium && user.hasCoupon ? 0.95 : 1;
}

function getSaleDiscount(product: Product, user: User): number {
  return product.isOnSale && user.isPremium ? 0.9 : 1;
}
クイズ5:このテストコードを改善(かいぜん)してください。
test('test1', () => {
  const s = new CalcService();
  expect(s.add(1, 2)).toBe(3);
  expect(s.add(-1, 1)).toBe(0);
  expect(s.subtract(5, 3)).toBe(2);
  expect(s.multiply(3, 4)).toBe(12);
  expect(s.divide(10, 2)).toBe(5);
  expect(() => s.divide(1, 0)).toThrow();
});

リファクタリング:

describe('CalcService', () => {
  const calculator = new CalcService();

  describe('add', () => {
    it('should return sum of two positive numbers', () => {
      expect(calculator.add(1, 2)).toBe(3);
    });

    it('should handle negative numbers', () => {
      expect(calculator.add(-1, 1)).toBe(0);
    });
  });

  describe('divide', () => {
    it('should return quotient of two numbers', () => {
      expect(calculator.divide(10, 2)).toBe(5);
    });

    it('should throw DivisionByZeroError when divisor is zero', () => {
      expect(() => calculator.divide(1, 0)).toThrow(DivisionByZeroError);
    });
  });
});

ツールと自動化(じどうか)

ESLint + Prettier設定(せってい)

{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ],
  "rules": {
    "max-lines": ["warn", { "max": 400 }],
    "max-lines-per-function": ["warn", { "max": 50 }],
    "max-params": ["warn", { "max": 3 }],
    "max-depth": ["warn", { "max": 3 }],
    "complexity": ["warn", { "max": 10 }],
    "no-else-return": "error",
    "prefer-const": "error",
    "no-var": "error"
  }
}

SonarQube Quality Gate

sonar.qualitygate.conditions:
  - metric: cognitive_complexity
    operator: GT
    error: 15
  - metric: duplicated_lines_density
    operator: GT
    error: 3
  - metric: coverage
    operator: LT
    error: 80

参考(さんこう)資料(しりょう)

  1. Robert C. Martin, "Clean Code: A Handbook of Agile Software Craftsmanship" (2008)
  2. Martin Fowler, "Refactoring: Improving the Design of Existing Code" (2018, 2nd Edition)
  3. G. Ann Campbell, "Cognitive Complexity: A new way of measuring understandability" — SonarSource (2023)
  4. Google Engineering Practices — Code Review Guidelines
  5. Airbnb JavaScript Style Guide
  6. TypeScript Deep Dive — Style Guide
  7. PEP 8 — Python Style Guide
  8. "The Art of Readable Code" by Dustin Boswell and Trevor Foucher (2011)
  9. SonarQube Documentation — Cognitive Complexity
  10. ESLint Rules Reference
  11. Kent Beck, "Test Driven Development: By Example" (2002)
  12. Michael Feathers, "Working Effectively with Legacy Code" (2004)
  13. "A Philosophy of Software Design" by John Ousterhout (2018)
  14. Clean Code JavaScript — GitHub (ryanmcdermott/clean-code-javascript)