WHITEPLUS TechBlog

株式会社ホワイトプラスのエンジニアによる開発ブログです。

DDDのチーム理解度をレベルMAXにする方法(アーキテクチャ編)

この記事はCalendar for WHITEPLUS | Advent Calendar 2021 - Qiitaの21日目の記事です。

はじめに

ホワイトプラスでモバイルアプリ&Webエンジニアをしている仲見川です。 私の所属するマーケティング部システム開発GではアプリチームとWebチームがあり、現在私は兼務として両方ともタイミングによって比重は変わりつつ担当しています。

昨年Androidアプリのこれからの設計を考えた - WHITEPLUS TechBlogこちらの記事でAndroidアプリのアーキテクチャ変更をアドベントカレンダーのネタにしていて、 今年はWeb(PHP)のDDDのチーム理解度を向上させるためのアーキテクチャのお話をネタにしています。

元々ベースとなるアーキテクチャはあったのですが、今回改めて資料にまとめてチーム内で議論しつつわかりやすさや実装の負担などを検討して変化させています。

DDDのチーム理解度をレベルMAXにするために

DDDを体系的に学ぶのはなかなか難しく、新規で参加したメンバーでもキャッチアップしやすい方法としてボトムアップで実現する事にしました。

弊チームではボトムアップでDDDの理解度を上げるためにまずは補助輪としてアーキテクチャを活用しています。

今回はアーキテクチャに話を絞っているため以降DDDの話はでてきません。あしからず。

弊チームで採用しているアーキテクチャ

アーキテクチャの資料の「はじめに」には以下のように記載されていて、必要に応じて変化させて行く物としています。

このドキュメントを元により良い(チームに合う)アーキテクチャにしていくのが目的です。 もっとこうの方が分かりやすい、設計しやすい、実装しやすいなど議論しつつ成長させて行きましょう。

概要

前提としてオニオンアーキテクチャやクリーンアーキテクチャに近い考え方をしています。 f:id:nakamigawa:20211214155852p:plain

図上のUseCaseはリネットではApplication/UseCaseと呼んでいます。 EntitiesはDomainModelと読み替えてください。

依存性(参照)は外側から内側にのみ向かいます。 (Controller -> Application/UseCase -> DomainModel)

参照関係と実装

f:id:nakamigawa:20211214160438p:plain ↑の参照をもうちょっと細かく書くと以下

f:id:nakamigawa:20211214163223p:plain

図ではApplicationService、ACLは省略。 DomainServiceはややこしい部分があるので一部省略しています。

リネットのアーキテクチャの基本的な考え方として、 Application/UseCaseにてRepositoryからDomainModelを生成し取得する構造です。 DBに保存したい場合は逆でModelを生成してsaveする形をとります。 Modelの生成は複雑では無い場合はApplication/UseCaseにて行い複雑な場合はDomainServiceとしてFactoryクラスを作成する事もあります。

他のBCから値をもらう、処理を委譲する等はDomainServiceにInterfaceを作成しApplication層のACLとして実装を行います。

Repository以外のQueryやCommandはDomainModelを使わずにDB等への操作を行う場合を想定しているのでApplication層にInterfaceを置きます。

DomainModel自体はイミュータブルで生成された時点で完成しているValueObjectを前提としています。 現状はいわゆるミュータブルなEntityは使っていません。今後必要なタイミングで検討したいと思います。

