パッケージ構成の考察
最新の考察
レイヤーで論理的な役割を整理したので、次はパッケージです。
パッケージ概要
フォルダ構成例
boundedcontext ├─application │ └─service │ └─hoge │ ├─domain │ ├─model │ │ └─hoge │ │ hogeFactory │ │ hogeRepository │ ├─query │ └─rule ├─infrastructure │ └─datasource │ └─hoge │ entity │ repositoryImpl └─presentation └─hogeregister
Presentation
- 画面(や コンポーネント)、スコープ(ConversationやFlow)の単位。
- RestURIの単位。
- 実装技術単位ではなく、インタフェース単位でフォルダを作成。
(特定のフォルダ名ではないので、上図でも背景を白にしています) - Controller(Action)とForm(表現の実体)は基本的に同一フォルダにまとめる。
ただし、複数コンポーネントで整理したい場合は、サブフォルダで分割。
同様に同一スコープの単位でフォルダを作成しても良い。
(登録→確認→完了を「XX登録」を親フォルダとした構成イメージ) - 命名:画面単位(Aggregate)はHogePage、部品はFugaForm
Application
Service
- ServiceおよびDomainにより振る舞いを表現する
- トランザクション境界
- コンテキストや主たる関心事でフォルダ分割して見通しを良くしても良い。
- 登録系は動詞単位、参照系はEntityまたはDTO単位
- Repositoryを使える唯一の層*1
Infrastructure
ドメインモデルを永続化する具体的な実装。 DataSource、Messagingだけということではなく、よくある永続化の参考例。 他にFileSystemなど、永続化対象に応じてフォルダを作成する。 一般的な永続化であるDatasourceによる整理を行う。
DataSource
- DomainのRepositoryの実装
Domain
Model
- ValueObjectとEntityによる関心事の実装
- AggregateRootModel単位でフォルダ作成
- Repositoryを使ってはいけない
Rule
- Modelで表現するのに適切でないルールの実装
- DDDのDomainServiceに類似
- コンテキストや関心事でフォルダ分割して見通しを良くしても良い
Repositoryを使って良い*2
Repository(DAO)よりもビジネスロジックを表現したい場合に使う- 複数のEntityを扱った判断/編集を行う際に使用する
- 無くて済むなら作らない方が良い
Repository
- パッケージはModelと同じにして集約ルートとの関連を明確にする
- 集約ルートを経由したデータ操作を行う(DBとは限らない)
- インターフェースによるInfrastructure層との依存解決
- 入出力の関心事の操作の主体*3
Query
- DTOを戻り値とする(Entity(集約)クラスを戻り値としない)Repository
- Modelではないため、別途フォルダーを設けてDTO単位で実装する
- 参照のみ
- インターフェースによるInfrastructure層との依存解決
Factory*4
- UtilなどをInjectしてEntityを生成
- 複雑なDomainやEntityの生成
考察
登録系のServiceは動詞
調べた限りでは「名詞(Entity名)Service」にしているケースが多いですが、登録系については「動詞」の方が良いように思います。
事前条件、事後条件の検証を 同一サービス内で実装することで 関心事の集約を図ろうとした場合、動詞の単位で実装した方が可読性も良いと思います。
DataSourceのentityフォルダ配下はフラット
2019/1/25 訂正
分類不要という観点で整理すると言うのはNG。
DBのテーブル自体がフラットなので、当該フォルダ配下も分類もしなくても良いかな、と思っています。
テーブルの構造についてはER図(設計)および、RepositoryにおけるDomain x Entity の変換において構造化されるので*5、フォルダによる構造化は必須としなくても良いだろうという判断です。
また、NetBeansによる自動生成したJPAのEntity資産を必要以上に編集しないだけ、というのが本音もあります。
DataSourceのEntityはRepositoryImplと同じフォルダ
2019/1/25 追記
@Entity
はpackage privateにして、RepositoryImplと同じパッケージに配置します。
こうすることで、@Entity
への操作を限定できます。
JPAの@Entity
は、mutableなJavaBeanなので気をつけて扱わないと想定していないデータ更新を引き起こす可能性もあり、コード上でも取り扱えるスコープを狭くしておいた方が良いという考えからです。
RepositoryはDAOではない
集約のルートととしたDAO(CRUD)的なところが基本になるけど、DAOではありません。
DAOは、あくまでORMが機能として提供するものであり、 Repositoryは あくまで使用者です。
Domain層ではinterfaceによる宣言しかできないので 戻り値(もしくは実行時例外)のEntity(もしくはDomainObject)によって表現するに留めます。
取得ロジックにドメインの関心事がある場合は、Ruleとして独立させて、Infrastructure層に ロジックを分散させないようにします。 *6
取得目的に関心事がある場合はメソッド名でそれを表明して入出力の窓口とすることで入出力を集約します。*7
Domain層に配置する意義にもつながるように思います。
Repositoryへのロジックの実装
基本的にRepositoryはDAOとの橋渡しが役割になりますが、ドメインロジックとまでは言えないけれど、いちいちルールとしてクラスを作るほどのことのないものもあります。
実際に、私がやってみて思った事例として
データ検索をしたけれど 対象データが無かった場合に実行時例外をスローしたい、というような要件があった場合、Serviceに判定を設けて振り分けてしまえば それでおしまいなのですが Serviceには判定を実装したくない(気が付けばロジックの置き場所になるトリガーになりかねないから)、Infrastructure層のRepositoryImplで実行時例外をスローさせても良いけれど 厳密には こういう判断もロジックなので 分散はさせたくない、でも このくらいのことのためにRuleクラスを作成するのも なんだか煩わしい、というケースがありました。
紆余曲折を経て*8、結論としては
interfaceのdefault実装を使ってRepositoryにロジックを実装することにしました。
(詳細は、後日 ブログにまとめる予定)
Repositoryインターフェースが「データを取得する行為に直結したロジック」の置き場所としての役割をもつことで、個人的には Serviceもすっきりし、Infrastructureにロジックも漏れ出ず 割と良い整理が出来たように思います。
DomainServiceじゃなくてRuleと表現する理由
Serviceという表現は紛らわしいと思いました。
それにDomainServiceの表現を調べると、ほとんど「ドメインオブジェクトの責務ではない、複数のドメインを扱うビジネスルール」に近しい表現がされており、それであれば「ルール」の方が紛らわしくない、という考えに至りました。
パッケージで境界付けされたコンテキストを表現
パッケージは名前衝突をさけるためにプロダクトのURLを逆にする、くらいしか考えていなかったのですが、パッケージは境界付けされたコンテキストにするという発想をDDD関連の記事で読んで、随分と思考がスッキリしたので、それを採用しました。
組織のドメイン.コンテキスト.レイヤー.詳細
モジュールの管理単位はコンテキストで集約。
共通部品とかインフラ層の抽象化したものとかは、フレームワークみたいな形で、別のコンテキストとしておくことで、自然と資産の分類がイイ感じになるのでは?と考えています。
なお、以前は
組織のドメイン.レイヤー.コンテキスト.詳細
という構成でした。
この構成だと、コンテキストがレイヤーの下であるため、発想が自然とモノリシックになってしまっていました。
ちょっとした違いかもしれませんが、実装によって発想が歪みにくくなる効果を期待しています。
FactoryでInjectって?(2018/11/8 追記)
Domainの実装はPOJOで実現できること、もしくはAnnotationを使った宣言による制御を通じて実現する(もしくは出来うる範囲に留めて 知識の表現を優先する)ことを想定していますが、共通Utilなどの便利な部品群を使いたい場合も 起こり得ると思います。
ザックリと 3つあると考えていて、CollectionsやArraysのように汎用的なライブラリに相当するもの、プロダクトとして共通的なDomain・機能としておきたいもの、汎用・共通な機能だけれど実装を置き換え可能なものにしておきたいもの、です。
1つ目は、横断的なものとして どのレイヤーからでも使用するUtilとして公開して、それを使います。何も考えずに実装するとcommon
パッケージが出来て、Utilが集まって、、となるパターンですが java.*
に相当するものは分離不可能なので皆無を前提とするのは難しいと思います。ただし 十分に吟味はしたいところです。
2つ目は、common
とは似て非なると思っているのですが、あくまでプロダクトにおける共通Domainです。例えば、myproduct.core
とか myproduct.base
とか そういう分類です。ニュアンスになってしまっていますが、他プロダクトで使うのではなく、同一プロダクトでの使用だが 複数パッケージで使いたいもの です。
3つ目は、実装を置き換えられるようにするということでDIを検討することになります。個々のDomainでDIして、、という やり方が思い付きますが、そうすると対象DomainがDI管理下になるようにしないといけません。ですが Domainにおける実装は基本的にDIを前提としていません。となると、どこかでUtilのインスタンスを渡してあげる必要があります。
Entityで、、とも考えられますが、Entityは Domainの集約であって生成が主たる仕事では無いと考えます。ということでFactoryでEntityを生成する際に、DIしておいたUtilのインスタンスを、そのUtilを必要とするDomainに提供するというやり方にしよう、と考えました。
そうすると、DIP*9によって実現するためInterfaceが必要になります。で、そのInterfaceですが、上述の1つ目に相当する場合は、どのプロダクトでも適用する仕様をまとめたパッケージで管理します(つまり commonに相当するレベルとして扱う)。2つ目に相当する場合は myproduct.core
とか myproduct.base
というパッケージで管理して 当該プロジェクトだけで参照するものとして管理します。
ただし、Factoryを使うことも含めて、基本的に そうならないような設計になることが望ましいと考えています。
安易に「これは2つのパッケージで扱うから共通にしよう」ではなく、
例えば、「3つ目のパッケージとして新たに切り出してServiceを経由して実装すべき」かもしれません。
例えば、宣言的な実装をすべき対象かもしれません。
StreamAPIやlombokのような仕組みを自作して、それを横断的に使うというケースであれば、上述の1つ目に相当すると思いますし、その場合は そのライブラリと一蓮托生レベルの蜜結合なシステムだと思います。覚悟のある判断なので、それを止めることはできません。
逆に、Javaの基本構文の範囲と Annotationによる補助で実現出来うる実装にDomainの実装は留めて 外部ライブラリによる影響を極力低くしておきたい、という発想でシステムを構築しようとした場合は、面倒でも ここで提示した Factoryに相当するものを経由して結合度を下げることになるのかな?と考えています。
結局 言いたいのは、Utilは気軽に作るんじゃなくて オレオレFWとして一蓮托生となるもの以外は 慎重に作ろう、ということかもしれません。
RuleでRepositoryは使わない
2021/2/14 追記
キッカケは以下(および一連のスレッド)
これはちょくちょく見かけるけど、ドメインオブジェクトが入出力から独立しなくなる方法。あ、ドメインサービスもドメインオブジェクトの一種。あと"副作用のない関数"とも整合性が取れない形にはなる。まぁ人類には扱いきれないので避けたい。リポジトリはドメインオブジェクトの外で扱うほうが無難 https://t.co/qJFl4MeMhv
— かとじゅん (@j5ik2o) February 14, 2021
「かとじゅんさんが言ったから」というよりも「あっ、そっかアプリケーションサービスでやれば良いんだ」と腹落ちしたから。
結果、DI(Scoped)からも解放されました。
アプリケーションサービスからアプリケーションサービスを呼べば良いだけなのに「トランザクション境界」という認識が強すぎたように思います。
これは例えば「Repositoryをトランザクション境界にする」と類似の整理誤りな気がしなくもないです。
個人的にはRepositoryから結果を取得して編集をするアプリケーションサービスを作ることは避けたいので、RepositoryにCRUDを超えたメソッドを作ろうと思います。「RepositoryはListのように使う」という考えもあるように思いますが、上述の通り「Repositoryは入出力を関心事とする主体」という整理にも合致しますし 元々の「Repositryへのロジックの実装」とも整合性はとれるように思います。
Scopeについて
Presentation
Service
Scopeは RequestScoped
。
本来は状態を保持することがないのでApplicationScoped
でも良さそうなところです。
ServiceクラスはApplicationScoped
にしている事例が多いため悩みました。
ですが、Infrastructure層のクラスのScopeを限定しない(後述)という整理をした場合、RequestScoped
にしておかないとDIできません。
(ServiceがApplicationsScoped
でDIしたいクラスがRequestScoped
だとDIできない)
ということで 調整役という目的を達成するために、RequestScoped
としました。
Domain
そもそもDomain層のクラス自身が主体的にScopeを持つことは無いと考えています。
もし、そのような設計となっている場合、DomainクラスそのものにScopeをつけるのではない方式で実装できないか再考した方が良いと思います。
(例えばDIではなく、Scopeを持つFormクラスにDomainを内包するような)。
例外は Rule。*10
Repositoryを使う場合、DIをしないといけないため Scopeが必要です。
Ruleは Serviceから使用するので RequestScoped
で良いと思いますが、Dependant
でも良いかもしれません。
Infrastructure
Infrastructure層のクラスは基本的にDIを通じて 各層に実装を提供することになるので全てのクラスにScopeの指定をすることが前提になります。
ただし、使用するライブラリの特性など色々な制約下に晒される層であるため 特定のScopeに限定することは出来ません。
例えば、DataStoreは 状態を持たない一例として、ApplicationScoped
にする、としても 全てのクラスがApplicationScoped
になるとは限らないということです。
参考
実践DDD本 第7章「ドメインサービス」~複数の物を扱うビジネスルール~ (1/4):CodeZine(コードジン)
混乱しがちなサービスという概念について - かとじゅんの技術日誌
履歴
- 2018/4/8 Scopeに関して追記
- 2018/4/10 UsecaseとServiceに関して手直し
- 2018/9/20 Usecaseを削除
ユースケースじゃなくてサービスとしよう - システム開発で思うところ - RepositoryとRuleについて追記
- 2018/9/25 Scopeの記述の見直しと、Actorに関する記述を削除
Repositoryを「ほぼDAO」とした記述を見直し - 2018/11/12 具体的なプロジェクトでの構成例を考察
パッケージ構成の考察(2) - システム開発で思うところ - 2021/2/14 RuleとRepositoryの記述を見直し
追加考察
ドメインオブジェクトは、BCのドメイン層とは別プロジェクトのレベルで扱うのが良いのかもしれないとか思ってきた。
— Yamashita (@_vermeer_) 2021年7月28日
ドメインオブジェクトは単独で価値を成すもので、
層はコンテナ実装に依存する資産配置みたいな。
集約(識別)は、状態管理に関わるのでコンテナ側、つまり層側#ddd
このあたりの発想も参考になりそう
Atomic Designをやめてディレクトリ構造を見直した話|食べログ フロントエンドエンジニアブログ|note