18
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Laravel】SOLID 原則の単一責任の原則を取り入れてみた

Last updated at Posted at 2022-12-18

1.はじめに

この記事は「つながる勉強会 Advent Calendar 2022」の18日目の記事です。
(1日間違ってしまったのは内緒の話…)

17日目は @zonoryo03 さん
30代未経験からエンジニア転職を成功させる上で意識した5つのこと

19日目は @akko_merry さん
Let’s Encryptを使用しDocker+nginxのアプリをSSL化した手順

の投稿です。


以下が記事の内容となります。

開発に携わらせていただいて約 1 年ほど経ちました。
この 1 年は開発経験を積むということはできました。

ただ、まだまだ良いコードを書けているとは思えません。

最近は、

  • 保守のしやすいコードを書きたい
  • 理解しやすいコードを書きたい
  • 再利用しやすいコードを書きたい

と思うようになり、
アーキテクチャ関係の本を色々と読んできました。

ただ、本を読むだけだと身についてないので、
コードを書き、記事を書きながらアウトプットし、知恵にしていきたいと思っています。

本記事は、SOLID 原則の 5 原則についてキャッチアップしたことをまとめました。

  • 単一責任の原則( ← 今回はここ)
  • オープン・クローズドの原則
  • リスコフの置換原則
  • インターフェース分離の原則
  • 依存関係逆転の原則

認識間違っているよとかありましたら、コメント頂けますと幸いです🙇‍♂️

2.目次

1.はじめに
2.目次
3.この記事でわかること
4.環境
5.SOLID原則とは
 5.1.単一責任の原則とは
 5.2.単一責任の原則を違反する例
 5.3.単一責任の原則に違反しない例
 5.4.単一責任の原則の有無の比較(コントローラ)
6.おわりに
7.参考

3.この記事でわかること

コントローラにいろいろな責任をもたせたものを、単一の責任にリファクタリングしていきます。

単一責任の原則を取り入れることで

  • 修正しやすい
  • 処理が複雑になりにくい
  • 問題箇所が突き止めやすい

などのメリットを感じることができるかと思います。

下記の単一責任の原則を違反しているメソッドを

単一責任の原則を違反する
    public function store(Request $request): JsonResponse
    {
        $input = $request->all();

        $rules = [
            'title'   => ['required', 'string', 'max:100'],
            'content' => ['required', 'string'],
        ];

        $validator = Validator::make($input, $rules);

        if ($validator->fails()) {
            return response()->json([
                'status' => 'validation error',
                'errors' => $validator->errors(),
            ], 400);
        }

        try {
            DB::beginTransaction();

            $post = new Post();

            $post->title   = $input['title'];
            $post->content = $input['content'];
                 
            $post->save();

            DB::commit();
        } catch (Exception $e) {
            Log::error($e->getMessage());
            DB::rollBack();

            return throw new InternalServerException('投稿できませんでした。');
        }

        return response()->json(['post' => $post], 201);
    }

下記の単一責任の原則を違反していないメソッドにリファクタリングします。

単一責任の原則しない
    public function store(StoreRequest $request, StoreAction $action)
    {
        $post = $request->makePost();

        try {
            $result = $action($post);

            return new StoreResource($result);
        } catch (Throwable $e) {
            return throw new InternalServerException('投稿できませんでした。');
        }
    }

4.環境

言語・FW 等 バージョン
PHP 8.1
Laravel 9.43.0
MySQL 8.0

5.SOLID原則とは

クリーンアーキテクチャにはこのように書かれています。

関数やデータ構造、クラスの相互接続をどのようにするのかを教えてくれる。

…略…

SOLID 原則の目的は、以下のような性質をもつ中間レベルのソフトウェア構造を作ることだ。

  • 変更に強いこと
  • 理解しやすいこと
  • コンポーネントの基盤として、多くのソフトウェアシステムに利用できること

モジュールやコンポーネントで使うソフトウェア構造の定義に役立つ。

これを読んだとき、僕は、
「うわ〜、今求めているものやん!!」
っとテンションが上がりました。

ただ読み進めてみると、なかなか理解するのが難しかったです。

多分同じような人はいっらっしゃるかと思うので、コードベースでできるだけ噛み砕いて説明していきたいと思います。

