「PHPフレームワーク Laravel Webアプリケーション開発」の感想・備忘録4

MVCパターンとADRパターン

MVCパターンのモデル

モデルというとデータベースの処理を想像するシーンが多いが、本来はビジネスロジック全体を指す。
通常のWebアプリケーションでのモデルは、ビジネスロジックを実装する層とデータベースを操作する層から構成される。

データベースを操作するためのEloquantモデルにビジネスロジックも実装してしまうと、2層が一緒になるファットモデルと呼ばれる状態となり巨大なクラスになってしまう。
(コントローラにビジネスロジックを実装してしまうと、ファットコントローラと呼ばれる状態になってしまう)
一般的にモデルは、トランザクションスクリプトパターンかドメインモデルパターンのどちらかが採用される。

トランザクションスクリプトパターン

ビジネスロジックの一連の処理を1つのクラスにまとめたもの。
例えば「書籍を購入する」というビジネスロジックであれば、BookServiceクラスにorderメソッドを定義する。

class BookService
{
    protected $user;
    public function __construct(User $user)
    {
        $this->user = $user;
    }
    public function order(array $books = [])
    {
        // 処理
    }
}

アプリケーションの規模が大きくなると、設計の一貫性が失われ様々な処理が混ざったクラスとなってしまうことが多い。
レイヤ化による責務の分割を行うレイヤードアーキテクチャ(後述)と呼ばれる考え方を取り入れることが、解決の第一歩となる。

ADRパターン

ADR(Action Domain Responder)パターンはMVCパターンをサーバサイド向けに洗練させたパターンとして提唱されたものである。
Action-Domain-ResponderはそれぞれContoroller-Model-Viewに相当する。

  • Action:リクエストを受け付けてDomainを呼びし、データをResponderへ渡す。
  • Domain:ビジネスロジックの入り口であり、トランザクションスクリプトなどでビジネスロジックを実装する。
  • Responder:Actionから受け取ったデータからレスポンスを構築する。

MVCのContorollerは複数のアクションに対応するが、ADRのActionは1つのアクションだけのクラスをして独立させる。
例えば、MVCではUserControllerクラスにindexメソッドを実装するが、ADRではUserIndexActionクラスに__invokeメソッド(またはhandleなどのメソッド)を実装する。

MVCパターンのコントローラの問題点

MVCパターンのコントローラは複数のアクションが実装されているため、あるアクションのみが利用するクラスが増えてしまう。
下記の例の場合、BookReviewServiceクラスはindexメソッドでは利用していない。

public function __construct(
    UserService $userService,
    BookReviewService $bookReviewService
) {
    $this->userService = $userService;
    $this->bookReviewService = $bookReviewService;
}
public function index(Request $request): View
{
    return view('user.index', [
        'user' => $this->userService->retrieveUser($request->get('id', '1'))
    ]);
}
public function store(Request $request): RedirectResponse
{
    $this->userService->activate($request->get('user_id'));
    $this->bookReviewService->addReview(
        $request->get('user_id'),$request->get('book_id'),$request->get('review'));
    return redirect('/users');
}

ADRパターンのディレクトリ構造例

app
┠Domain
┃┗Book
┃ ┠Entity
┃ ┗Services
┗HTTP
 ┠Actions
 ┃┗UserIndexAction.php
 ┗Responder
  ┗BookResponder.php

アクション単位でクラスが増えていくことにデメリットを感じるかもしれない。
しかし、クラスが増えることは小さな機能の集まりとして責務が明確化されているということであり、決してデメリットではない。

ADRパターンの実装例

class UserIndexAction extends Controller
{
    private $domain;
    private $userResponder;

    public function __construct(
        UserService $userService,
        UserResponder $userResponder
    ) {
        $this->domain = $userService;
        $this->userResponder = $userResponder;
    }
    public function __invoke(Request $request): Response
    {
        return $this->userResponder->response(
            $this->domain->retrieveUser($request->get('id', '1'))
        );
    }
}

MVCパターンではステータスコードの変更やクッキーの操作など、プレゼンテーションロジックがコントローラに含まれてしまっている。
レスポンダはコンテンツ情報だけでなく、HTTPレスポンスを構築する処理を担当する。 HTMLやJSONの返却はレスポンダの一部に過ぎない。

