レイヤードアーキテクチャで新規アプリのパッケージ構成を考えた話
技術部のやすだです。
先日までかかわっていたPJで既存アプリの機能をSpring Bootベースの新規アプリに移植する作業を担当していました。
新規アプリなのでパッケージ構成の検討で色々悩みつつ反省もしつつ、ひとまずは落としどころが見つかったのでその辺りの話をまとめようかと思います。
要件・仕様
弊社の決済事業[Paid]での決済情報登録に関する一部機能を提供できるWebAPIを作成します。
機能の概要は以下の通りです。
- 利用可否確認:特定のユーザーがPaidを利用できるか否かを確認します
- 決済登録確認:決済情報が登録できるか否かを確認します
- 決済登録:決済情報を登録します
インターフェースとしては、いずれの機能もWebUIは提供せず、jsonでのやり取りを行います。
エンドポイントについては、各機能毎に個別のエンドポイントを設定します。
パッケージ構成
少人数ではありますが複数人で開発を行う前提となっています。
各クラスの責務や依存関係についてある程度の共通認識を取る必要があったため、オーソドックスなレイヤードアーキテクチャをベースにしています。
パッケージ構成は以下のようにしました。
()内はアーキテクチャ上で対応しているレイヤーです。
実際にはadviceやconfigなどのパッケージもありますが、説明用に割愛しています。
├─app(Presentation/Application)
│ ├─order
│ │ ├─controller(Presentation/Application)
│ │ ├─factory(Presentation/Application)
│ │ └─form(Presentation)
│ │ ├─request
│ │ └─response
│ ├─member
│ │ ├─controller
│ │ ├─factory
│ │ └─form
│ │ ├─request
│ │ └─response
│ └─その他関心単位のパッケージ
└─domain(Domain/Infrastructure)
├─model(Domain)
│ ├─dto
│ └─entity
├─query(Domain/Infrastructure)
├─service(Domain)
└─repository(Domain/Infrastructure)
各パッケージの説明
app
Presentation層とApplication層をまとめています。
仕様上インターフェースはjsonのみということもあり、なるべく構成をシンプルにすることを重視しています。
ただし、機能毎に個別のエンドポイントを利用することもあり、アプリケーションの関心単位でのパッケージ分割を行っています。
app/controller
controllerをまとめたパッケージです。
Presentation層とApplication層をまとめているので、下記の責務を持ちます。
- リクエストパラメータ->ドメインオブジェクトの変換メソッド呼び出し(Presentation)
- ドメインロジックの呼び出し(Application)
- ドメインオブジェクト->レスポンスパラメータの変換メソッド呼び出し(Presentation)
app/factory
リクエストパラメータ->ドメインオブジェクトとドメインオブジェクト->レスポンスパラメータの変換を責務とするクラスをまとめたパッケージです。
app/form
リクエストパラメータとレスポンスパラメータのDTOをまとめたパッケージです。
domain
Presentation層とApplication層同様に、こちらもシンプルな構成にしたいという考えからDomain層とInfrastructure層をまとめています。
今回はアプリの規模が小規模で開発人員が少人数なこともありドメインロジックが散らばってしまうようなケースは防げるかと判断しました。
domain/model
ドメインオブジェクトをまとめたパッケージです。
Entityのみで表現しようとするとどうにも違和感が出がちなので、そんな場合はムリせずDTOを使う方針にしています。
パッケージ名はDomainObjectにしちゃってよかったような気もしますね。
domain/query
Repositoryで対応するには検索条件が複雑になってしまう時に助かるQueryServiceのパッケージです。
今の構成だとQueryServiceから返すDTOもdomain/model以下にまとめていますが、DTOは特定のコントローラ(Application)以下のパッケージにある方が自然な気もちょっとします。
domain/repository
Entityの永続化を責務とするRepositoryをまとめたパッケージです。
domain/service
ドメインロジックをまとめたパッケージです。
反省点と落としどころ
Presentation層とApplication層をまとめたことは、当初の想定通り構成のシンプルさというメリットにつながりました。
対して、Domain層とInfrastructure層をdomainパッケージにまとめたことは大きなデメリットにつながることになりました。
開発中の仕様変更に伴って、複数テーブルを操作する処理を1つのrepositoryクラスで記述するケースやカラムのデータ更新の要否をrepository内で判定してしまうケースなどが出てくるようになります。
開発人員間での情報共有等は都度行っていましたが、それでもドメインロジックに閉じるべき責務が徐々にrepositoryに漏れていくというあるあるを目の当たりにして「なるほど。。こうなるのか。。。」という経験をすることになります。
アプリケーションの規模や開発人員の状況によらず、Domain層とInfrastructure層はパッケージレベルで分割してドメインロジックが散りにくいような構成にしておいた方が良かったなと思いました。
今後の対応として、ドメインロジックが漏れにくい構成にする=ドメインロジックがserviceパッケージにまとまりやすい構成へ修正することを考えています。
具体的には、既存のqueryとrepositoryをインターフェースとしてdomainパッケージ直下に残しつつ、実装のみInfrastructure層に移すことでドメインロジックが漏れにくくできるかと考えています。
対応を実施すると下記のようなパッケージ構成になるかと思います。
├─app(Presentation/Application)
│ ├─order
│ │ ├─controller
│ │ ├─factory
│ │ └─form
│ │ ├─request
│ │ └─response
│ └─その他関心単位のパッケージ
├─domain(Domain)
│ ├─model
│ │ ├─dto
│ │ └─entity
│ ├─query
│ ├─service
│ │ └─serviceImpl
│ └─repository
└─infrastructure(Infrastructure)
├─queryImpl
└─repositoryImpl
おわりに
短くまとめると、シンプルに保守しやすく作っていたつもりが、実はアンチパターンになっていたという話でした。
今回はDBに関連する実装をinfrastructureパッケージにまとめる対策を行う予定ですが、
今後より洗練された構成にできたときや新しい気づきがあった時には、改めてこちらで発信できればと思います。
それでは、よい開発を!