単一責任の原則(SRP)を「関数を小さくすること」だと思い込んでいませんか?

西山秀治 / 2025年12月12日

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出社(それ以外はリモートワーク)できる「デザイナー」「エンジニア」を募集しています。

興味のある方は、カジュアル面談しますので気軽にお問い合わせください!

同じテーマの記事