僕の所感はこれぐらいにして、SOLID 原則の話に戻します。
SOLID 原則は、下記の 5 つの設計原則の頭文字をとって命名されています。

  • 単一責任の原則(Single Responsibility Principle)
  • オープン・クローズドの原則( Open-Closed Principle)
  • リスコフの置換原則(Liskov Substitution Principle)
  • インターフェイス分離の原則(Interface Segregation Principle)
  • 依存性逆転の原則(Dependency Inversion Principle)

本記事は、1 つ目の単一責任の原則(Single Responsibility Principle) について記述しています。

5.1.単一責任の原則とは

では、本題の 単一責任の原則 について見てきます。

クリーンアーキテクチャには、

モジュールは、たった一つのアクターに対して責務を負うべきである

と表現されています。

つまり、ソースコードで解決したい対象の「人 or モノ」は、たった一つに絞るべきということです。

複数の対象に対して、解決しようとするソースコードは、クラスや関数を分けるのが良いよ!!
っとことですね。

複数の対象に対してソースコードを書いていると下記の問題が生じます。

  • 変更しようと思ったら、別のアクターにも影響が出るかも…
  • 修正するために、どのアクターに影響が出るか調べないといけない…
  • 変更後に、すべてのアクターに問題がないかを調べないといけない…
  • コードの変更部分が同時に発生し、コンフリクトが発生するかもしれない…

などですね。

最初は、そこまで問題に感じないかもしれないですが、機能追加や保守するときには苦労してしまいます。

それでは、次項から、単一責任の原則を 違反している ものをみてから、違反していない ものを見ていきます。

簡単な記事投稿(store メソッドのみ)のソースコードをサンプルとしています。

あくまでも一例ですので、こんな風にするともっと良いよっというのがあれば、コメントいただけますと幸いです。🙇‍♂️

5.2.単一責任の原則を違反する例

まずは、単一責任の原則を違反しているものを見てきます。

コントローラの store メソッドでは、

  1. リクエストのバリデーションチェックを行う
  2. リクエストのバリデーションチェックに引っかかるとエラーを返す
  3. リクエストに問題なければ、投稿オブジェクトを作成する
  4. オブジェクトにリクエストの内容を代入する
  5. オブジェクト作成に問題なければ、DB に保存する
  6. オブジェクト作成に問題があれば、エラーを返す
  7. オブジェクト作成に問題なければ、投稿内容とステータスコードを返す

ということをやっています。

これを見ただけでもいろんなことをやっているなって思われるかと思います。

それでは、ソースコード見ていきます。

本題であるコントローラ以外の内容もご興味があれが、下記をクリックしてください。

ルーティング(src/routes/api.php)
src/routes/api.php
<?php

declare(strict_types=1);

use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

Route::post('/post', [PostController::class, 'store']);
該当のルーティング
  POST       api/post ............ PostController@store
モデル(src/app/Models/Post.php)
src/app/Models/Post.php
<?php

declare(strict_types=1);

namespace App\Models;

use Eloquent;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

/**
 * App\Models\Post
 *
 * @method static \Database\Factories\PostFactory factory(...$parameters)
 * @method static Builder|Post newModelQuery()
 * @method static Builder|Post newQuery()
 * @method static Builder|Post query()
 *
 * @mixin Eloquent
 */
final class Post extends Model
{
    use HasFactory;

    /**
     * @var string テーブル名 指定
     */
    protected $table = 'posts';

    protected $fillable = [
        'title',
        'content',
    ];
}
マイグレーション(src/database/migrations/2022_12_17_090215_create_posts_table.php)
src/database/migrations/2022_12_17_090215_create_posts_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id()->comment('投稿ID');
            $table->string('title', 100)->comment('タイトル');
            $table->text('content')->comment('コンテンツ');
            $table->timestamp('created_at')->useCurrent()->comment('登録日時');
            $table->timestamp('updated_at')->useCurrent()->comment('更新日時');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

マイグレートの実施

php artisan maigrate
テスト(src/tests/Feature/PostTest.php)

※ リファクタリングしたいのでテストを作成しています。

src/tests/Feature/PostTest.php
<?php

declare(strict_types=1);

namespace Tests\Feature;

use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Support\Facades\Artisan;
use Tests\TestCase;

class PostTest extends TestCase
{
    use DatabaseMigrations;

    private const TABLE_NAME = 'posts';

