🏔️

Laravel で DDD のレイヤードアーキテクチャを試す

18 min read

まえがき

一年半かっちりとした設計を頑張ってみて、なんとなく形が見えてきたので、共有しようと思います。
タイトル通り、DDD の戦術の話がメインです。

いろんなデザインパターンを勉強しましたが、その中でも効果の解りやすいもののみを取り入れることで、迷い少なく方針を決めてこれました。
途中で設計変更は何度も行っていますし、設計変更することを前提に設計してます。
まだ悩んでる部分もいくつかあります。最後の方に書いています。

目的

設計するにあたって、以下の目的が達成できることを重視している。

  • 業務知識があればプログラミングがわからなくてもなんとなくわかるようにする
  • 部品ごとの役割を明確にする
  • 部品を使い回しできるようにする
  • 部品をテスト可能にする
  • 状況に合わせて設計方針をどんどん変えていく
    • 新しい書き方と古い書き方を混在させやすくする
    • リファクタリングしやすくする
    • 使わなくなった部品を簡単に削除できるようにする
  • フレームワークへの過度の依存を防ぐ

効果の解りやすい設計手法を取り入れ、それ以外は保留にする。
最初から完璧を目指さず、緩く書いてもいい場所を用意する。

構成

概要

DDD のレイヤードアーキテクチャに倣い、以下の 4 つのレイヤーに分割する。

  1. UI
  2. Application
  3. Domain
  4. Infrastructure

それぞれのレイヤーは下位のレイヤーにのみ依存する。
Infrastructure 層だけは Interface を用意し、他のクラスが実装ではなく Interface に依存するようにしている。
依存性の逆転だが、解りやすさを重視して Interface も Infrastructure 層に置くことにした。

Domain 層と Infrastructure 層はなるべく綺麗に保つ。
UI 層と Application 層は多少汚くなっても構わない。

名前空間

※ 斜体はファイル

  • App
    • Common …… システム全体に関すること
    • Http
      • Middleware
        • Middleware …… 汎用的な Middleware
      • Controllers
        • Controller …… Laravel で用意されてる Controller の親クラス
      • Kernel
    • Console
      • Commands
        • Command …… システム全体に関わるコマンド
      • Kernel …… 各 Context の Commands もロード
      • Providers
        • ServiceProvider …… Laravel の ServiceProvider
      • Exceptions
        • Handler …… Handler はこっち
      • Listeners
        • EventListener
    • Support …… 汎用サブドメインとして分離するには規模の小さいもの
    • (Context)
      • UI
        • Http
          • Requests
            • Request …… 実装は Validation と型の変換程度に留める
          • Middleware
            • Middleware
          • Controllers
            • Action …… 1 Action 1 Class で実装
          • Resources
            • Resource …… API リソース
          • Responders
            • Responder …… レスポンスを作成
        • Console
          • Commands
            • Command
          • Register …… Kernel から呼び出してコマンドを登録
      • Mail
        • Mail …… 送信は Infrastructure 層で行う
      • Notifications
        • Notification
        • Jobs …… 必要になったら
          • Job
        • Exceptions
          • Exception
      • Application …… Domain 層を汚すくらいならこっちを汚す
        • UseCases …… Application Service
          • UseCase
        • Exceptions
          • Exception
      • Domain
        • Entities
          • Entity …… モデルから永続化を除いたもの
        • ValueObjects
          • ValueObject …… 値に対する制約を型によって実現する
        • Services …… Domain Service
          • Service
        • Events
          • Event …… Laravel の Event と混ぜてしまった方がらくそう
        • Exceptions
          • Exception
      • Infrastructure
        • Contracts
          • Commands
            • CommandContract …… Command の Interface
          • Queries
            • QureyContract …… Query の Interface
        • Commands
          • Command …… 更新系の処理を 1 Class 1 Public method で実装
        • Queries
          • Query …… 参照系の処理を 1 Class 1 Public method で実装
        • Eloquents
          • Eloquent Model …… なんだかんだ出番は多い
        • Exceptions
          • Exception

説明

App

  • Laravel の構成に倣って App は残している
    • 規模が大きくなってきたら分離しやすくするために見直しをするかも

Common

  • フレームワークの制御やシステム全体に関するコードを置く
    • Laravel の機能をカスタマイズするためのコードもここに置く
  • 全く性質の異なるアプリケーションでも、Laravel を使っていたら使う可能性のあるものを置く
  • Kernel や Handler のような全体を制御するプログラムもここに置く
    • 個別の処理は各 Context のクラスに書き、それを呼び出すだけにする

