単一責任の原則とは

Robert C. Martinの定義

「クラスを変更する理由は1つでなければならない」

これは、各モジュール(クラス、関数、コンポーネント)は、ただ一つのアクターに対して責任を持つべきという意味です。

アクターとは?

  • システムの変更を要求する人やグループ
  • 例:経理部門、営業部門、顧客、管理者など
  • 異なるアクターは異なる変更理由を持つ

責任の粒度を見極める

レベル別の責任分離

1. 関数レベル

❌ 複数の責任を持つ関数
function processUserData(userData) {
    // 検証の責任
    if (!userData.email.includes('@')) {
        throw new Error('Invalid email');
    }
    if (userData.age < 18) {
        throw new Error('Must be 18 or older');
    }
    
    // 変換の責任
    const normalizedData = {
        email: userData.email.toLowerCase(),
        name: userData.name.trim(),
        age: parseInt(userData.age)
    };
    
    // 保存の責任
    database.save('users', normalizedData);
    
    // 通知の責任
    emailService.sendWelcome(normalizedData.email);
    
    return normalizedData;
}
✅ 単一責任の関数群
// それぞれが単一の責任を持つ
function validateUserData(userData) {
    if (!userData.email.includes('@')) {
        throw new Error('Invalid email');
    }
    if (userData.age < 18) {
        throw new Error('Must be 18 or older');
    }
}

function normalizeUserData(userData) {
    return {
        email: userData.email.toLowerCase(),
        name: userData.name.trim(),
        age: parseInt(userData.age)
    };
}

function saveUser(userData) {
    return database.save('users', userData);
}

function sendWelcomeEmail(email) {
    return emailService.sendWelcome(email);
}

// 組み合わせて使用
async function registerUser(userData) {
    validateUserData(userData);
    const normalized = normalizeUserData(userData);
    await saveUser(normalized);
    await sendWelcomeEmail(normalized.email);
    return normalized;
}

2. クラスレベル

クラスレベルでの責任分離は、より大きな粒度での設計判断を必要とします。

// Before: 従業員クラスが複数のアクターに対して責任を持つ
class Employee {
    constructor(name, salary) {
        this.name = name;
        this.salary = salary;
    }
    
    // 経理部門が関心を持つメソッド
    calculatePay() {
        return this.salary / 12;
    }
    
    // 人事部門が関心を持つメソッド
    reportHours() {
        return 40; // 週40時間
    }
    
    // IT部門が関心を持つメソッド
    save() {
        database.save('employees', this);
    }
}

// After: アクターごとに責任を分離
class Employee {
    constructor(name, salary) {
        this.name = name;
        this.salary = salary;
    }
}

class PayrollCalculator {
    calculatePay(employee) {
        return employee.salary / 12;
    }
}

class HourReporter {
    reportHours(employee) {
        return 40;
    }
}

class EmployeeRepository {
    save(employee) {
        return database.save('employees', employee);
    }
}

3. モジュールレベル

モジュール分割の指針

  • ビジネスドメインごとに分割(注文、在庫、配送など)
  • 技術的関心事ごとに分割(データアクセス、認証、ロギングなど)
  • 変更の頻度が異なる部分を分離

SRP違反を見つける方法

コードの匂い(Code Smells)

  • 巨大なクラス/関数:行数が多い場合、複数の責任を持っている可能性が高い
  • 多数の import/require:様々な依存関係は責任の混在を示唆
  • "and"を含むクラス名:UserAndOrder, ValidationAndSaveなど
  • 変更理由の多様性:異なる理由で頻繁に変更される

チェックリスト

  • このクラス/関数の目的を一文で説明できるか?
  • "そして"や"また"を使わずに説明できるか?
  • 異なる理由で変更される部分があるか?
  • テストが複雑になっていないか?

実践的なリファクタリング手法

Extract Method(メソッドの抽出)

// Before
function generateReport(data) {
    let html = '

Report

'; // データの集計 let total = 0; for (const item of data) { total += item.amount; } // HTMLの生成 html += `

Total: ${total}

`; html += '
    '; for (const item of data) { html += `
  • ${item.name}: ${item.amount}
  • `; } html += '
'; return html; } // After function calculateTotal(data) { return data.reduce((sum, item) => sum + item.amount, 0); } function generateHTML(data, total) { let html = '

Report

'; html += `

Total: ${total}

`; html += '
    '; for (const item of data) { html += `
  • ${item.name}: ${item.amount}
  • `; } html += '
'; return html; } function generateReport(data) { const total = calculateTotal(data); return generateHTML(data, total); }

Extract Class(クラスの抽出)

関連するデータとメソッドを新しいクラスに移動して、責任を分離します。