プロジェクトを新しく始める時、既存のイケてないアプリケーションのリファクタリングを始めたい時、アプリケーションのパッケージ構成について頭を悩まされることが増えた。
でも結局のところ、どういうパッケージ構成でアプリケーションを作ればいいのかわからない。何がベストなのかもわからない。
そんな自分への備忘録
目次
クリーンアーキテクチャと DDD の違い
-
- コンポーネントをレイヤー毎に分割することで各レイヤの関心事を分離する
- 各層の依存関係は内側へと向かう(内側に依存する)
- 抽象的な interface を外側に向けて提供することで、制御の流れと依存関係を逆転させる
-
- アプリケーションが扱うビジネスにおける「知識」や「ルール」、「要求」をドメインモデルとして表現する
- ビジネスロジックを流出させないことで、ビジネスの成長と共にドメインモデルも習熟していく
クリーンアーキテクチャはアプリケーションの設計をどのように行うべきかを説く設計思想。
一方、ドメイン駆動設計はアプリケーションにおけるドメインモデルを定義し、それをどのように成長させていくかに着目している開発手法。
だと思っている。
そもそもなぜアーキテクチャ設計に考えを巡らせる必要があるのか
具体化するために自問自答。
Q. アーキテクチャ設計の必要性とは?
A. 保守しやすいアプリケーションを作るため
Q. 保守しやすいアプリケーションとは?
A. コードのメンテナンス性が高い
Q. メンテナンス性が高いコードとは?
A. 各コードの責務が分離されており、変更を加えても影響範囲が小さいこと
Q. より具体的には?
A. ユニットテストが書きやすい
なんとなく腑に落ちたので、ユニットテストが書きやすいアーキテクチャを目標にパッケージ構成を考えてみる。
構成案
各層配下のディレクトリはドメイン単位で作成する。
root ├── application │ └── usecase(ユースケースの実態) │ └── mapper(プレゼンテーション層とアプリケーション層間のマッパー) ├── domain │ ├── adapter(各層との緩衝材) │ │ ├── in(ユースケースの抽象化) │ │ │ └── input(ユースケースの入力用DTO) │ │ └── out(インフラ層の操作の抽象化) │ └── model(ドメインモデルの格納) │ ├── entity(値オブジェクトなどの集合体) │ └── value(値オブジェクトや区分オブジェクト) ├── infrastructure │ ├── datasource(データベース) │ │ ├── adapter(インフラ層の操作の実態) │ │ ├── entity(ORM Entity) │ │ ├── mapper(DTOへの変換) │ │ └── repository(データベース操作の実態) │ ├── externalapi(外部API) │ │ ├── adapter │ │ ├── config(WebClientのコンフィグ) │ │ ├── mapper │ │ ├── request │ │ └── response │ └── transfer(外部ストレージ) └── presentation ├── controller │ ├── request │ └── response └── handler(コントローラー共通処理)
参考
- ドメイン駆動 + オニオンアーキテクチャ概略[DDD]
- DDD を意識した際の package 構成
- Ports & Adapters architecture on example
- Clean Architecture で分からなかったところを整理する
各層の役割
おなじみの図を元に考える。
インフラ層
ORM で管理する 各データオブジェクトを entity に格納して repository で DB 操作を実装する。(ここでいう entity は DDD における entity ではない)
例えば Spring Data JPA なら以下のような形。
@Entity public class UserEntity { @Id @Column String id; @Column String name; } public interface UserRepository extends JpaRepository<UserEntity, String> {}
repository から得られるデータはインフラ層のみが知るべき I/F であるため、mapper を介してドメイン層の I/F へ変換を行う adapter を実装することで インフラ層をドメイン層に依存させる。(依存性の逆転)
public class UserAdapterImpl implements UserAdapter { @Override public UserId save(User user) { UserEntity savedUser = userRepository.save(mapper.toEntity(user)); return UserId.from(savedUser.getId()); } }
datasource 以外のパッケージについて
externalapi、transfer でも基本的な考えは同じ。
外部との I/F を繋ぐ adapter をドメイン層に依存する形で定義し、entity となるクラスを用意して mapper で DTO へ変換する。
例えば外部 API なら API の response が entity に当たり、外部ストレージなら Stream、IO などという具合。
アプリケーション層
アプリケーションにおけるドメインオブジェクトのユースケースの定義。(ユーザーの登録、参照、更新、削除)
プレゼンテーション層からの入力を受け付けるための I/F として input クラスをドメイン層に用意する。
ユースケース単位にクラスを作成していき、handle や execute など共通メソッドを実装してプレゼンテーション層から利用する。
public class AddUserImpl implements AddUser { public UserId handle (AddUserInput input) { User user = User.from(input); return userAdapter.save(user); } }
ドメイン層
隣接するアプリケーション層とインフラ層それぞれをドメイン層に依存させるためのインターフェースを定義する。
具体的には、ヘキサゴナルアーキテクチャのポートアンドアダプターのようにアプリケーション層からの input とインフラ層への output を adapter という名称でパッケージを切る。
ドメインオブジェクトについて
value
Enum などの区分オブジェクト や UserId などの値オブジェクトを表現する。
(DDD における ValueObject)
entity
値オブジェクトや区分オブジェクトの集合体としてドメインオブジェクトを構成する。
(DDD における Entity)
レイヤー間の I/F について
プレゼンテーション層からの入力値をそのままドメインオブジェクトに変換することは難しいが、インフラ層の ORM Entity をドメインオブジェクトに変換するのは比較的容易いと感じた。
例えば、ユーザのドメインオブジェクトには名前とメールアドレスのフィールドがあるので DB にも 2 カラム定義している。
しかし、ユースケースとしてはメールアドレスだけ登録すればユーザ登録が可能。名前は後から設定でも OK、というようなパターン。
そのため、ユースケース用の専用 DTO を input として用意したが、インフラ層の DTO は ORM Entity から DDD Entity へ直接変換する構成を取った。
また、構成を合わせるという意味では、ORM Entity の格納場所はインフラ層ではなくドメイン層の adapter/out 配下に定義するのが綺麗かとも考えたが、ORM の概念をドメイン層に持ち出したくなかったのでインフラ層側に entity を配置している。
この点については運用しながら最適な構成を模索するような形で様子見をしたい。
所感
これが正解であることはきっとないが、いったん今の自分が考えられるベストな構成であると思いたい。
この構成を元にサンプル API アプリケーションを作って本当にテストコードが書きやすいのかを検証する。
Top comments (0)