Support

  • ドメイン知識に依存しないちょっとしたツールを置いておく
  • 汎用サブドメインとして分離するには規模の小さいもの
    • サブドメインなので Util のような名前にはしない
  • いずれ composer モジュールにしたいが、まだそこまで作り込めていないもの
  • composer モジュールをラップしたものなど

(Context)

  • 境界付けられたコンテキスト
    • ドメインとも呼ぶ
  • ドメインを表す名前を付ける
    • プロダクト名とか部署名とか
  • アプリケーション全体を制御するコードや、Laravel をカスタマイズするためのコードは、まとめて Common に置く
    • それ以外は境界付けられたコンテキストごとに分けて置く
  • コンテキストによって用語の意味が変わるため、それらを混ぜないようにする
  • 例えば、外部サービスを扱うためのコードが増えた場合は、外部サービスの名前で Context を作成する
    • 外部サービスと自前のアプリケーションとでは使ってる用語が違うため
    • 規模が小さい内は App\Support などに置いてもいい

UI

  • この層のコードが基点となり、下の層のコードが呼ばれる形になる
    • Http / Console / Job など
  • フレームワークの機能はだいたいこの層に詰め込んでいる
    • まとめて置いた方が扱いやすい
  • この層にはビジネスロジックはほとんど書かないようにする
    • 下の層の機能を呼び出すだけにする
  • Mail や Notification のような View に関するコードもここに置く
    • 送信処理は Infrastructure 層に書く

UI > Http > Request

  • Laravel の Request
  • HTTP リクエストを扱うクラス
    • HTTP で送られた値をバリデーションする
    • HTTP で送られた値を内部で使う型に変換する
      • 日付文字列を CarbonImmutable 型に変換
      • 数値を ValueObject に変換
  • 実装するのはバリデーションと型の変換程度に留める
    • 親クラスに機能が多くテストしづらいため
  • ルートパラメータの取得も Request 経由で行う
    • /users/{user}{user} の部分の値
  • クラス名の末尾に Request を付ける

UI > Http > Controllers > Action

  • Laravel の Controller/Action
  • Web インタフェースに関する以下のような処理を行う
    • Request からパラメータを取得
    • パラメータによる条件分岐
    • 必要なら Entity を作成
    • UseCase を使って機能を実現
      • Infrastructure 層のコードを直接呼び出してもよい
    • Responder を使って Entity や ValueObject から Response を作成
  • テストを書きづらいため、なるべく他のクラスに書けないか検討する
    • 入力値に関することは Request
    • 出力値に関することは Reponder
    • それ以外は UseCase
  • Public メソッドは __invoke() ひとつだけ
    • Protected/Private メソッドは何個作ってもよい
  • 同じリソースに対する Action は同じディレクトリに入れることで整理する
    • 例: Http\Controllers\Users\ShowUserAction
  • 依存するクラスはコンストラクタで受け取る
    • Request クラスだけは実行する直前に受け取りたいため、__invoke() で受け取る
  • クラス名の末尾に Action を付ける
  • イミュータブル
    • プロパティには依存するクラスのインスタンスのみを持つ

UI > Http > Resource

  • Laravel の API リソース
  • ドメインモデルを API レスポンスの形に整形する役割を持つ
    • 整形処理をドメインモデルに書かなくて済む
  • 基本的に Entity を扱うことになるため、Laravel における API リソースの全ての機能を使える訳ではない
    • Eloquent Model を扱うことを想定して作られているため

UI > Http > Responder

  • HTTP レスポンスを作成する
    • レスポンスボディと、HTTP ステータスコードやヘッダーをここで作成する
      • API の場合はレスポンスボディは Resource で作れるため、ここでは toResponse() するだけ
      • Web ページの場合はここで View を生成する
  • Public メソッドは __invoke() ひとつだけ
    • Protected/Private メソッドは何個作ってもよい
  • クラス名の末尾に Responder を付ける
  • イミュータブル
    • プロパティには依存するクラスのインスタンスのみを持つ
  • ADR の Responder
  • クリーンアーキテクチャの Pressenter

UI > Console > Command

  • Laravel の Artisan Command
  • Console における Controller の役割
  • ここには具体的な処理は書かない
    • UseCase などの下のレイヤーのクラスを呼び出す
  • コマンドライン引数など、入力値の操作が複雑になりそうだったら、Http の Request に該当するクラスを作った方がよい
  • 画面出力やログ出力は Event を使うか、モックしやすい実装にした方が、テスト実行時にログが汚れなくて済む
  • デバッグ用だったり使い捨てのコマンドは、雑に作ることもある
    • ヘルパー関数を使ったり

