Laravelを用いたクリーンアーキテクチャの実装


Uncle Bob 's Clean Architecture まさに現在の建築家の世界の誇大宣伝です.しかし、実際の実装に関しては、何も注目されていないLaravel .
そして、それは理解可能ですMVC アーキテクチャとその傾向を使用してすべての時間を交差させるFacades きれいで、切り離されたソフトウェア部品を設計するのを助けません.
そこで、今日、私はLaravelアプリの中でクリーンアーキテクチャの原理を実践していますThe Clean Architecture そばRobert C. Martin .
ここで説明されている概念の完全で、実用的な実装は私のGitHub repository . この記事を読んでいる間、実際のコードを見てみてください.
一度は手を清潔にしましょう👍

すべては図から始まった

The architecture must support the use cases. [...] This is the first concern of the architect, and the first priority of the architecture.


( The Clean Architecture 第十六章P 148
あなたが使用ケースを聞いたことがない場合は、機能として、何か意味のあることを行うためのシステムの容量を考えることができます.UML 名前をよく使って説明しましょうUse Case Diagrams .
CAでは、ユースケースは、アプリケーションの中心にあります.彼らはあなたのアプリケーションのマシンを制御するマイクロチップです.

では、それらのユースケースをどう実装するのか?
あなたが尋ねたうれしい!次の図です.

簡単に説明し、実際のコードに飛び込もう.
ピンクラインはコントロールの流れですこれは、異なるコンポーネントが実行されている順序を表します.まず、ユーザーはビュー上で何かを変更します(例えば、登録フォームを送信します).この相互作用はRequest オブジェクト.コントローラはそれを読み込み、Aを生成するRequestModel 使用するUseCaseInteractor .
The UseCaseInteractor それから、そのもの(例えば、新しいユーザーをつくります)をして、Aの形で応答を準備してくださいResponseModel , そして、Presenter . これは、順番にビューを更新するViewModel .
うわー、それはたくさんある😵 それはおそらくCAに作られた主な批評ですそれは長いです!
呼び出し階層は次のようになります.
Controller(Request)
  ⤷ Interactor(RequestModel)
      ⤷ Presenter(ResponseModel)
          ⤷ ViewModel

ポートはどうですか.
私は、あなたが全く観察者であるのを見ることができます!低レバー層(使用ケースとエンティティ、しばしばドメインと呼ばれ、上記のスキーマの赤と黄色の円として表される)のために、高レベル層(フレームワークとして表されるフレームワーク)から切り離されるために、アダプタ(緑の円)が必要です.彼らの仕事は、彼らのそれぞれのAPIと契約(またはインターフェース)を使用して、高低層の間でメッセージを伝えることです.
彼らは、フレームワークの変化がドメインとその逆の変化を必要としないと保証します.CAでは、私たちのユースケースをフレームワーク(実際の実装)から抽象化して欲しいので、他のレイヤの変更を伝播することなく、両方とも変更できます.
きれいなアーキテクチャで設計された伝統的なPHP/HTMLアプリケーションは、したがって、そのコントローラーとプレゼンターを変更するだけで、残りのAPIに変換することができます-使用例は、そのままのままです!または、同じユースケースを使用して、HTML + REST側の両方を持つこともできます.あなたが私に尋ねるならば、それはかなりきちんとしています🤩
そのためには、各レイヤが動作する必要がある方法を「振る舞う」ためにアダプターを「強制」する必要があります.インターフェイスを使用して入出力ポートを定義します.あなたは私と話をしたいなら、あなたはこのようにそれをしなければならないだろう

ブラ・ブラ・ブラ.私はいくつかのコードを見たい!
以来UseCaseInteractor これから始めましょう.
class CreateUserInteractor implements CreateUserInputPort
{
    public function __construct(
        private CreateUserOutputPort $output,
        private UserRepository $repository,
        private UserFactory $factory,
    ) {
    }

    public function createUser(CreateUserRequestModel $request): ViewModel
    {
        /* @var UserEntity */
        $user = $this->factory->make([
            'name' => $request->getName(),
            'email' => $request->getEmail(),
        ]);

        if ($this->repository->exists($user)) {
            return $this->output->userAlreadyExists(
                new CreateUserResponseModel($user)
            );
        }

        try {
            $user = $this->repository->create(
                $user, new PasswordValueObject($request->getPassword())
            );
        } catch (\Exception $e) {
            return $this->output->unableToCreateUser(
                new CreateUserResponseModel($user), $e
            );
        }

        return $this->output->userCreated(
            new CreateUserResponseModel($user)
        );
    }
}
ここで注意を払う必要がある3つのことがあります.
  • インターアクターはCreateUserInputPort インターフェース
  • インターアクターはCreateUserOutputPort ,
  • インターアクターはViewModel その代わりに、それはプレゼンターにそれをするように言います.
  • 以来Presenter (ここで要約)CreateUserOutputPort ) アダプター(緑)層に位置していますCreateUserInteractor 本当に良い例ですinversion of control : フレームワークはユースケースを制御しておらず、ユースケースはフレームワークを制御しています.
    あまりにも退屈に複雑になっている場合は、すべてを忘れて、すべての意味のある決定がユースケースレベルで行われていると考えてください.userCreated , userAlreadyExists , or unableToCreateUSer ). コントローラとプレゼンターはただ従順な奴隷であり、ビジネスロジックを欠いている.
    我々はそれを十分にリハーサルすることができないので、私とそれを歌う:コントローラ👏 SHOULD 👏 ない👏 含む👏 ビジネス👏 論理👏

    では、それはコントローラの視点からどのように見えますか?
    コントローラにとって、人生は簡単です.
    class CreateUserController extends Controller
    {
        public function __construct(
            private CreateUserInputPort $interactor,
        ) {
        }
    
        public function __invoke(CreateUserRequest $request)
        {
            $viewModel = $this->interactor->createUser(
                new CreateUserRequestModel($request->validated())
            );
    
            return $viewModel->getResponse();
        }
    }
    
    あなたはそれがCreateUserInputPort 実際の代わりに抽象化CreateUserInteractor 実装.それは私達に意志の使用ケースを変更し、コントローラをテスト可能にする柔軟性を与えます.後でそれ以上.

    OK、それは本当に簡単で愚かな.プレゼンターはどうですか.
    再び、非常に簡単に:
    class CreateUserHttpPresenter implements CreateUserOutputPort
    {
        public function userCreated(CreateUserResponseModel $model): ViewModel
        {
            return new HttpResponseViewModel(
                app('view')
                    ->make('user.show')
                    ->with(['user' => $model->getUser()])
            );
        }
    
        public function userAlreadyExists(CreateUserResponseModel $model): ViewModel
        {
            return new HttpResponseViewModel(
                app('redirect')
                    ->route('user.create')
                    ->withErrors(['create-user' => "User {$model->getUser()->getEmail()} alreay exists."])
            );
        }
    
        public function unableToCreateUser(CreateUserResponseModel $model, \Throwable $e): ViewModel
        {
            if (config('app.debug')) {
                // rethrow and let Laravel display the error
                throw $e;
            }
    
            return new HttpResponseViewModel(
                app('redirect')
                    ->route('user.create')
                    ->withErrors(['create-user' => "Error occured while creating user {$model->getUser()->getName()}"])
            );
        }
    }
    
    伝統的に、すべてのコードはifs コントローラの終了時.これにより、ユースケースはコントローラーに何が起こったのかを$user->wasRecentlyCreated 例えば、例外をスローすることにより、例えば
    ユースケースによって制御プレゼンターを使用すると、選択して、コントローラに触れることなく結果を変更することができます.どのように素晴らしいですか?

    それで、すべては抽象化に依存します、私は容器が若干の点で関与していると想像します?
    あなたは絶対に正しい、私の良い友人だ!今日は良い会社にいるような気がします.
    ここでどのようにすべてのapp/Providers/AppServiceProvider.php :
    class AppServiceProvider extends ServiceProvider
    {
        /**
         * Register any application services.
         *
         * @return void
         */
        public function register()
        {
            // wire the CreateUser use case to HTTP
            $this->app
                ->when(CreateUserController::class)
                ->needs(CreateUserInputPort::class)
                ->give(function ($app) {
                    return $app->make(CreateUserInteractor::class, [
                        'output' => $app->make(CreateUserHttpPresenter::class),
                    ]);
                });
    
    
            // wire the CreateUser use case to CLI
            $this->app
                ->when(CreateUserCommand::class)
                ->needs(CreateUserInputPort::class)
                ->give(function ($app) {
                    return $app->make(CreateUserInteractor::class, [
                        'output' => $app->make(CreateUserCliPresenter::class),
                    ]);
                });
        }
    }
    
    私はCLIバリアントを追加して、ユースケースをスワップしてユースケースを別にするのはどのように簡単かを示すViewModel インスタンス.ルックアアthe actual implementation 詳細は👍

    テストしてもいいですか.
    おお!それはあなたを懇願している!CAについてのもう一つの良いことは、それがそれが風をテストすることを抽象化にあまりに頼るということです.
    class CreateUserUseCaseTest extends TestCase
    {
        use ProvidesUsers;
    
        /**
         * @dataProvider userDataProvider
         */
        public function testInteractor(array $data)
        {
            (new CreateUserInteractor(
                $this->mockCreateUserPresenter($responseModel),
                $this->mockUserRepository(exists: false),
                $this->mockUserFactory($this->mockUserEntity($data)),
            ))->createUser(
                $this->mockRequestModel($data)
            );
    
            $this->assertUserMatches($data, $responseModel->getUser());
        }
    }
    
    完全なテストクラスが利用可能ですhere .
    私の使用Mockery まあ、モッキングは、何かで動作します.それは多くのコードのように見えるかもしれません、しかし、それは実は書くのが非常に簡単です、そして、それは楽にあなたの使用ケースの100 %の報道をします.

    この実装は、本と少し異なりますか?
    はい.あなたは、CAがJava人々によって設計されているのを見ます.ほとんどの場合、Javaプログラムでは、ビューを更新したい場合は、Presenter .
    PHPではありません.なぜなら、我々は完全にビューを制御していないし、フレームワークが応答を返すコントローラの概念によって構成されているからです.
    それで、私は原則を適応させなければなりませんViewModel 適切な応答を返すためにコントローラにスタックスタックを登る.あなたがより良いデザインを思い付くことができるならば、コメントで私に知らせてください🙏
    あなたは私にコメントで何を考えてお知らせください.私のビジョンに挑戦し、毎日新しいことを学ぶためにそれらの記事を書きます.
    もちろん、デモリポジトリへの変更を提案することによってpull-request . あなたの貢献はありがたい🙏
    この記事は私に4日間の研究、実施、テスト、執筆をしました.私は本当にあなたの社会的ネットワーク上のような🙏
    おかげで、みんな、あなたの貢献は、私はあなたのための記事を書くことをやる気に保つのに役立ちます👍
    更なる読書
  • Clean Coder Blog
  • Entity-Control-Boundary
  • Clean Architecture: Use case containing the presenter or returning data?
  • A button, as a “Clean Architecture” plugin
  • UML Diagrams cheatsheet