class UserResponder
{
    protected $response;
    protected $view;

    public function __construct(Response $response, ViewFactory $view)
    {
        $this->response = $response;
        $this->view = $view;
    }

    public function response(UserModel $user): Response
    {
        $statusCode = Response::HTTP_OK;
        if (!$user->id) {
            $statusCode = Response::HTTP_NOT_FOUND;
        }
        return response(view('user.index', ['user' => $user]), $statusCode);
    }
}

アーキテクチャ設計

モデルの定義と利用

  • コントローラにEloquentを使った処理は書かない。
  • Eloquentモデルにリクエストやセッションを扱う処理は書かない。

コントローラにEloquentモデルによるデータベース処理を記述してしまうと、データベースのリファクタリングなどがあった場合に大幅な改修が必要となってしまう。
また、Eloquentモデルにリクエストやセッション、キャッシュなどを扱う処理を記述してしまうと、フォームの変更などによる影響範囲が大きくなってしまう。

アプリケーション規模に適した設計

小規模アプリで大規模な設計をしたり、逆に大規模アプリで小規模な設計をしてしまうと、拡張性や保守性が低下してしまう。
アプリケーション規模に適した設計をすることが最も大事である。

拡張性や保守性を保つには、ビジネスロジックからデータベース処理を分離した設計を行うことが重要である。

レイヤードアーキテクチャ

レイヤードアーキテクチャは、実装をいくつかのレイヤに分割して設計する手法である。
モデルやコントローラの肥大化を防ぐために、いくつかの層に分割する。

モデルとコントローラの分離

ビジネスロジックをサービスクラスとして分離する。
これにより、コントローラからデータベース処理を排除することができる。

例)以下のアクションメソッドは、サービスクラスとして分離する。

public function index(string $id)
{
    $user = User::find(intval($id));
    $purchases = Purchase::findAllBy($user->id);
    // ここにデータベースの値を使った処理など
    return view('user.index', ['user' => $user):
}
class UserPurchaseService
{
    public function retrievePurchase(int $id): User
    {
        $user = User::find($id);
        $purchases = Purchase::findAllBy($user->id);
        // ここにデータベースの値を使った処理など
        return $user:
    }
}

上記のサービスクラスは、コントローラでコンストラクタインジェクションを使って利用する。

class UserController extends Controller
{
    protected $userPurchaseService;
    public function __construct(UserPurchaseService $userPurchaseService) {
        $this->userPurchaseService = $userPurchaseService;
    }
    public function index(string $id)
    {
        $user =$this->userPurchaseService->retrievePurchase(intval($id));
        // ここにデータベースの値を使った処理など
        return view('user.index', ['user' => $user]):
    }
}

クラスが増えることをデメリットに感じるかもしれないが、サービスコンテナを介してビジネスロジックを簡単に差し替えることができるというメリットがある。
サービスコンテナとサービスプロバイダをしっかりと理解することが重要である。

サービスレイヤとデータベースの分離

モデルとコントローラを分離しただけでは、ビジネスロジックがまだデータベースに依存した状態である。
データベースへの依存を解決するために、データベース操作を抽象化したリポジトリと呼ばれる層を取り入れる。
リポジトリ層でデータベースを操作し、サービスレイヤからデータベース操作を分離する。

コントローラ⇒サービス⇒リポジトリ⇒モデル、というレイヤになる。

class UserRepository
{
    public function find(int $id): User
    {
        return User::find($id);
    }
}

リポジトリクラスは、サービスクラスでコンストラクタインジェクションを使って利用する。

class UserPurchaseService
{
    protected $userRepository;
    protected $purchaseRepository;
    public function __construct(UserRepository $userRepository, PurchaseRepository $purchaseRepository) {
        $this->userRepository = $userRepository;
        $this->purchaseRepository = $purchaseRepository;
    }
    public function retrievePurchase(int $id): User
    {
        $user = $this->userRepository->find($id);
        $purchases = $this->purchaseRepository->findAllBy($user->id);
        // ここにデータベースの値を使った処理など
        return $user:
    }
}

【新しい記事】

【古い記事】

コメントを残す

メールアドレスが公開されることはありません。