Wiz テックブログ

Wizは、最新のIoTやICTサービスをお客様に届ける「ITの総合商社」です。

Laravel×クリーンアーキテクチャ

どうもバックエンドエンジニアの小室です。

最近クリーンアーキテクチャについて理解が以前に比べ深まってきました。

まとめがてら、簡単なユーザー作成機能をクリーンアーキテクチャを使い実装していきたいと思います。

まずクリーンアーキテクチャといえば

f:id:shuto_komuro:20210706151155j:plain
クリーンアーキテクチャ
お馴染みの円の図です。

レイヤーの説明

・Enterprise Business Rules

ビジネスロジックを表現するレイヤーになります。DDDによる設計が最も影響し、

DDDでいうEntity, Value Object, Domain Serviceが該当します。

・Application Business Rules

ビジネスロジックを組み合わせ、ソフトウェアは何ができるのか(UseCase)を表現するレイヤーになります。

・Interface Adapters

入力、永続化、出力を司るレイヤーになります。

・Frameworks & Drivers

フレームワークやDBなど、詳細な技術についてを置くためのレイヤーです。 このレイヤーでは、Interface Adapterが理解できる形へ変換される必要があります。

レイヤー間の依存方向

f:id:shuto_komuro:20210709001032p:plain
依存方向

この矢印の意味は依存方向を表します。 依存方向は上位レベルのレイヤーに向かっていきます。

f:id:shuto_komuro:20210709002232p:plain
flow of control

大事なのはここ、リクエストからレスポンスまでの流れを表しています。 もっと詳しく表したが図こちら。

f:id:shuto_komuro:20210709002909j:plain
flow of control

クリーンアーキテクチャのメリット

フレームワーク独立

アーキテクチャは、ソフトウェアのライブラリに依存しない。

フレームワークを道具として使うことを可能にし、システムはフレームワークの限定された制約に縛られない。

・テスト可能

ビジネスルールは、UI、データベース、ウェブサーバー、その他外部の要素なしにテストできる。

・UI独立

ビジネスルールの変更なしに、UIを置き換えられる。

・データベース独立

OracleあるいはSQL Serverを、Mongo, BigTable, CouchDBあるいは他のものと交換することができる。

ビジネスルールは、データベースに拘束されない。

・外部機能独立

ビジネスルールは、単に外側についてなにも知らない。

原文: Clean Coder Blog

クリーンアーキテクチャにおいて大事(コア)な考え

重要なものを些細なものに依存させない。

重要なもの=ビジネスルール

ビジネスルールは図でいう

・赤色の層(Entity: Application Business Rules)

・黄色の層(UseCase: Enterprise Business Rules)

の2層の部分です。

些細なもの = DB, UI, フレームワークなど

重要なものを些細なものに依存させないために「依存関係の逆転」を行う。

※「依存関係の逆転」後述してます。

実装コード

ルーティング

<?php

Route::post('/createUser', [\App\Http\Controllers\UserController::class, 'createUser']);

Controller

<?php

namespace App\Http\Controllers;

use App\Packages\Domain\User\Dto\UserInputData;
use App\Http\Requests\UserCreateRequest;
use App\Packages\UseCase\User\CreateUserUseCaseInterface;

class UserController extends Controller
{
    /**
     * @param UserCreateRequest $request
     * @param CreateUserUseCaseInterface $userCreateUseCase
     * @return \Illuminate\Http\JsonResponse
     */
    public function createUser(
        UserCreateRequest $request,
        //②Input Boundary<I>
        CreateUserUseCaseInterface $userCreateUseCase
    )
    {
        //①Input Data<DS>
        $userInputData = new UserInputData($request['name'], $request['tel']);
        //③UseCase Interactor
        $userOutputDataResponse = $userCreateUseCase->handle($userInputData);

        return response()->json([
            'uuid' => $userOutputDataResponse->getUuid()
            ],200);
    }
}

①でリクエスデータをアプリケーション用Input Dataに変換します。

②インターフェースクラスを定義してメソッドインジェクション(DI)をしています。

DIについては Laravelで始める依存性の注入(DI) - Qiita

環境変数に応じてRepositoryをInMemoryに切り替えたりもできます。

<?php

namespace App\Providers;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        if(app()->environment() == 'local') {
            $this->app->bind(
                'App\Packages\Infrastructure\User\RepositoryInterface',
                'App\Packages\Infrastructure\User\InMemoryRepository'
            );
            $this->app->bind(
                'App\Packages\UseCase\User\CreateUserUseCaseInterface',
                'App\Packages\UseCase\User\CreateUserInteractor'
            );
        }else{
            $this->app->bind(
                'App\Packages\Infrastructure\User\RepositoryInterface',
                'App\Packages\Infrastructure\User\UserRepository'
            );
        }
    }

③UseCase InteractorにInput Dataを渡します。

Input Data<DS>

<?php

namespace App\Packages\Domain\User\Dto;

class UserInputData
{
    private $name;
    private $tel;

    public function __construct($name, $tel)
    {
        $this->name = $name;
        $this->tel = $tel;
    }

    /**
     * @return mixed
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @return mixed
     */
    public function getTel()
    {
        return $this->tel;
    }
}

?>

いわゆるDTOってやつです。 ※ <DS>はData Structure

Input Boundary

<?php

namespace App\Packages\UseCase\User;

use App\Packages\Domain\User\Dto\UserInputData;

interface CreateUserUseCaseInterface
{
    public function handle(UserInputData $userInputData);
}

?>

Use Caseのインターフェースクラスになります。

実装クラス(UseCase Interactor)で必要となるメソッド名を定義します。

DIを使うことで、Controller層はUseCase層の実際の実装クラス(UseCase Interactor)ではなく、

