Laravel + Vue.js + DDDのすゝめ – ユースケースを作る

Laravel + Vue.js + DDDのすゝめ – ユースケースを作る

ユースケースとは、アプリケーション層に作成するプログラムです。
実際のアプリケーションの機能を実現するためのドメインやリポジトリを組み立てる手続きを定義します。

連載

本記事は複数記事の連載記事の1つです。

この記事に関連するコミット

アプリケーション層のディレクトリ構成

「packages\applications」に配置します。
さらに機能(orders)やユースケース名(find, create)で階層構造を作ります。
ユースケースのディレクトリにはユースケースクラスの他、必要に応じて入力引数の型定義や返却値の型定義のクラスを作成します。

findユースケース

注文IDを指定した1件の注文情報を取得・表示するユースケースです。
OrderFindUseCase.phpを追加します。

1ユースケースにpublicメソッドは1つまで

1ユースケースにpublicメソッドは1つまでを基本として作ります。
よくあるアンチパターンを例に挙げると、「OrderUseCase」という汎用的なユースケースファイルを作ります。
findメソッドを1つ定義します。
おそらく3ヶ月後もすればcreate, update, search…とタケノコのように次々とメソッドが生えているでしょう。

注文のユースケースという何とでも解釈できる曖昧な名前がついていると、アーキテクチャの理解が不十分な開発者は「ここに追加すればいいのかな?」と、それらしい名前のクラスに実装していきます。

Orderのユースケースがまとまってて分かりやすいじゃん」って言われることも多いですが、リファクタリングするときに絶望します。
ユースケースは単体で成り立つものではなく、ドメイン・ドメインサービス、リポジトリなど他のクラスに依存します。
findとcreateはOrderRepositoryを共通で依存しますが、createは別のクラスにも依存している…という関係性が3,4組と増えてくると改修しようとした時に手が止まります。

また、テストを書こうと思った場合にも依存関係にあるクラスがはっきりしないと何をモックにすれば良いのかも分からず、テストを書くのが辛くなっていきます。

例外

個人的な例外として、fetchのバリエーションが色々ある場合はFetchUseCaseとしてまとめることもあります。
fetchById、fetchByName…など、fetchすることは共通でパラメータのバリエーションだけが違う場合はfetchとして集約することもあります。

DIコンテナで依存関係を逆転する

本来、ユースケースクラス(アプリケーション層)はリポジトリ(インフラ層)に依存しないところを逆転させて…と、DIの解説になるので詳しい話はそちらへ任せます。

ソースで語るというのが一応主題なので、LaravelでDIコンテナを使用する流れを記載していきます。

    public function __construct(
        private OrderRepository $orderRepository
    ) {
    }

コンストラクタインジェクションで、OrderRepositoryをDIで解決します。
ソースを見るときは、コンストラクタでDIされている = 依存していると読み取ることが出来ます。

createユースケース

OrderCreateUseCase.phpではOrderを新規作成します。

inputData引数で受け取る

サンプルでは少し単純過ぎるのでアレですが、複雑なユースケースになってくると引数も複雑になってくるため、引数をinputDataクラスとして引数に関わる整合性やnullの場合のデフォルト値などをこのクラスで解決して責務を明確にします。

プリミティブ型の1つ2つ程度であればinputDataを省略して、そのままcreateメソッドの引数にすることもあります。

トランザクションをかける

データベースの整合性を保つためのトランザクションはアプリケーション層で行います。
トランザクションのほとんどはユーザーの要求単位で全て正常に完了すればCOMMIT、一部失敗するようであればROLLBACKが基本なので大体はユースケースの始まりから終わりまでがトランザクション範囲となります。

    public function create(OrderCreateInputData $inputData): int
    {
        $order = Order::create($inputData->orderNumber);

        return DB::transaction(function () use ($order): int {
            return $this->orderRepository->insert($order);
        });
    }

エンティティにドメイン知識を集める

Orderインスタンスを生成するには通常、new Order(...)と生成しますが、あえてstatic関数のcreateをコールしています。

        $order = Order::create($inputData->orderNumber);

コンストラクタ(__construct)のスコープはpublicではなくprivateになっていることで、外部からnewすることが出来なくなります。
さらにcreate(新規作成)メソッドでインスタンス生成したオブジェクトを返却することで、IDが未発行なのでnull、orderDatetime(注文日時)は必ず現在日時というcreateの知識をエンティティで表します。

ユースケースではなくエンティティで表すことで、別のOrderを作成する機能(ユースケース)が出てきた場合でも必ずエンティティのcreateメソッドをコールすることでルールや制約を守ることが出来ます。

reconstructの存在について

通常のコンストラクタと同等の全プロパティを引数に取るメソッドを用意しています。
これはドメイン知識ではなくDBからORマッパーを通してモデルに変換する際にどうしても必要なため、普通のコンストラクタではないことを明示的に表現しています。

private function __construct(
    public readonly ?int $id,
    public readonly string $orderNumber,
    public readonly Carbon $orderDatetime,
) {
    if (strlen($orderNumber) > 10) {
        throw new InvalidArgumentException('注文番号は10文字以内で入力して下さい。');
    }
}

public static function create(string $orderNumber): Order
{
    return new self(
        id: null,
        orderNumber: $orderNumber,
        orderDatetime: Carbon::now()
    );
}

public static function reconstruct(int $id, string $orderNumber, Carbon $orderDatetime): Order
{
    return new self(
        id: $id,
        orderNumber: $orderNumber,
        orderDatetime: $orderDatetime
    );
}

プログラミングカテゴリの最新記事