単一責任の原則(SRP)の実践
Single Responsibility Principleを深く理解し、実践的に適用する方法を学びます
単一責任の原則とは
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 += '
Report
'; html += `Total: ${total}
`; html += '- ';
for (const item of data) {
html += `
- ${item.name}: ${item.amount} `; } html += '
Extract Class(クラスの抽出)
関連するデータとメソッドを新しいクラスに移動して、責任を分離します。