マイクロサービスへの上手な分割手法#2 戦術的 DDD
データとロジックはセットで分割
「マイクロサービスへの上手な分割手法」の中で「データとロジックはセットで分割する」という話をしました。その理由を記載すると戦術的 DDD の話が多岐にわたるため、別途詳細に説明することにしました。なぜ問題になるかは大抵の場合具体的なケースを考えるだけで明らかになります。
「データ」と「ロジック」はセットであるべきというのは「ロジック」を実装しているマイクロサービスの中に「データモデル」を永続化するデータソースが含まれるべきという意味です。よく聞く1サービス1DB とほぼ同義です。
なぜ「データ」と「ロジック」は分割すべきではないのか
前の記事で紹介した「基本機能」をロジックのみを載せる「基本機能」サービス、CRUD API + RDBMS で「データ」、と言う形で分離して管理することにします。
さて、ここで CRUD API についてみてみましょう。CRUD API はここではビジネスロジックを持たない単純な CRUD 操作を実施できることを意味しています。が、CRUD API は果たして本当にビジネスロジックなしで機能するのでしょうか。
例)
- ISBN は現在13桁ですが、API 利用者が 10桁で登録してきたら?
- 発売日が未来になっていたら?(未発売として扱う?)
- ページ数がマイナスになっていたら?
- 価格がマイナスで登録されたら?
これらは Design By Contract の考え方で取り扱われる概念ですが、ビジネスロジックの一部だと考えます。特に未来の日付で登録できるかどうかはシステムとしてどう実装したいか(未発売の本を登録できる仕様であれば登録可能にすべきだし、そうでなければ弾くべき)に影響を受ける条件です。これがビジネスロジックではなかったらなんなのでしょうか。
こうした条件は、CRUD API 側で判断しないとおかしな状態、例えばページ数 -10 頁のような状態で登録可能にすることを意味します。
ではこれらの制約は「RDBMS が持つ制約の機能で実装すべきでしょうか。RDBMS 側の制約で実装してしまうと、アプリケーションが特定の RDBMS に完全に依存した作りになる上に、ロジックがアプリケーション、データベースと複数に分散してしまい、改修の際に影響範囲が大きくなるため避けた方が良いでしょう(最悪の場合改修漏れが発生するでしょう)。
誰が CRUD API をメンテナンスするのか
前の記事のように、ここに「マーケ」が「ポイント」機能を追加しようとした場合はどうなるでしょうか。「商品モデル」に「割引期間」や「割引率」のプロパティが追加されます。
さらに「在庫」が「在庫」に関する「プロパティ」を追加していくと、「商品モデル」には「基本機能」、「マーケ」、「在庫」の混在した知識がどんどん溜まっていくことになります。
CRUD API 担当者は複数のビジネスエキスパートの観点を持ち、「データモデル」にプロパティを追加する/しないを判断でき、積極的な改修をする必要がありますが、一体誰が担当するのでしょうか。「基本機能」チームでしょうか?「マーケ」チームでしょうか、「在庫」チームでしょうか。いずれにしても、さまざまなチームが実装に絡みそうなので 2pizza rule の視点からも良くなさそうです。自分のチームが実装していないコードも合わせて確認する必要が出てきそうです。
ヘキサゴナルアーキテクチャ/オニオンアーキテクチャ/クリーンアーキテクチャ
このような複数の層にビジネスロジックが溢れ出してしまう課題に対しての解決策として、ヘキサゴナルアーキテクチャというものがあります。似たようなアーキテクチャにオニオンアーキテクチャ、クリーンアーキテクチャがあります。
ヘキサゴナルアーキテクチャは、上記のようにビジネスロジックを新しい層を作って隔離しても、それ以外の層(特にユーザーインターフェース)にそのうちビジネスロジックが漏洩してしまうという問題点を解決するためのアーキテクチャです。
層を増やして左右の呼び出しをいくら変えてもビジネスロジックの他の層への漏洩は何ら解決しない、問題の本質はアプリケーションの内と外にあるという考え方をしています。
ヘキサゴナルアーキテクチャは別名 Ports & Adapters ともいいます
ヘキサゴナルアーキテクチャはビジネスロジックを「ドメインモデル」層に閉じ込めます。「マイクロサービスへの上手な分割手法」では「カタログ」、「マーケ」、「在庫」のコンテキストに分割した例をご紹介しましたが、このヘキサゴナルアーキテクチャはコンテキストごとに作成されます(モジュラーモノリスの場合は異なる)。
「カタログ」には「カタログ」のビジネスロジックを閉じ込めることになります。出版日が未来かどうかチェックして弾くのかどうかもこの「ドメインモデル」内で実装されます。同様に「マーケ」のコンテキストには「マーケ」のビジネスロジックを閉じ込めます。
そして「カタログ」のビジネスユースケースを「アプリケーションサービス層」に実装します。上記の例だと「取り扱い商品を検索」、「取り扱い商品を追加」というのがユースケースになります。
ユーザーインターフェース、API、RDBMS であろうと何であろうと、外部から接続されてくるアプリケーションはビジネスロジックが記述してある「ドメインモデル層」を直接扱うことは許されません。「アプリケーションサービス層」の呼び出しの結果取得できたオブジェクト(集約、アグリゲート)を操作して状態の変更や参照を行います。
RDBMS への接続箇所はインフラに依存しないよう「依存関係逆転の法則」が使われていますが、ここでは詳細に説明しません。
ドメインモデルとは何か
「ドメインモデル」は「エンティティ」と「値オブジェクト」で構成されるオブジェクトのことです。「エンティティ」と「値オブジェクト」を組み合わせることで「データモデル」を実際のクラスに落としこみます。
ドメインモデル貧血症
以下のようなコードを見たことはありませんか?私が10年前にコーディングしていたコードは須く以下のようなコードになっていました。
このコードはプロパティに対する setter/getter しか存在せずとてもビジネスロジックを実装しているとは言い難いコードです。このような setter/getter しかないコードを 実践ドメイン駆動設計本では「ドメインモデル貧血症」と言っています。このようなコードにしないための戦術的 DDD、「エンティティ」「値オブジェクト」が存在します。
エンティティ(Entity)と値オブジェクト(Value Object)
エンティティと値オブジェクトの説明は割愛します。こちらの記事を読んでいただくととても良くわかるのではないかと思います。
ざっくり言うと、属性(プロパティ)が変わっても同一だと認識すべきもの = エンティティ、それ以外は値オブジェクトになります。
後述する「Book」クラスは「エンティティ」、後述する「Auhor」クラスは「値オブジェクト」になります。
値オブジェクトの例
上記の「著者」を値オブジェクトとして実装した例は以下になります。
setter/getter はありません。代わりに Author のフルネームを String 型で変換する fullName() メソッドが存在しています。
エンティティの例
「Book エンティティ」の例です。
ISBN が13桁かどうかは ISBN クラスに記載されているため、「Book エンティティ」の実装には出現しません。
※もしかしたら、引数に与えられている List<Authors> は DDD 的には問題があり、Authors という値オブジェクトを実装するべきかもしれません。この辺は私の DDD 歴がまだ浅く実装に迷っているところになります。
アプリケーションサービスの例
アプリケーションサービスはインターフェースで宣言してあります。実際はこれを実装した REST API を経由して外からの呼び出しに対応することになります。
RDBMS に保存したり、参照したりするコードは実際は上記の形で EntityManager を利用して実装されます。しかし、RDBMS への保存や参照は「ドメインモデル」層には興味がないため、Adapter 層での実装となります。
集約(アグリゲート)
上記のようなケースの場合、「Book エンティティ」は「集約」と呼ばれます。「集約」はトランザクションの単位です。
本来は「集約」を通して Book 内部の「値オブジェクト」や「エンティティ」の変更操作をおこなうことになりますが、今回の例の「本」の特性上、一度決まった後に作者が変更されたり、出版日が変更されるといったユースケースはそれほど考えられないため 「Book エンティティ」にはそう言ったユースケースが実装されていないことに注意が必要です。
詳しくはこちらの記事を参照ください。
https://codezine.jp/article/detail/10776
最後に
「ロジック」だけ分割しても、「データモデル」のそばには必然的に「ロジック」が入り込みます。これは仕方のないことです。なぜならば、「データモデル」は RDBMS などのデータソースに永続化する必要があるためです。永続化にあたって、システム的な意味でのチェック、ビジネス的な意味でのチェックがどうしても必要になります(おかしな状態のオブジェクトの生成を防ぐ)。
今回のケースではいくら「基本機能」サービスに「ロジック」を載せると決めたとしても「商品モデル」に関するロジックは「CRUD API」にも実装されてしまいます。
前の記事で「CRUD は REST API にとってよくないのか」という記事を紹介しました。
- 「サービス」というものについての全体構想を避けています。ビジネスロジックが一切ないのですから。
- 外部に見せているのは内部的なデータベース構造ないしデータであって、考え抜かれた契約ではありません。」
というのはまさにこう言うところにあります。
「データモデル」の周辺にはどうしてもロジックが必要になるため、これら二つは無理に分離させる必要がないのではないでしょうか。もともと「データ」と「振る舞い(ロジック)」をクラスとして実装することがオブジェクト指向の考え方だったはずです。
「データ」と「ロジック」をバラバラにしてしまうと、同じ「カタログ」に関する変更なのに、いろんな場所(ロジック用サービス、CRUD API、データベース等)に手を入れる必要がでてきます。バラバラにした場合、「データモデル」はクラスの役割を放棄して C 言語の構造体の役割しか果たしていません。結果、以下のような事態を引き起こしスパゲッティになっていくでしょう。
- 自分のために作った「データモデル」なのに役割が混在して「データモデル」を利用している人がわからない
- 「データモデル」の変更の影響範囲が大きくなる(上記が理由)
オブジェクト指向の考え方で実装されていれば、「データモデル」を参照しているロジックはすぐそばにあります。
DDD はオブジェクト指向の考え方を発展させたもの
DDD はオブジェクト指向の考え方を発展させて、さまざまな課題を解決できる設計手法です。
「ロジック」はヘキサゴナルアーキテクチャにおいて「ドメインモデル」として「データモデル」と共に実装し外に漏れ出さないようにすることを考えましょう。
マイクロサービスへの上手な分割は「データ」と「ロジック」をセットでおこなう方が後々のスパゲッティを発生させない方法と言うのが理解していただけたのではないかと思います。
実際、「マイクロサービスパターン」という書籍では、早い段階でヘキサゴナルアーキテクチャが紹介され、以降のマイクロサービスの構成はすべてヘキサゴナルアーキテクチャで記載されています。
DDD を学ぶことはかなり遠まわりに見えますが、スリムなアプリケーションを作る近道だと自分は考えています。