TL;DR
- クリーンアーキテクチャやオニオンアーキテクチャを参考にしたオリジナル
- フレームワークの力を活用するためにドメイン層以外ではLaravelの機能を使用
- 標準実装パターン・ガイドラインを整備しボトムアップでDDDを推進
始めに
こんにちは、ホワイトプラスでテックリードをしている仲見川です。 今回はホワイトプラスが運営する宅配クリーニングサービス「リネット」の開発で用いているアーキテクチャについてご紹介しようと思います。今回のアーキテクチャはLaravel上に構築するアプリケーションの範囲となります。
背景
リネットではWebで注文を受け付けると集荷の連携や工場作業の予定登録などが行われます。これ以外にも工場で行われた作業に基づいてお客様の参照するステータスが随時変化します。 また、集荷や配達の際など配送キャリアにてお品物が管理されることもあります。
このように、注文やクリーニングするお品物に対して複数のコンテキストが同時並行で存在している形です。
開発のアプローチとしてDDDを採用し、コード上にドメイン知識を集積する場所としてドメインモデルを用いた開発を行うようにしています。
アーキテクチャの変遷
1. Laravel導入:ActiveRecordパターン
ControllerでEloquentを使用する、ドメイン知識はEloquentのモデルに集約されている実装です。 複合的な物はUserLogicやOrderLogicに共有化されていました。
2. DDD導入:レイヤードアーキテクチャ
コンテキスト毎にドメイン知識を扱う事を指向し始めDDDを採用しました。 実装としてはドメインロジックとフレームワークやDBアクセスなどを分割する事で実装が整理され、一例として手続き的にDBが変更されるのを予防し不具合が起きづらいなどの効果がありました。
3. CAのエッセンスを取込:クリーンアーキテクチャ風
レイヤードアーキテクチャから実装時に迷う点をクリーンアーキテクチャの考え方を取り入れてUseCaseやドメインモデルの公開範囲など使われ方について整理を行いました。 また、標準実装パターンを定義することで、DDDの理解は後回しでもひとまずは手を動かせるようできました。
現在のアーキテクチャ
クリーンアーキテクチャではControllerもPresenterとしてアーキテクチャに組み込まれていますが、私たちの場合はControllerは意図的にこのアーキテクチャの外に置いています。 Controllerはフレームワークの都合上Infra層の知識を扱う事もあり、Controllerを厳格にアーキテクチャに組み込むと実装負荷が高いと考えているためです。
UseCaseとDomainモデルを切り出す事で、業務として行うべき事を整理し「誰の」「何を」「どのように」解決するのかを明確にしています。 これによって例えば同じ注文への変更であったとしても、お客様が届け日を変更するユースケースと、工場側で届け日を変更するユースケースは異なります。これは、実際に行われるチェックの内容が違ったり、通知すべき相手が変わる、つまりはUseCaseが変わるためです。
実際には先の例はコンテキストが異なるため、お客様に関わる部分と工場に関わる部分でそれぞれお互いに処理を委譲する関係性となります。
アーキテクチャ実装ガイド
アーキテクチャに沿った実装を行う為にアーキテクチャの概要、実装ガイドラインという形で複数の資料を用意しています。
アーキテクチャの概要
以下はアーキテクチャ概要からディレクトリ構成と、各層の参照方向を定義した図の抜粋です。
参照方向を定義し、Interfaceの設置場所も定義することでボトムアップでひとまずアーキテクチャに沿った実装を可能にしています。 以下はドキュメントの一部で各ディレクトリにどのような処理を置くのかの簡単な解説しています。
- Controller // ユースケースを呼び出しドメインモデルを受け取って画面表示などを行う - 境界付けられたコンテキスト(BC)のディレクトリ(OrderやUser,Cleaning等) |- App | |- UseCase // ユースケース単位の実装をします。業務的に価値のある単位で処理をまとめます。 | | // 例えばお知らせ一覧を取得し、既読にして返すなど。 | | // お知らせ一覧を取得するだけが目的で副作用が無ければ取得して返すだけもユースケースです。 | | // 場合によってはRepositoryを呼び出すだけのProxyメソッドでしかない場合もあります。(リネットのアーキテクチャではままある) | | // UIとのPOI(point of interface)で画面から渡されたプリミティブな値はここでドメインモデルに変換する | | // 逆にレスポンスを返すときはドメインモデルをプリミティブな値へ変換する。 | | | |- Acl // 腐敗防止層、別のBCの処理を呼び出しを配置する | |- Ohs // 別BCへ公開するクラスを配置する | |- Pl // Ohsのリクエスト、レスポンスのクラスを配置する | | | |- Command // 集約を介さない登録や処理のリクエスト(のInterface) | |- Querie // 集約を介さない取得(のInterface) | |- Domain │ |- Exception // ドメイン内のエラーは基本的にこれを返す。個別のエラーが必要であれば継承して使用する。 | |- Model // ドメインモデル。副作用を持たない振る舞いを持つ。 | | // 基本的には業務ロジックはここに集まるように作る。(UIを除く) | | // ここがただのデータの入れ物でしか無いときは一歩ひいてここに書くべきロジックが別の所に書かれていないか確認する | | // もちろん絶対ロジックがあるわけではありませんが、特に○○Listなどのドメインモデルをグルーピングしている | | // ドメンモデルが何も持っていなかったらおそらく書くべき処理が漏れ出ています | | | | // ドメインモデルの初期化時にはドメインモデルとして持つ物はドメインモデルを引数として渡すようにしましょう | | // プリミティブ型の場合、誤ったものでも渡すことが出来てしまうためです(型付言語のパワーを活かしましょう!) | | // 例) Bad new Hoge(string $title, string $time) | | // Good Hoge(Title $title, DateTimeImmutable $time) | | // 呼び出し Hoge(Title("たいとる"), new DateTimeImmutable(stringTime)) | | // こうすることで、正しく無いモデルの生成を防ぐ事が出来ます。 | |- Repository // ドメインモデルの生成とStore(のInterface) | | | |- Type // 境界付けられたコンテキスト(BC)内で汎用的な物を表現する | // OrderIdやUserId、DateTimeなど | |- Infra | |- Db | Api | S3 | PubSub... | | |- Repository // ドメインモデルの生成とStoreの実装、APIやDBからの値取得時にはここがドメインモデルの生成を担う | | |- Command // 集約を介さない登録や処理のリクエストの実装 | | |- Query // 集約を介さない取得の実装
ボトムアップで進める為の実装ガイドライン
DDDやアーキテクチャでの実装は正解がなく、チームやプロダクトに合わせた実装となります。ですのでそのチームでどのようなアプローチを取っているかを理解しないとチームの意図する実装と異なる場合が出てしまいます。
そこで、手を動かしながらキャッチアップしたり、実装・レビューの迷いを低減させる事を目的に具体的な実装方法をガイドラインとして整備を進めています。
緑色のリンクになっている物が現在ドキュメントのある部分。
迷いやすかったり、実際に議論になった部分などを整理してドキュメント化を行う事で、共通認識を作り次回はスムーズに出来るようにする事と今後入社した方がリネットでの開発に早く馴染めるようにすることが目的です。
さいごに
今後はコンテキスト境界を跨ぐ場合の実装パターンなど不足している実装ガイドラインの整備を進めながら、既存のアーキテクチャに則っていない箇所のリアーキテクチャを進めていきたいと考えています。
まだまだやることがたくさんありますのでご興味があればオンラインでカジュアル面談などもしていますのでお気軽にご連絡ください。
ホワイトプラスでは、ビジョンやバリューに共感していただけるエンジニアを募集しています!
ネットクリーニングの「リネット」など「生活領域×テクノロジー」で事業を展開している会社です。
どんな会社か気になった方はオウンドメディア「ホワプラSTYLE」をぜひご覧ください。