UX / UI のデザインに強いWebシステムの開発と、BtoB Webマーケを支援するWeb制作を提供するN’s Creates (エヌズクリエイツ) 株式会社の西山です。
【クリーンアーキテクチャ】単一責任の原則(SRP)を「関数を小さくすること」だと思い込んでいませんか?
エンジニアとして設計を学ぶ中で、必ず出会う「単一責任の原則(Single Responsibility Principle:SRP)」。
実は私自身、この原則について長い間、大きな誤解をしていました。
私がしていた誤解
以前の私は、SRPを以下のような意味だと思っていました。
- 「1つのクラスや関数がやることを、極力少なくすること」
- 「共通処理をまとめて、コードを簡素化すること(DRY原則)」
- 「とにかくメソッドを行数少なく分割すること」
つまり、「コードの整理整頓」の話だと思っていたのです。
しかし、『Clean Architecture』を読み込み、理解を深めるにつれて、その認識が間違いであることに気づきました。
SRPの真の意味は、機能の数ではなく、「変更する理由(アクター)」にあったのです。
SRPの真の意味:「アクター」ごとに分割せよ
正しいSRPの定義はこうです。
「モジュールは、たったひとつのアクターに対して責任を負うべきである」
ここで言う「アクター」とは、「仕様変更を要求してくる人(部署)」のことです。
- 経理部門(CFO):給与計算のロジックを変えたい
- 人事部門(COO):労働時間のレポート表示を変えたい
- 技術部門(CTO):DBの保存先を変えたい
これら異なるアクターの要望を、1つのクラスに混ぜてしまうことが「SRP違反」なのです。
論より証拠。実際のコードで見比べてみましょう。
❌ 悪い例:SRP違反のコード(Fat Model)
まずは、私が以前よくやっていた実装です。「従業員(Employee)」に関することは全てEmployeeクラスに書いてしまっています。
一見便利そうですが、3人の異なるアクター(CFO, COO, CTO)のロジックが混在しています。
<?php
namespace App\BadExample;
use Illuminate\Database\Eloquent\Model;
class Employee extends Model
{
// 共通ロジック(ここが事故の元!)
// 経理と人事で「時間の定義」が違うのに、共通化してしまっている
private function getHours()
{
return $this->worked_minutes / 60;
}
// アクターA:CFO(経理)の領域
public function calculateMonthlyPay()
{
// 共通メソッドに依存
$hours = $this->getHours();
return (int) ($hours * $this->hourly_rate);
}
// アクターB:COO(人事)の領域
public function getWorkReportLabel()
{
// 共通メソッドに依存
$hours = $this->getHours();
return sprintf('今月の労働時間は %.1f 時間です', $hours);
}
// アクターC:CTO(技術)の領域
public function saveToDb()
{
$this->save();
}
}
このコードの最大の問題点は、「経理(CFO)の要望で getHours() を修正すると、人事(COO)のレポートまで勝手に変わってしまう」ことです。
✅ 良い例:SRPを守ったアーキテクチャ
では、正しくSRPを適用したコードを見てみましょう。
「ドメインモデル貧血症 + サービス層」パターンを採用し、アクターごとにクラスを明確に分けています。
1. データ構造(Model)
ここはロジックを持ちません。全アクターが使う単なる「データの入れ物」です。
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* 従業員モデル
* 責任:データの保持のみ
* アクター:なし(全アクターが参照する共通言語)
*/
class Employee extends Model
{
// 氏名、時給、当月の総労働分などを保持
protected $fillable = ['name', 'hourly_rate', 'worked_minutes'];
}
2. 技術担当(CTO)の領域:リポジトリ
「データをどう保存するか」は技術的な関心事なので分離します。
namespace App\Repositories;
use App\Models\Employee;
/**
* 責任:データの永続化(保存・取得)
* アクター:CTO(技術部門・DBA)
*/
class EmployeeRepository
{
public function find(int $id): Employee
{
return Employee::findOrFail($id);
}
public function save(Employee $employee): void
{
$employee->save();
}
}
3. 経理担当(CFO)の領域:給与計算サービス
ここには「お金の計算」だけを書きます。
namespace App\Services\Accounting;
use App\Models\Employee;
/**
* 責任:給与金額の計算
* アクター:CFO(経理部門)
*/
class SalaryCalculator
{
public function calculateMonthlyPay(Employee $employee): int
{
// 経理独自の計算ルール(例:15分単位で切り捨てて計算など)
$billableHours = floor($employee->worked_minutes / 15) * 0.25;
return (int) ($billableHours * $employee->hourly_rate);
}
}
4. 人事担当(COO)の領域:レポート作成サービス
ここには「報告・表示」だけを書きます。給与計算とは完全に独立します。
namespace App\Services\Hr;
use App\Models\Employee;
/**
* 責任:労働時間の可視化・レポート
* アクター:COO(人事・運用部門)
*/
class WorkHourReporter
{
public function formatWorkDuration(Employee $employee): string
{
// 人事独自の表示ルール(例:○時間○分 表記)
$hours = floor($employee->worked_minutes / 60);
$minutes = $employee->worked_minutes % 60;
return sprintf('%d時間 %02d分', $hours, $minutes);
}
}
5. 利用側(Controller)
最後に、DI(依存性注入)を使ってこれらを組み合わせます。
namespace App\Http\Controllers;
use App\Repositories\EmployeeRepository;
use App\Services\Accounting\SalaryCalculator;
use App\Services\Hr\WorkHourReporter;
use Illuminate\Http\JsonResponse;
class EmployeeController extends Controller
{
public function __construct(
private EmployeeRepository $repository,
private SalaryCalculator $calculator,
private WorkHourReporter $reporter
) {}
public function show(int $id): JsonResponse
{
// 1. 技術屋(CTO)のコードを使ってデータを取る
$employee = $this->repository->find($id);
// 2. 経理(CFO)のコードを使って給与計算する
$salary = $this->calculator->calculateMonthlyPay($employee);
// 3. 人事(COO)のコードを使って表示用フォーマットを作る
$workTime = $this->reporter->formatWorkDuration($employee);
return response()->json([
'name' => $employee->name,
'salary' => number_format($salary) . '円',
'work_time' => $workTime,
]);
}
}
解説:なぜこれが「単一責任の原則」なのか?
この構成にすることで、以下のような変更要望(シナリオ)に完璧に対応できます。
シナリオA:経理(CFO)からの要望
「今月から、給与計算の端数処理を1分単位に変えてください」
- 修正箇所:
Services/Accounting/SalaryCalculator.phpのみ。 - 影響範囲: 人事レポート(WorkHourReporter)やDB保存処理(Repository)には絶対に影響しません。テストも
SalaryCalculatorTestだけを実行すれば安心です。
シナリオB:人事(COO)からの要望
「レポートの労働時間を『9.5h』のような小数点表記に変えてください」
- 修正箇所:
Services/Hr/WorkHourReporter.phpのみ。 - 影響範囲: ここをどう書き換えても、給与計算ロジック(SalaryCalculator)は別のクラスにあるため、「表示を変えたら給与計算がバグってお給料が変わってしまった」という最悪の事故は物理的に発生しません。
シナリオC:技術(CTO)からの要望
「DBをMySQLからNoSQLに移行したい」
- 修正箇所:
Repositories/EmployeeRepository.phpのみ。 - 影響範囲: 計算ロジックやレポートロジックは、データがどこから来たか(MySQLかNoSQLか)を知らないため、修正不要です。
まとめ
SRPを守るとは、「誰がそのコードの変更権利を持っているか」ごとにフォルダやクラスを分けることです。
以前の私のように、何でもかんでも Employee クラスに詰め込んでしまうと(Fat Model)、3つの異なる部署が同じファイルを触ることになり、予期せぬバグ(副作用)を生みます。
上記のようにサービスやリポジトリに分けることで、システムは「変更に強く、壊れにくい」状態になります。これがSRPの本当の価値なのです。
UX / UI のデザインに強いWebシステムの開発と、BtoB Webマーケを支援するWeb制作を提供する
N's Creates 株式会社は、神戸三宮オフィスまで週1出社(それ以外はリモートワーク)できる「デザイナー」「エンジニア」を募集しています。
興味のある方は、カジュアル面談しますので気軽にお問い合わせください!