    protected function setUp(): void
    {
        parent::setUp();
        Artisan::call('migrate:refresh');
    }

    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_投稿できる_正常(): void
    {
        $url = '/api/post';

        $query = [
            'title'      => 'テストタイトル',
            'content'    => 'テストコンテンツ',
        ];

        $response = $this->json('POST', $url, $query);

        $response->assertStatus(200);
        $this->assertDatabaseCount(self::TABLE_NAME, 1);
        $response->assertJsonFragment([
            'id'         => 1,
            'title'      => 'テストタイトル',
            'content'    => 'テストコンテンツ',
        ]);
    }
}

コントローラは以下のとおりです。

src/app/Http/Controllers/PostController.php
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Models\Post;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Throwable;

final class PostController extends Controller
{
    /**
     * @throws Throwable
     */
    public function store(Request $request): JsonResponse
    {
        // 1. リクエストのバリデーションチェックを行う
        $input = $request->all();

        $rules = [
            'title'   => ['required', 'string', 'max:100'],
            'content' => ['required', 'string'],
        ];

        $validator = Validator::make($input, $rules);

        // 2. リクエストのバリデーションチェックに引っかかるとエラーを返す
        if ($validator->fails()) {
            return response()->json([
                'status' => 'validation error',
                'errors' => $validator->errors(),
            ], 400);
        }

        try {
            DB::beginTransaction();

            // 3. リクエストに問題なければ、投稿オブジェクトを作成する
            $post = new Post();

            // 4. オブジェクトにリクエストの内容を代入する
            $post->title   = $input['title'];
            $post->content = $input['content'];
                 
            // 5. オブジェクト作成に問題なければ、DB に保存する
            $post->save();

            DB::commit();
        } catch (Exception $e) {
            // 6. オブジェクト作成に問題があれば、エラーを返す
            Log::error($e->getMessage());
            DB::rollBack();

            return throw new InternalServerException('投稿できませんでした。');
        }

        // 7. オブジェクト作成に問題なければ、投稿内容とステータスコードを返す
        return response()->json(['post' => $post], 201);
    }
}

今回は、投稿の 登録(保存) だけですが、通常であれば、

  • 投稿一覧の取得
  • 指定の投稿の取得
  • 投稿の更新
  • 投稿の削除
  • 投稿の検索

なども同じコントローラに記述するかと思います。
これらの実装を記述するとなると、もっと膨大ないクラスとなってしまいます。

  • バグ原因箇所を探すのに手間がかかる
  • 機能追加するときに他の箇所に影響を及ぼす可能性が生じる
  • 他の開発者が同じ箇所に別の機能を追加してコンフリクトが起きる  など…

の問題が生じる可能性があります。

これらの問題を解決するために、責任をできるだけ分けることが大切になってきます。

それでは、今回の store メソッドの処理内容を分解してみます。

下記が store メソッドの処理内容でしたね。

  1. リクエストのバリデーションチェックを行う
  2. リクエストのバリデーションチェックに引っかかるとエラーを返す
  3. リクエストに問題なければ、投稿オブジェクトを作成する
  4. オブジェクトにリクエストの内容を代入する
  5. オブジェクト作成に問題なければ、DB に保存する
  6. オブジェクト作成に問題があれば、エラーを返す
  7. オブジェクト作成に問題なければ、投稿内容とステータスコードを返す

これを下記のように分解してみました。
(これはあくまでも一例です)

  • 分解 1) リクエストを受け取ってバリデーションを行う処理

1.リクエストのバリデーションチェックを行う
2.リクエストに問題なければ、投稿オブジェクトを作成する
3.オブジェクトにリクエストの内容を代入する

  • 分解 2) 受け取ったリクエストを DB に保存する処理

5.オブジェクト作成に問題なければ、DB に保存する

  • 分解 3) レスポンスとして返す処理

7.オブジェクト作成に問題なければ、投稿内容とステータスコードを返す

  • 分解 4)エラーを返す処理

2.リクエストのバリデーションチェックに引っかかるとエラーを返す
3.オブジェクト作成に問題があれば、エラーを返す

このように分解してみると、コントローラの一つのメソッドでいろいろな責任を負わしていることがわかるかと思います。
もともとの store メソッドは、単一責任の原則を違反していますね。

次に、単一責任の原則を満たすようにリファクタリングしようと思います。

5.3.単一責任の原則に違反しない例

前項で、コントローラ一つのメソッドに責任をもたせすぎていることがわかったと思います。

本項では、分解した内容を元にソースコードを書いていきます。

下記は、先程分解した内容に、責任を負わすクラスを追加しました。

  • 分解 1) リクエストを受け取ってバリデーションを行う処理
    Requests/Post/StoreRequest.php に責任を負わす