// 疑似コードです
class LenetUserUseCase
{
    // LaravelのDIコンテナを使ったConstructorInjectionでInterfaceに実装を注入
    public function __construct(
        UserRepository $repository
    {
        $this->repository = $repository;
    }
    
    public function get(int $userId): UserDomainModel
    {
        // ここのケースではシンプルに取得した情報を表示したいのでmodelをControllerに返す
        return $this->repository->of(UserId::of($userId));
    }
    
    public function save(int $userId): void
    {
        $user = UserDomainModel::ofByInt($userId); // 実際には集約にorderIdしか値が無い事はないですが・・・
        return $this->repository->save($user);
    }
}

モデルに対して何か操作を行った結果を保存したい場合もApplicationUseCaseでは行わずにDomainに処理を実装します。

各レイヤーの役割とディレクトリ構成

- Controller        // ユースケースを呼び出しドメインモデルを受け取って画面表示などを行う
  |                 // ここはドメンモデルを利用するだけでドメインモデルに対して変更は行わない
  |                 // 一部を取り出してロジックを噛ます等も微妙(UIの都合もあるので絶対ダメではない)
- 境界付けられたコンテキストで切られたモジュール
  |- App
  |  |- UseCase    // ユースケース単位の実装をします。業務的に価値のある単位で処理をまとめます。
  |  |             // 例えばお知らせ一覧を取得し、既読にして返すなど。
  |  |             // お知らせ一覧を取得するだけが目的で副作用が無ければ取得して返すだけもユースケースです。
  |  |             // 場合によってはRepositoryを呼び出すだけのProxyメソッドでしかない場合もあります。(リネットのアーキテクチャではままある)
  |  |             // UIとのPOI(point of interface)で画面から渡されたプリミティブな値はここでドメインモデルに変換する
  |  |             // RepositoryやQueryから集約やデータをもらう事に徹する、ロジックらしいロジックは書かない(ループがあったらスメル
  |  |
  |  |- ACL        // 腐敗防止層、別のBCの処理を呼び出しを書く。
  |  |             // 別BCのドメインモデルを自分のBCのドメインモデルに変換する役割を担う。
  |  |
  |  |- Command    // ドメインモデルを介さない一括登録(のInterface)
  |  |- Querie     // ドメインモデルを介さない抽出(Count等)(のInterface)
  |
  |- Domain
  |  |- Model      // ドメインモデル、ValueObjectであり。副作用を持たない振る舞いを持つ。
  |  |             // 基本的には業務ロジックはここに集まるように作る。(UIを除く)
  |  |             // ここがただのデータの入れ物でしか無いときは一歩ひいてここに書くべきロジックが別の所に書かれていないか確認する
  |  |             // もちろん絶対ロジックがあるわけではありませんが、特に○○Listなどのドメインモデルをグルーピングしている
  |  |             // ドメンモデルが何も持っていなかったらおそらく書くべき処理が漏れ出ています
  |  |             //
  |  |             // ドメインモデルの初期化時にはドメインモデルとして持つ物はドメインモデルを引数として渡すようにしましょう
  |  |             // プリミティブ型の場合、誤ったものでも渡すことが出来てしまうためです(型付言語のパワーを活かしましょう!)
  |  |             // 例) Bad     Hoge(title: String, time: String)
  |  |             //     Good    Hoge(title: Title, time: DateTime)
  |  |             //     呼び出し Hoge(title: Title(string: "たいとる"), time: DateTime(string: stringTime))
  |  |             // こうすることで、正しく無いモデルの生成を防ぐ事が出来ます。
  |  |
  |  |- Repository // ドメインモデルの生成とStore(のInterface)
  |  |
  |  |- Service   // ドメインモデルで表現が難しい処理を書く
  |  |            // ドメインモデルからインフラアクセスを行う必要のある処理などはここにInterfaceを置いてInfraで実装する
  |  |
  |  |- Type            // 境界付けられたコンテキスト(BC)内で汎用的な物を表現する
  |      |- Identifier  // OrderIdやUserIdなど
  |      |- Enum        // BC内で汎用的に使用される列挙型をおく
  |      |- Exception   // ドメイン内のエラーは基本的にこれを返す。個別のエラーが必要であれば継承して使用する。
  |
  |- Infra
  |  |- DB | API | S3 | PubSub...
  |  |          |- Repository   // ドメインモデルの生成とStoreの実装、APIやDBからの値取得時にはここがドメインモデルの生成を担う
  |  |          |- Command      // ドメインモデルを介さない一括登録の実装
  |  |          |- Query        // ドメインモデルを介さない抽出(例としてはCount等)の実装

まとめ

今回ご紹介したのは私の所属するチームで現在採用しているアーキテクチャとなります。 このアーキテクチャと資料自体は現在のチームメンバーがキャッチアップするのにつまずくポイントや実装で困った事を反映して育てている物です。

会社やチームの状態によっても取り得るアーキテクチャは異なると思いますがボトムアップでDDDを実現する足がかりとして何か参考になれば幸いです。

さいごに

ホワイトプラスでは、ビジョンバリューに共感していただけるエンジニアを募集しています!

ネットクリーニングの「リネット」など「生活領域×テクノロジー」で事業を展開している会社です。どんな会社か気になった方はオウンドメディア「ホワプラSTYLE」をぜひご覧ください。

オンラインでカジュアル面談もできますので、ぜひお気軽にお問い合わせください。

open.talentio.com open.talentio.com