ドメイン駆動開発について
ドメイン駆動開発はその名の通り、単なる実装パターンではなく開発要員以外のメンバーをも巻き込んだ開発手法である。
なぜ 3 層に分割するのか
ドメイン駆動開発の設計では以下の 3 層に分けてソフトウェアを設計せよとされている。
- アプリケーション層
- ドメイン層
- インフラ層
なぜこのような分割をするのか。突然モバイルアプリ版の開発が始まるかもしれないし、突然 DB が RDB ではなくなるかもしれないからである。
そのような場合に同じドメインのソフトウェア間で再利用できるものがドメイン知識であり、これを含んだものがドメイン層にあたる。
アプリケーション層
- メイン関数
- アプリケーションサービス (セッション管理など)
- コントローラー
- ビュー, メールのテンプレートなどのプレゼンテーション
- ビュー用の値オブジェクト, DPO
ドメイン層
- ドメインサービス
- エンティティ
- 値オブジェクト
- 集約
- レポジトリのインターフェイス
インフラ層
- レポジトリ
- DTO
レポジトリの実装クラスがインフラ層なのは分かる。だがなぜインターフェイスがドメイン層なのだろうか。
それはインターフェイスをインフラ層としてしまうと、それを呼び出すドメイン層の実装が必然的にインフラ層に依存してしまうからである。つまり利用するインフラライブラリを選択できなくなってしまう。
Spring DI の強み
ドメイン層をインフラ層に依存させないためレポジトリのインターフェイスをドメイン層に入れると説明した。
では実装知識を含んだレポジトリのインスタンスはどこで初期化するのか。単純に考えて、アプリケーション層で初期化してドメインサービスまで伝搬するしかなさそうである。
しかし Spring DI を利用していて、かつドメインサービスのクラスとインターフェイスを継承した実装クラスをコンポーネントとして宣言しておけば、この問題を Spring DI が解決してくれる。Factory パターンや ApplicationContextAware
などを用意する必要はない。
@Repository
public class Repository extends IRepository {
...
}
Dependency の解決にはフィールド @Autowired
ではなくコンストラクターの @Autowired
を使おう。単体テストの記述が楽になる。
Lombok の @RequiredArgsConstructor
でもっと簡単に記述することができる。
@Service
@RequiredArgsConstructor
public class Service {
private final IRepository repository;
}
IRepository mockRespository = new MockRepository();
var service = new Service(mockRespotory);
モジュール分割
ソフトウェアを 3 層に分けて設計と書いたが、 Maven モジュールも 3 層に分割すべきか。
ソフトウェアが単一である場合は、パッケージで分割しさえすれば十分である。
逆にモジュール分割をする場合は親モジュールを用意して
<modules>
<module>./web</module>
<module>./mobile</module>
<module>./domain</module>
<module>./infrastructure</module>
</modules>
として
<parent>
<groupId>me.yong_ju.application</groupId>
<artifactId>parent</artifactId>
<version>...</version>
<relativePath>../</relativePath>
</parent>
...
<dependencies>
<dependency>
<groupId>me.yong_ju.application</groupId>
<artifactId>domain</artifactId>
<version>...</version>
</dependency>
<dependency>
<groupId>me.yong_ju.application</groupId>
<artifactId>infrastructure</artifactId>
<version>...</version>
</dependency>
</dependencies>
としてしまえばよい。親モジュールで先に子のモジュールを定義しておくことによって、そのモジュールを依存関係に加えたときに Maven のレポジトリではなくローカルから取得するようになる。(ただしバージョンが一致している必要がある)
Spring の Configuration 定義
パッケージを 3 層分割すると早速ある問題にぶち当たる。
それは @SpringBootApplication
をアプリケーションパッケージに配置したがゆえに、 Spring のコンポーネントスキャンがアプリケーション層のコンポーネントしか Injection してくれないという事態だ。
どうするか。 scanBasePackages
でスキャン対象のパッケージを上のレイヤーに変更することはできるが、おすすめはしない。
ここは、 Component Scan を有効にした Configuration クラスをドメイン層、インフラ層に用意し、アプリケーションから利用する Configuration を明示的に @Import
することにした。
@Configuration
@ComponentScan
public class DomainConfiguration {
...
}
@Configuration
@ComponentScan
public class InfrastructureConfiguration {
...
}
@SpringBootApplication
@Import({ DomainConfiguration.class, InfrastructureConfiguration.class })
public class Application {
...
}
アンチパターン
サービスがリソース単位
よく見るサービスがただのレポジトリのラッパーになってしまっているパターン。
意味のある操作単位でサービス化するべきだし、それがまだ判明していないのなら、なおさらサービス化してしまうのは早い。
(これに関連して全く意味のない操作単位で @Transactional
してしまってるパターンもよく見る)
実装パターンにこだわりすぎる
実装パターンは一般的に先の計画見通しの悪いソフトウェア開発において、致命的な設計ミスを避けつつ思考停止するためのツールだと個人的には思っている。
よって何でもかんでもベストプラクティスを求めさまよい時間を浪費するのは本末転倒である。
また Java やその周辺フレームワークは絶え間なく進化する。それらを利用してより冗長性の少ないかつ安全な実装ができることもある。
暇があれば自分が理解に時間を要したところを重点的に書いていく。