1.リクエストのバリデーションチェックを行う
2.リクエストに問題なければ、投稿オブジェクトを作成する
3.オブジェクトにリクエストの内容を代入する

  • 分解 2) 受け取ったリクエストを DB に保存する処理
    UseCases/StoreAction.php に責任を負わす

5.オブジェクト作成に問題なければ、DB に保存する

  • 分解 3) レスポンスとして返す処理
    UseCases/StoreAction.php に責任を負わす

7.オブジェクト作成に問題なければ、投稿内容とステータスコードを返す

  • 分解 4)エラーを返す処理
    Exceptions/HttpResponseException.phpExceptions/InternalServerException.php に責任を負わす

2.リクエストのバリデーションチェックに引っかかるとエラーを返す
3.オブジェクト作成に問題があれば、エラーを返す

そして、過剰な設計かも知れませんが、
コントローラは、単一責任の原則を満たすために シングルアクションコントローラ としました。

シングルアクションコントローラとは、
言葉の通り、コントローラに一つのアクションしか持たさないことです。

ReaDouble のシングルアクションコントローラ 参照

今回は、store メソッドしかありませんが、
showgetupdatedeletesearch などが存在すると別々のコントローラに分けるということになります。

コントローラは、責任を持たせたクラスに処理を投げる 仲介役 となります。
言い換えると、仲介役以外の責任は持たせていません。

なので、ホイ、ホイと別のクラスに処理を投げるので、store メソッド、40 行ほどあったソースコードが 15 行ほどになりました。

ルーティングとテストに関しては、以前の内容からルーティングが変更した程度です。
ご興味のある方は下記をクリックしていただければです。

ルーティング(src/routes/api.php)
src/routes/api.php
<?php

declare(strict_types=1);

use App\Http\Controllers\Post\StoreController;  // 追加
use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

Route::post('/post', [PostController::class, 'store']);
Route::post('/srp-post', StoreController::class);       // 追加
該当のルーティング
  POST       api/post ............ PostController@store
  POST       api/srppost ......... PostController@store // 今回のルーティング
テスト(src/tests/Feature/Post/StoreTest.php)
src/tests/Feature/Post/StoreTest.php
<?php

declare(strict_types=1);

namespace Tests\Feature\Post;

use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Support\Facades\Artisan;
use Tests\TestCase;

class StoreTest extends TestCase
{
    use DatabaseMigrations;

    private const TABLE_NAME = 'posts';

    protected function setUp(): void
    {
        parent::setUp();
        Artisan::call('migrate:refresh');
    }

    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_投稿できる_正常(): void
    {
        $url = '/api/srp-post';

        $query = [
            'title'   => 'テストタイトル',
            'content' => 'テストコンテンツ',
        ];

        $response = $this->json('POST', $url, $query);

        $response->assertStatus(201);
        $this->assertDatabaseCount(self::TABLE_NAME, 1);
        $response->assertJsonFragment([
            'id'      => 1,
            'title'   => 'テストタイトル',
            'content' => 'テストコンテンツ',
        ]);
    }

    /**
     *
     * @return void
     */
    public function test_バリデーションエラー_異常系(): void
    {
        $url = '/api/srp-post';

        $query = [
            'title'   => null,
            'content' => null,
        ];

        $response = $this->json('POST', $url, $query);

        $response->assertStatus(400);
        $this->assertDatabaseCount(self::TABLE_NAME, 0);
    }
}

ちょっと前置きが長くなってしまいましたが、分解したソースコードを見ていきます。

5.3.1.仲介役のコントローラ

シングルアクションコントローラとなっているため、
store メソッドではなく、__invoke メソッドを使用しています。

ReaDouble のシングルアクションコントローラ 参照

src/app/Http/Controllers/Post/StoreController.php
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Post;

use App\Exceptions\InternalServerException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Post\StoreRequest;
use App\Http\Resources\Post\StoreResource;
use App\Http\UseCases\StoreAction;
use Throwable;