UI > Mail

  • Laravel の Mail
  • View なので UI 層に置いた
  • 送信処理は Infrastructure 層に実装する

UI > Notification

  • Laravel の Notification
  • Mail と同じ場所に置くことにした

UI > Job

  • Laravel の Job
  • アプリケーションを起動するトリガーになり得るので、Http や Console と同じ場所に置いた
  • 中の処理は UseCase などに実装するのが望ましい

Application

  • 人によっては Controller をこの層に置く人もいるが、UI 層に置くことにした
    • フレームワークの機能を UI 層に寄せた
  • 中身は今のところ UseCase のみ

Application > UseCase

  • アプリケーションでやりたいこと、ひとかたまりのタスクを扱う
    • 例えば「ユーザーを登録する」など
      • 登録後にメールを送るといった、付随する処理も含む
    • オブジェクトではなく、タスクに関心があるのが特徴
      • そこが Service (Domain Service) との大きな違い
  • タスクの中にはいくつかの作業が含まれる
    • 例えば、ブログを書くというタスクの中には、写真をクラウドストレージに保存、記事をデータベースに保存、通知を購読者に送るといった作業が含まれる
  • タスクごとにクラスを分ける
    • 初めは、ひとつの Action に対して、ひとつの UseCase を実装するとよい
    • ひとつの Action から複数の UseCase を呼んでもいい
    • UseCase から UseCase を呼んでもいい
  • Web やバッチなどのインタフェースに依存させない
  • 入力値や出力値の変換は、Request や Resource、Responder で行うためここでは扱わない
  • Public メソッドは __invoke() ひとつだけ
    • クラス名を動詞から始める
    • Protected/Private メソッドは何個作ってもよい
  • イミュータブル
    • プロパティには依存するクラスのインスタンスのみを持つ
  • DDD の Application Service
  • クリーンアーキテクチャの Use Case

Domain

  • ドメインロジックを置くところ
  • なるべくフレームワークの機能に依存させずに実装した方がよい
    • フレームワークの寿命よりドメインロジックの寿命の方が長いため
  • 役割を明確にして綺麗に保つのが望ましい
    • 設計に迷いがある場合は、まずは UseCase の方に汚く書く

Domain > ValueObject

  • 値を扱う
    • ひとつの値を変えると、必ず他の値にも影響する値同士は、複数まとめて扱うこともある
      • 例えば
        • 郵便番号、都道府県、市区町村、番地
        • 苗字、名前
  • 値に対する制約を型によって実現する
    • 例えば
      • ユーザーネームにはアルファベットしか使えない
      • 郵便番号は 7 桁の整数
      • 性別には雄と雌しか存在しない
      • 支払金額は合計金額×税率で決まる
    • コンストラクタで値を設定したら、以後は値の変更は行わない
      • 値を変更したい場合は、新しい ValueObject インスタンスを作成する
        • これは String のような組み込み型と同じ
    • 値が制約に反する場合は、コンストラクタで例外を発生させる
      • ValueObject のインスタンスであれば、その中の値は制約に従っていることを保証する
  • 中の値に対する処理のみを実装する
    • 複数の値を扱うような処理は Entity や Service に書く
      • 他の値との比較程度なら実装してもよい
  • イミュータブル
    • 値を変更したい場合は再代入する
  • Domain 内では全ての値を ValueObject で扱うのが理想
    • クラスを大量に生成することになるが、それであっている
    • まずは Enum 型などの制約や処理を伴うものから

Domain > Entity

  • いくつかの値をひとまとめにしたもの
    • ひとつの値が変更されたからといって、他の値には必ずしも影響しない
  • MVC の Model からデータだけを取りだしたものと考えるとわかりやすいかもしれない
    • 言い換えると、モデルから永続化に関する処理を除いたもの
      • もちろん見た目などの表現に関する処理も含まない
    • 必ずしも 1 テーブル 1 Entity ではない
      • 集約
  • Entity 同士が同一であるかどうかを識別するための識別子を持つ
    • 普通は ID
    • 識別子が一致するかどうかで、同じものを指しているかどうかを判断する
      • DB のテーブルにプライマリーキーがあり、その値によってどのレコードを指しているかを判断しているのと同じ
  • 値の変更にはセッターを使う
    • セッターで値の妥当性を検証しつつ値の変更を行う
    • セッターが用意されていない値は変更できない
  • ミュータブル
    • 状態を持つ
  • 全ての値を ValueObject の形で持つことが理想
    • 大変なので string、bool、CarbonImmutable などを使っても構わない
  • 単一のインスタンスに対する処理だけを書く
    • 複数のインスタンスにまたがる処理は Service や UseCase などに書く

