システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る

パッケージ構成の考察

レイヤーで論理的な役割を整理したので、次はパッケージです。

パッケージ概要

フォルダ構成例

  boundedcontext
    ├─application
    │  └─service
    │      └─hoge
    │  
    ├─domain
    │  ├─model
    │  │  └─hoge
    │  │    hogeFactory
    │  │    hogeRepository
    │  ├─query
    │  └─rule
    ├─infrastructure
    │  └─datasource
    │      ├─entity
    │      └─repository
    │          └─hoge
    └─presentation
        └─hogeregister

Presentation

  • 画面(や コンポーネント)、スコープ(ConversationやFlow)の単位。
  • RestURIの単位。
  • 実装技術単位ではなく、インタフェース単位でフォルダを作成。
    (特定のフォルダ名ではないので、上図でも背景を白にしています)
  • Controller(Action)とForm(表現の実体)は基本的に同一フォルダにまとめる。
    ただし、複数コンポーネントで整理したい場合は、サブフォルダで分割。
    同様に同一スコープの単位でフォルダを作成しても良い。
    (登録→確認→完了を「XX登録」を親フォルダとした構成イメージ)
  • 命名:画面単位(Aggregate)はHogePage、部品はFugaForm

Application

Service

  • ServiceおよびDomainにより振る舞いを表現する
  • トランザクション境界
  • コンテキストや主たる関心事でフォルダ分割して見通しを良くしても良い。
  • 登録系は動詞単位、参照系はEntityまたはDTO単位

Infrastructure

ドメインモデルを永続化する具体的な実装。 DataSource、Messagingだけということではなく、よくある永続化の参考例。 他にFileSystemなど、永続化対象に応じてフォルダを作成する。 一般的な永続化であるDatasourceによる整理を行う。

DataSource

  • DomainのRepositoryの実装

Domain

Model

  • ValueObjectとEntityによる関心事の実装
  • AggregateRootModel単位でフォルダ作成
  • Repositoryを使ってはいけない

Rule

  • Modelで表現するのに適切でないルールの実装
  • DDDのDomainServiceに類似
  • コンテキストや関心事でフォルダ分割して見通しを良くしても良い
  • Repositoryを使って良い
    Repository(DAO)よりもビジネスロジックを表現したい場合に使う
  • 複数のEntityを扱った判断/編集を行う際に使用する
  • 無くて済むなら作らない方が良い

Repository

  • パッケージはModelと同じにして集約ルートとの関連を明確にする
  • 集約ルートを経由したデータ操作を行う(DBとは限らない)
  • インターフェースによるInfrastructure層との依存解決

Query

  • DTOを戻り値とする(Entity(集約)クラスを戻り値としない)Repository
  • Modelではないため、別途フォルダーを設けてDTO単位で実装する
  • 参照のみ
  • インターフェースによるInfrastructure層との依存解決

Factory*1

  • UtilなどをInjectしてEntityを生成
  • 複雑なDomainやEntityの生成

考察

登録系のServiceは動詞

調べた限りでは「名詞(Entity名)Service」にしているケースが多いですが、登録系については「動詞」の方が良いように思います。
事前条件、事後条件の検証を 同一サービス内で実装することで 関心事の集約を図ろうとした場合、動詞の単位で実装した方が可読性も良いと思います。

DataSourceのentityフォルダ配下はフラット

DBのテーブル自体がフラットなので、当該フォルダ配下も分類もしなくても良いかな、と思っています。
テーブルの構造についてはER図(設計)および、RepositoryにおけるDomain x Entity の変換において構造化されるので*2、フォルダによる構造化は必須としなくても良いだろうという判断です。
また、NetBeansによる自動生成したJPAのEntity資産を必要以上に編集しないだけ、というのが本音もあります。

RepositoryはDAOではない

集約のルートととしたDAO(CRUD)的なところが基本になるけど、DAOではありません。
DAOは、あくまでORMが機能として提供するものであり、 Repositoryは あくまで使用者です。
Domain層ではinterfaceによる宣言しかできないので 戻り値(もしくは実行時例外)のEntity(もしくはDomainObject)によって表現するに留めます。
取得ロジックにドメインの関心事がある場合は、Ruleとして独立させて、Infrastructure層に ロジックを分散させないようにします。

Repositoryへのロジックの実装

基本的にRepositoryはDAOとの橋渡しが役割になりますが、ドメインロジックとまでは言えないけれど、いちいちルールとしてクラスを作るほどのことのないものもあります。

実際に、私がやってみて思った事例として

データ検索をしたけれど 対象データが無かった場合に実行時例外をスローしたい、というような要件があった場合、Serviceに判定を設けて振り分けてしまえば それでおしまいなのですが Serviceには判定を実装したくない(気が付けばロジックの置き場所になるトリガーになりかねないから)、Infrastructure層のRepositoryImplで実行時例外をスローさせても良いけれど 厳密には こういう判断もロジックなので 分散はさせたくない、でも このくらいのことのためにRuleクラスを作成するのも なんだか煩わしい、というケースがありました。

紆余曲折を経て*3、結論としては
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*4によって実現するためInterfaceが必要になります。で、そのInterfaceですが、上述の1つ目に相当する場合は、どのプロダクトでも適用する仕様をまとめたパッケージで管理します(つまり commonに相当するレベルとして扱う)。2つ目に相当する場合は myproduct.core とか myproduct.baseというパッケージで管理して 当該プロジェクトだけで参照するものとして管理します。

ただし、Factoryを使うことも含めて、基本的に そうならないような設計になることが望ましいと考えています。
安易に「これは2つのパッケージで扱うから共通にしよう」ではなく、
例えば、「3つ目のパッケージとして新たに切り出してServiceを経由して実装すべき」かもしれません。
例えば、宣言的な実装をすべき対象かもしれません。

StreamAPIやlombokのような仕組みを自作して、それを横断的に使うというケースであれば、上述の1つ目に相当すると思いますし、その場合は そのライブラリと一蓮托生レベルの蜜結合なシステムだと思います。覚悟のある判断なので、それを止めることはできません。
逆に、Javaの基本構文の範囲と Annotationによる補助で実現出来うる実装にDomainの実装は留めて 外部ライブラリによる影響を極力低くしておきたい、という発想でシステムを構築しようとした場合は、面倒でも ここで提示した Factoryに相当するものを経由して結合度を下げることになるのかな?と考えています。

結局 言いたいのは、Utilは気軽に作るんじゃなくて オレオレFWとして一蓮托生となるもの以外は 慎重に作ろう、ということかもしれません。

Scopeについて

Presentation

vermeer.hatenablog.jp

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。
Repositoryを使う場合、DIをしないといけないため Scopeが必要です。
Ruleは Serviceから使用するので RequestScopedで良いと思いますが、Dependantでも良いかもしれません。

Infrastructure

Infrastructure層のクラスは基本的にDIを通じて 各層に実装を提供することになるので全てのクラスにScopeの指定をすることが前提になります。
ただし、使用するライブラリの特性など色々な制約下に晒される層であるため 特定のScopeに限定することは出来ません。
例えば、DataStoreは 状態を持たない一例として、ApplicationScopedにする、としても 全てのクラスがApplicationScopedになるとは限らないということです。

参考

実践DDD本 第7章「ドメインサービス」~複数の物を扱うビジネスルール~ (1/4):CodeZine(コードジン)

混乱しがちなサービスという概念について - かとじゅんの技術日誌

履歴


*1:2018/11/8 追記

*2:少なくとも集約ルートを通じた操作において

*3:実装してみては眺めて、戻しては眺めて、考えてみては直してみて眺めてのループ

*4:依存性逆転の原則