インターフェースクラス(Input Boundary)に依存することになり、Use Case層への依存度を緩和します。

※ <I>はInterface

UseCase Interactor

<?php
namespace App\Packages\UseCase\User;

use App\Packages\Domain\User\User;
use App\Packages\Domain\User\Dto\UserInputData;
use App\Packages\Domain\User\Dto\UserOutputData;
use App\Packages\Domain\User\VO\Name;
use App\Packages\Domain\User\VO\Tel;
use App\Packages\Infrastructure\User\RepositoryInterface as UserRepositoryInterface;

class CreateUserInteractor implements CreateUserUseCaseInterface
{
    private $userRepository;

    //①Data Access Interface<I>
    public function __construct(UserRepositoryInterface $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    /**
     * @param UserInputData $userInputData
     * @return UserOutputData
     * @throws \Exception
     */
    public function handle(UserInputData $userInputData)
    {
        $uuid = uniqid("user_");
        //②Entity
        $user = new User(
            $uuid,
            new Name($userInputData->getName()),
            new Tel($userInputData->getTel()));
        //③Data Access
        $this->userRepository->save($user);
        //本来はPresenterを用意するが、MVCFWの都合上OutPutDataをレスポンス
        $userOutputData = new UserOutputData($uuid);
        return $userOutputData;
    }
}

?>

UseCase Interactorではアプリケーションロジックを実装します。

①Data Access InterfaceはRepositoryのインターフェースクラスになります。

実装クラスで必要となるメソッドを定義します。

②User Entiryのオブジェクトを生成しています。

③User EntiryのオブジェクトをRepositoryに渡し、保存します。

Entity

<?php

namespace App\Packages\Domain\User;

use App\Packages\Domain\User\VO\Name;
use App\Packages\Domain\User\VO\Tel;

class User
{
    private $uuid;
    private $name;
    private $tel;

    public function __construct(
        $uuid,
        Name $name,
        Tel $tel
    )
    {
        $this->uuid = $uuid;
        $this->name = $name;
        $this->tel = $tel;
    }

    /**
     * @return string
     */
    public function getUuid() :string
    {
        return $this->uuid;
    }

    /**
     * @return Name
     */
    public function getName() :Name
    {
        return $this->name;
    }

    /**
     * @return Tel
     */
    public function getTel() :Tel
    {
        return $this->tel;
    }

    /**
     * @param Name $name
     */
    public function changeName(Name $name)
    {
        $this->name = $name;
    }
}

?>

エンティティはビジネスルールをカプセル化したオブジェクトです。

クリーンアーキテクチャでいうEntityはDDDでいうEntityとは定義幅が異なります。

DDDドメインモデルはEntityとValue Objectとドメインサービスを含んだものを指します。

クリーンアーキテクチャのEntityは、DDDドメインモデルにあたります。

ドメイン駆動設計のエンティティと比べるとその定義幅はかなり広いです。

UseCase層で受け取ったデータをもとにEntityのオブジェクトを作成しています。

EntityのValueObject

<?php

namespace App\Packages\Domain\User\VO;

use Exception;

class Name
{
    private $value;

    public function __construct(string $value)
    {
        if(strlen($value) < 3) throw new Exception();
        if(strlen($value) > 20) throw new Exception();
        $this->value = $value;
    }

    /**
     * @return string
     */
    public function getValue() :string
    {
        return $this->value;
    }
}

?>

User Entiryを構成するValue Objectになります。

DataAccessInterface

<?php

namespace App\Packages\Infrastructure\User;

use App\Packages\Domain\User\User;

interface RepositoryInterface
{
    /**
     * @param User $user
     * @return mixed
     */
    public function save(User $user);

    /**
     * @param $userId
     * @return mixed
     */
    public function find($userId);
}

?>

Repositoryのインターフェースクラスを定義しています。

DataAccess

<?php

namespace App\Packages\Infrastructure\User;

use App\Packages\Domain\User\User;
use App\Models\User as UserModel;

class UserRepository implements RepositoryInterface
{
    /**
     * @param User $user
     * @return mixed|void
     */
    public function save(User $user)
    {
        $userModel = new UserModel();
        $userModel->uuid = $user->getUuid();
        $userModel->name = $user->getName();
        $userModel->tel = $user->getTel();
        $userModel->save();
    }

    public function find($userId)
    {
        $userModel = UserModel::findOrFail($userId);
        ...
    }
}
?>

Repositoryクラス(Data Access)を定義しています。

Repository Interface(Data Access Interface<I>)を継承しています。

DIにより、Data AccessはUseCase層であるData Access Interface<I>に依存することになります。

InMemoryRepository

<?php

namespace App\Packages\Infrastructure\User;

use App\Packages\Domain\User\User;

class InMemoryRepository implements RepositoryInterface
{
    protected $data;

    public function __construct()
    {
        $this->data = collect();
    }

    public function all()
    {
        return $this->data;
    }

    public function save(User $user)
    {
        $this->data->push([
            'name' => $user->getName(),
            'tel' => $user->getTel()
            ]);
    }

    public function find($userId){}
}

DIにより、ロジックが疎結合になります。

特定のインフラストラクチャに依存しないようにロジックを記述できます。

インメモリに切り替えることも容易になります。

まとめ

俯瞰してみると、重要なビジネスルールであるEntity層とUseCase層はフレームワークを変えたとしても、

特に変更はなくそのまま使うことができる様な形になり、フレームワークに依存していないと、見て取れます。

UIや、データベースなどの変化しやすいレイヤーと

ビジネスロジックという変化しづらいレイヤーを分離することができました。

最後に

Wizではエンジニアを募集しております。

興味のある方、ぜひご覧下さい。

careers.012grp.co.jp