[Laravel] サービスリロケートパターンのススメ
まえがき
2024年現在、WordPressなど一部の特殊なアーキテクチャを除いて、Webアプリケーションの多くはMVCパターンを採用しています。
非常にわかりやすく、優れた設計手法ですが、Laravelの場合は何も考えずに入門書の通り書くとControllerが肥大化する傾向にあります。
これはファットコントローラーと言われるアンチパターンで、やってはいけないものです。
なぜLaravelはファットコントローラーになりやすいか
まず1つには入門書や入門動画が悪いです。
LaravelにはModelクラスがあり、これがMVCのMだと説明されがちです。
確かにこれもMですが、MVCパターンのMはもっと責務が広いです。
MVCパターンのM(Model)とはビジネスロジック(処理ロジック)全般を指すものであり、データベース読み書きに限らず計算ロジックや、ファイル・データの生成・加工、なども含みます。
入門書ではデータベース関連以外は全てControllerクラスで処理させようとします。
しかしControllerは本来ModelとViewをつなぐ指揮官としての役割を行うものであり、ビジネスロジックを実装するところではありません。
サービスリロケートとは
サービスリロケートは、ビジネスロジックを行うクラスを、その役割ごとに作成し、コントローラクラスから切り離す手法です。
それにより見通しの良さと再利用性を高めます。
さらに処理部分を切り出すことで動作確認も楽になります。
ソースコード例
サービスクラス用のフォルダはないので以下のフォルダ構成で自分で作ります。
クラスのファイルも自作。
下の2行がそうです。
src/app
├── Console
│ └── Kernel.php
├── Exceptions
│ └── Handler.php
├── Http
│ ├── Controllers
│ │ ├── A01.php
│ │ ├── A02.php
│ │ └── Controller.php
│ ├── Kernel.php
│ └── Middleware
│ ├── Authenticate.php
│ ├── EncryptCookies.php
│ ├── PreventRequestsDuringMaintenance.php
│ ├── RedirectIfAuthenticated.php
│ ├── TrimStrings.php
│ ├── TrustHosts.php
│ ├── TrustProxies.php
│ └── VerifyCsrfToken.php
├── Models
│ ├── Ticket.php
│ └── User.php
├── Providers
│ ├── AppServiceProvider.php
│ ├── AuthServiceProvider.php
│ ├── BroadcastServiceProvider.php
│ ├── EventServiceProvider.php
│ └── RouteServiceProvider.php
└── Services
└── TicketService.php
サービスクラスはこんな感じ。
DBへの読み書きはここでやります。
Controllerに直接Modelをインポートしたりしません。
<?php
namespace App\Services;
use App\Models\Ticket;
class TicketService
{
public function storeTicket(string $ticketTitle, string $ticketSummary, int $ticketStatus)
{
$ticket = Ticket::create(
[
'subject' => $ticketTitle,
'summary' => $ticketSummary,
'status' => $ticketStatus,
]
);
return;
}
public function getTicketList()
{
$ticketList = Ticket::all();
return $ticketList;
}
}
続いてコントローラ。
まず基底クラスのコンストラクタで、サービスクラスを読み込みます。
コンストラクタの引数に注目してください。
Laravelには依存性の注入(ディペンデンシーインジェクション、略してDI)と言う強力な機能があり、クラスの結合度を下げることができます。
このように記述すると、Laravelがいい感じにクラスのインスタンスを生成して初期化してくれます。
え? new すればいいんじゃないのって?
メモリ管理の観点や、抽象化の観点からこちらの方が優れています。
抽象化に関しては、この記事の「サービスリロケートパターン」の更に応用である「サービス・リポジトリーパターン」などで利用されています。
抽象化する場合は具象サービスクラスの代わりサービスインターフェースを引数に指定します。
実はそれを利用すると環境ごとに違う実装を割り当てたり、モックと実コードを切り替えたりできるのですが、それだけで長い記事になるので今回は割愛。
<?php
namespace App\Http\Controllers;
use App\Services\TicketService;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
// 子クラスで使えるようにprotectedでプロパティを追加。
protected TicketService $ticketService;
// Dependency Indectionで作成したサービスクラスを注入する。
public function __construct(TicketService $ticketService)
{
$this->ticketService = $ticketService;
}
}
具象コントローラクラス。
めちゃくちゃ短いコードになっていることがお分かりでしょうか?
今回の例では単純な読み書きなので実感しずらいかもしれませんが、たとえばHTTPから受け取った値と編集してから登録するとか、関連するDB読み書きと合わせて行う必要があるとかだとすごすぎて感動すると思います。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class A01 extends Controller
{
public function index()
{
return view('A01');
}
public function store(Request $request)
{
// blade.phpから値を取得する。
$ticketTitle = $request->input('ticket_subject');
$ticketSummary = $request->input('ticket_summary');
$ticketStatus = $request->input('ticket_status');
// サービスクラスにDBへの登録を依頼する。
$this->ticketService->storeTicket($ticketTitle, $ticketSummary, $ticketStatus);
return redirect()->route('A01');
}
}
まとめ
依存性の注入はLaravelの真髄と言っても過言ではありません。
しっかり活用して美しく、メンテしやすいコードを目指しましょう。
株式会社ONE WEDGE
【Serverlessで世の中をもっと楽しく】 ONE WEDGEはServerlessシステム開発を中核技術としてWeb系システム開発、AWS/GCPを利用した業務システム・サービス開発、PWAを用いたモバイル開発、Alexaスキル開発など、元気と技術力を武器にお客様に真摯に向き合う価値創造企業です。
Discussion