class StoreController extends Controller
{
    /**
     * @param StoreRequest $request
     * @param StoreAction $action
     *
     * @return StoreResource
     *
     * @throws Throwable
     */
    // __invoke の引数として、リクエスト(StoreRequest クラス)とユースケース(StoreAction クラス)を受け取る
    public function __invoke(StoreRequest $request, StoreAction $action)
    {
        // StoreRequest クラスでバリデーションちぇっくしたものから、Post インスタンスを作成
        $post = $request->makePost();

        try {
            // 作成した Post インスタンスを StoreAction クラスに渡す
            $result = $action($post);

            // StoreAction から返ってきたものを StoreResource クラスに渡す
            return new StoreResource($result);
        } catch (Throwable $e) {
            // エラーを InternalServerException クラスにて対応
            return throw new InternalServerException('投稿できませんでした。');
        }
    }
}

5.3.2.分解 1)リクエストを受け取ってバリデーションを行う処理

1 つ目の分解は下記でしたね。

分解 1) リクエストを受け取ってバリデーションを行う処理

1.リクエストのバリデーションチェックを行う
2.リクエストに問題なければ、投稿オブジェクトを作成する
3.オブジェクトにリクエストの内容を代入する

src/app/Http/Requests/Post/StoreRequest.php
<?php

declare(strict_types=1);

namespace App\Http\Requests\Post;

use App\Models\Post;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;

final class StoreRequest extends FormRequest
{
    /**
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * 1. リクエストのバリデーションチェックを行う
     *
     * @return array<string, mixed>
     */
    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:100'],
            'content' => ['required', 'string'],
        ];
    }

    /**
     * 3. リクエストに問題なければ、投稿オブジェクトを作成する
     * 4. オブジェクトにリクエストの内容を代入する
     *
     * @return Post
     */
    public function makePost(): Post
    {
        // バリデーションした値で Post を作成
        return new Post($this->validated());
    }

    /**
     * バリデーションエラーのときは、HttpResponseException クラスにて対応
     *
     * @param Validator $validator
     *
     * @return void
     */
    protected function failedValidation(Validator $validator): void
    {
        $response = response()->json([
            'status' => 'validation error',
            'errors' => $validator->errors(),
        ], 400);

        throw new HttpResponseException($response);
    }
}

5.3.3.分解 2)受け取ったリクエストを DB に保存する処理

2 つ目の分解は下記ですね。

分解 2) 受け取ったリクエストを DB に保存する処理

5.オブジェクト作成に問題なければ、DB に保存する

よく Service にいろいろな処理を詰め込んで Fat なクラスになるかと思います。(僕自身、これやっていました…)
それを回避するために、UseCases というディレクトリを作成し、ユースケース毎に処理を分けました。

src/app/Http/UseCases/StoreAction.php
<?php

declare(strict_types=1);

namespace App\Http\UseCases;

use App\Models\Post;
use Throwable;

class StoreAction
{
    /**
     * @param Post $post
     *
     * @return Post
     *
     * @throws Throwable
     */
    public function __invoke(Post $post): Post
    {
        $post->save();

        return $post;
    }
}

今回のユースケースは受け取ったインスタンスを DB に保存するだけの責任ですが、
業務で使うロジックは、これがもっと複雑になるってくるかと思います。

複雑になりそうなときは、なんの責任があるかを念頭において分解しながら実装する必要があります。
単一責任です!!

5.3.4.分解 3)レスポンスとして返す処理

3 つ目は、リソースですね。

分解 3) レスポンスとして返す処理

7.オブジェクト作成に問題なければ、投稿内容とステータスコードを返す

これは、レスポンスを整形するためのクラスです。

Laravel では、フレームワークが自体がコントローラでリターンするだけで、整形してくてます。
ただ、これだと柔軟な対応ができません。

これを解決するためのクラスが Recource クラスになります。

ReaDuble の API リソース 参照

src/app/Http/Resources/Post/StoreResource.php
<?php

declare(strict_types=1);

namespace App\Http\Resources\Post;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class StoreResource extends JsonResource
{
    /**
     * 7. オブジェクト作成に問題なければ、投稿内容とステータスコードを返す
     *
     * @param Request $request
     *
     * @return array<string, array<string, mixed>|int>
     */
    public function toArray($request): array
    {
        return [
            'post'   => [
                'id'         => $this->resource->id,
                'title'      => $this->resource->title,
                'content'    => $this->resource->content,
                'created_at' => $this->resource->created_at,
                'updated_at' => $this->resource->updated_at,
            ],
            'status' => 201,
        ];
    }
}

5.3.5.分解 4)エラーを返す処理

最後は、エラー処理ですね。

分解 4)エラーを返す処理

2.リクエストのバリデーションチェックに引っかかるとエラーを返す
3.オブジェクト作成に問題があれば、エラーを返す