Domain > Service

  • 永続化に関する処理を除いた、Entity や ValueObject に対する操作を実装する
  • タスクではなくオブジェクトを扱うのが特徴
    • MVC だと Model に該当する
      • UseCase は Controller に該当
  • オブジェクトに関する処理なのに、Entity にも Command/Query にも書けないような処理を書く
    • 例えば
      • 他の Service や Command/Query に依存した処理
      • Entity や ValueObject の集合を扱う処理
      • ある型の Entity を、別の型の Entity に変換するような処理
    • 実際に実装してみると結構出てくるはず
      • まず他の場所に実装できないかを検討してから、Service への実装を検討する
        • 何も考えないと、Service が肥大化しやすい
        • 役割がわかりにくいので、なるべくなら使わない
  • Public メソッドは __invoke() ひとつだけ
    • クラス名を動詞から始める
    • クラス名の末尾に Service を付ける
    • Protected/Private メソッドは何個作ってもよい
  • イミュータブル
    • プロパティには依存するクラスのインスタンスのみを持つ
  • DDD の Domain Service
    • DDD の人も Domain Service の扱いは難しいと言っている

Domain > Event

  • Laravel の Event
    • 発生させたイベントはフレームワークのイベントリスナーで処理される
  • 依存関係を分離させるために使う
  • Domain 層はフレームワークに依存しないように書くのが望ましいが、Event は Laravel の Event をそのまま使った方が実装がらく
  • 将来的に CQRS + ES の ES でも使うかもしれないが、今は考えない

Infrastructure

  • 永続化に関する処理を書く
  • Eloquent Model もここに置く
  • 外部サービスを呼び出すようなコードもここに置く
    • API クライアント
      • Command/Query
    • メールの送信処理
      • Command
    • 通知の送信処理
      • Command

Infrastructure > Command

  • Entity の永続化に責任を持つ
    • Command は副作用のある更新系の処理のみを扱う
      • 更新と同時に参照もすることは、今の段階では許す
    • 永続化と関係ない処理は極力書かないようにする
      • それらは Entity や Service に書く場合が多い
  • ひとつの Command で複数のテーブルを扱うこともある
    • DDD の集約単位
    • 他にも水平分割や垂直分割されたテーブルは、ひとつの Command で扱う
  • 他の Command/Query や、Service に依存させない
    • 依存する処理は Service (Domain Service) に書く
  • 内部で Eloquent を使ってよい
    • 使わなくてもよい
      • Query Builder や他の ORM を使ってもよい
  • Interface を公開し、他のクラスからは Interface に依存させる
    • 依存性の逆転 (DIP)
    • Interface は Infrastructure > Contracts > Commands に置く
    • サービスコンテナで Interface と実装を紐付ける
  • Public メソッドは __invoke() ひとつだけ
    • クラス名を動詞から始める
    • クラス名の末尾に Command を付ける
      • UseCase と区別するため
    • Protected/Private メソッドは何個作ってもよい
  • イミュータブル
    • プロパティには依存するクラスのインスタンスのみを持つ
  • CQRS の Command

Infrastructure > Query

  • Entity の永続化に責任を持つ
    • Query は副作用のない参照系の処理のみを扱う
      • 参照と同時に更新も行われる場合は、Command にする
    • 永続化と関係ない処理は極力書かないようにする
      • それらは Entity や Service に書く場合が多い
  • ひとつの Query で複数のテーブルを扱うこともある
    • DDD の集約単位
    • 他にも水平分割や垂直分割されたテーブルは、ひとつの Query で扱う
  • 他の Command/Query や、Service に依存させない
    • 依存する処理は Service (Domain Service) に書く
  • 内部で Eloquent を使ってよい
    • 使わなくてもよい
      • Query Builder や他の ORM を使ってもよい
  • そこまで安全に作り込まなくてよい
    • 開発速度を優先
    • 実行速度を優先
    • DDD のルールを破ってもよい
      • 外部とのデータの受け渡しに、Entity を使わず、配列などを使ってもよい
    • テストもほどほどでよい
      • UseCase 側でちゃんとテストされていれば、書かなくてもよい
  • Interface を公開し、他のクラスからは Interface に依存させる
    • 依存性の逆転 (DIP)
    • Interface は Infrastructure > Contracts > Queries に置く
    • サービスコンテナで Interface と実装を紐付ける
  • Public メソッドは __invoke() ひとつだけ
    • クラス名を動詞から始める
    • クラス名の末尾に Query を付ける
      • UseCase と区別するため
    • Protected/Private メソッドは何個作ってもよい
  • イミュータブル
    • プロパティには依存するクラスのインスタンスのみを持つ
  • CQRS の Query

Infrastructure > Eloquent

  • Laravel の Eloquent Model
  • 認証とか Factory でもこれを使う
  • PHP と DB との構造の違いを吸収する役割を持つ
    • 例えば PHP の bool 型を MySQL の TINYINT 型で保存している場合、Eloquent で Boolean に変換する
      • ミューテタ
      • スコープ
  • 太りやすいのであまり機能を盛り込まない
    • 複雑なスコープは作らず、Command/Query に寄せた方がいい
    • 入力には PHP が持つ原始的な型のみを使い、ドメインモデルには依存させない方がよい
      • ValueObject の原始的な型への変換は、Command/Query で行う
  • Eloquent モデルを Entity に変換する処理は書いてもいいことにする
    • これがあるとらく
  • Eloquent Model 以外から使う場合は as で Suffix に Model を付けた別名を付ける
    • Entity と区別するため
  • 名前空間名が長くなるので、Tinker では別名を付けて短い名前で扱えるようにしておくとらく

Exception

  • PHP の Exception
  • 例外を扱う
  • Exception は発生させる各レイヤーに配置する
    • UI / Application / Domain / Infrastructure
  • Exception の名前には、具体的な例外内容を付ける
  • 似たような例外だからと、同じ Exception を使わない方がよい
    • 同じ抽象クラスを持たせるのはあり
    • 将来的に、全ての例外に対して個別の処理を実装する可能性があることを意識する
  • 処理されなかった例外はフレームワークの例外ハンドラで処理される
    • Laravel で扱う render() メソッドは実装しない方がいい
      • Handler で Responder を使ってレスポンスを作成した方がいい
    • 全ての例外に getContext() を実装して、例外発生時の変数の値をログに出すようにするとデバッグしやすい

参考にしている設計手法

  • DDD
  • ADR
  • CQRS
  • クリーンアーキテクチャ
  • 独立したコアレイヤパターン
  • 削除しやすい設計

検討事項

  • 名前空間とりあえず App の下に置いてるけど、パッケージ名のルールに従うべき?
  • 名前空間が長い
    • そういうものと思って受け入れる
  • Http 以下に Re- から始まる名前空間が多過ぎてタブで補完しづらい
    • どうしようもない
  • 認証周りをどこに実装する?
    • Laravel の認証機能を Service でラップして使っている
  • イベントをどうやって発行させる?
    • ヘルパー関数の event() を使っている
      • ヘルパー関数はこれ以外使っていない (テスト以外)
  • 一部のカラムのみを扱うときに Entity どうする?
    • nullable にする?
    • 別の Entity を作る?
      • こっちの方がいいかも
    • 配列を使う?
      • Query 系ならこれでもいいと思う
  • Laravel の Policy 機能をどう扱う?
    • 使わない方がいい気がする
      • ドメインロジックはなるべくフレームワークに依存させない方がいい
  • Interface は必要?
    • エディタでコードジャンプができなくなるデメリットに対してメリットが小さい
    • Mockery とサービスコンテナでなんとかなっちゃう
    • 書かなきゃいけないコード量も増えるしメンテも大変
      • Interface はコマンドで自動生成できるようにしたい
    • ServiceProvider で Interface と実装を紐付けるコードが大量に発生
  • Collection の中身の型を制限できない
    • 中身の型ごとに専用の Collection を用意すべきなのか?
  • 単体テストでも resolve() でインスタンス作ったり、モックに差し替えるのに $this->mock() 使ったりしたい
    • Tests\TestCase を使うことを許容する
  • Infrastructure 層のテストに DB 使いたい
    • 単体テスト扱いだけど DB 使うことにする
  • DIP したなら Infrastructure 層の Interface は Domain 層に置くべき?
    • 解りにくいのでやめた

参考資料

DDD

ADR

クリーンアーキテクチャ

CQRS

独立したコアレイヤパターン

デザインパターン

その他