これは、各エラー毎に処理を切り分けています。

処理内容によってエラーコードを分けたいときに使えますね。

src/vendor/laravel/framework/src/Illuminate/Http/Exceptions/HttpResponseException.php
<?php

namespace Illuminate\Http\Exceptions;

use RuntimeException;
use Symfony\Component\HttpFoundation\Response;

class HttpResponseException extends RuntimeException
{
    /**
     * The underlying response instance.
     *
     * @var \Symfony\Component\HttpFoundation\Response
     */
    protected $response;

    /**
     * Create a new HTTP response exception instance.
     *
     * @param  \Symfony\Component\HttpFoundation\Response  $response
     * @return void
     */
    public function __construct(Response $response)
    {
        $this->response = $response;
    }

    /**
     * Get the underlying response instance.
     *
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function getResponse()
    {
        return $this->response;
    }
}
src/app/Exceptions/InternalServerException.php
<?php

declare(strict_types=1);

namespace App\Exceptions;

/**
 * リクエスト内容のエラー
 */
class InternalServerException extends MyHttpException
{
    // 取り扱うステータスコード
    public const STATUS_CODE = 500;
    /**
     * constructor.
     *
     * @param string $message 簡易エラーメッセージ
     */
    public function __construct(string $message = 'internal server error')
    {
        // super コンストラクタ
        parent::__construct($message, self::STATUS_CODE);
    }
}

5.4.単一責任の原則の有無の比較(コントローラ)

単一責任の原則に違反しているのコントローラ
src/app/Http/Controllers/PostController.php
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Models\Post;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Throwable;

final class PostController extends Controller
{
    /**
     * @throws Throwable
     */
    public function store(Request $request): JsonResponse
    {
        $input = $request->all();

        $rules = [
            'title'   => ['required', 'string', 'max:100'],
            'content' => ['required', 'string'],
        ];

        $validator = Validator::make($input, $rules);

        if ($validator->fails()) {
            return response()->json([
                'status' => 'validation error',
                'errors' => $validator->errors(),
            ], 400);
        }

        try {
            DB::beginTransaction();

            $post = new Post();

            $post->title   = $input['title'];
            $post->content = $input['content'];
                 
            $post->save();

            DB::commit();
        } catch (Exception $e) {
            Log::error($e->getMessage());
            DB::rollBack();

            return throw new InternalServerException('投稿できませんでした。');
        }

        return response()->json(['post' => $post], 201);
    }
}

?>

単一責任の原則に違反していないのコントローラ
src/app/Http/Controllers/Post/StoreController.php
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Post;

use App\Exceptions\InternalServerException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Post\StoreRequest;
use App\Http\Resources\Post\StoreResource;
use App\Http\UseCases\StoreAction;
use Throwable;

class StoreController extends Controller
{
    /**
     * @param StoreRequest $request
     * @param StoreAction $action
     *
     * @return StoreResource
     *
     * @throws Throwable
     */
    // __invoke の引数として、リクエスト(StoreRequest クラス)とユースケース(StoreAction クラス)を受け取る
    public function __invoke(StoreRequest $request, StoreAction $action)
    {
        // リクエストを StoreRequest クラスに渡して、Post インスタンスを作成
        $post = $request->makePost();

        try {
            // 作成した Post インスタンスを StoreAction クラスに渡す
            $result = $action($post);

            // StoreAction から返ってきたものを StoreResource クラスに渡す
            return new StoreResource($result);
        } catch (Throwable $e) {
            // エラーを InternalServerException クラスにて対応
            return throw new InternalServerException('投稿できませんでした。');
        }
    }
}

?>

慣れないうちは、

「いろいろなファイルに移動して処理を確認しないといけない…めんどくさい…」

と思われるかもしれませんが(僕がそうでした)、

修正や機能追加をするときに効果を発揮します。

どこで何が起こっているのかを容易に把握することができます。

6.おわりに

今まで、1 つのメソッド、クラスにロジックを書きまくっていました。

そのせいで、処理を複雑にしてしまっていました。

単一責任の原則を意識してロジックを組み立てたものは、修正や機能追加がしやすいなと感じています。

単一責任の原則を取り入れたソースコードを書くことをここに誓います!

併せて他の記事も読んでいただけると嬉しいです 🙇‍♂️

最後まで読んでいただき、ありがとうございました 😊

7.参考

18
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?