集約とトランザクション境界に関するメモ
この記事はドメイン駆動設計 #1 Advent Calendar 2018の22日目です。
昨日は@crossroad0201さんによる「DDDの構成要素とマイクロサービスの単位をどう合わせるべきか」でした。
今日はエリック・エヴァンスのDDD本に書かれたパターンの一つである集約について、自分なりのまとめを書いてみたいと思います。実は以前まで集約については「言いたいことはわかるが実践で使う意義がいまいち見いだせない」というスタンスだったのですが、最近になってようやく腑に落ちました。
バートランド・メイヤーの契約による設計
DDD本のパターンの多くは、オブジェクト指向プログラミングで築かれてきた理論や原則に基づいたものです。OOPの理論で特に有益なものの一つに、契約による設計というものがります。これは鈍器としても名高い『オブジェクト指向入門』*1の著者であるバートランド・メイヤー博士が提唱したもので、簡単に言えば以下のような設計スタイルのことです。
- クラスは、そのインスタンスがライフサイクルの間、常に守らなければならないルール(クラス不変表明)を持つ
- クラスの利用者は、あるオブジェクトのメソッドを呼び出す時、そのメソッドが正しく働くための前提条件(事前条件)を守る義務がある
- メソッドは事前条件を守って呼び出された限りにおいて、予め定められた契約(事後条件)を守る義務がある
- オブジェクト生成命令(コンストラクタやファクトリメソッド)及び、コマンド(副作用のあるメソッド)は、クラス不変表明を守る義務がある
このスタイルを守ることで、プログラムが想定通りに動かなかった時にどのコードが契約違反を犯したのかが明白になります。また、プログラム言語によってはアサーションという仕組みを使うことで、契約違反の発生を即座に検出することが可能です。これを使うと問題のあるコードが実行されたまさにその場所でアサーションエラーが発生するので、バグの原因調査が著しく楽になります。
この辺りの要点は、@t_wadaさんがスライドにまとめられています。
【改訂版】PHP7で堅牢なコードを書く - 例外処理、表明プログラミング、契約による設計 / PHP Conference 2016 Revised - Speaker Deck
カプセル化によってドメインの知識を表す
DDDの文脈で言えば、上記のクラス不変表明は、あるエンティティや値オブジェクトの「正しい状態」を宣言することになります。何が正しくて何が正しくないとするかは、扱うドメインとそれをどうモデリングするかによって異なるので、これも一つのドメイン知識と言えるはずです。
例えば、以下のような「金額」値オブジェクトがあったとします。
ここで「通貨にアメリカドルが入っていた場合、価格は0以上であり、小数点第二位まで持てる」「通貨に日本円が入っていた場合、価格は0以上であり、小数点以下は持てない」など、そのアプリケーションで「金額」という概念をどのようにモデリングしたのかをコードに落とし込む必要があります。ここがはっきりしていないと、どのような状態のオブジェクトが壊れていない状態なのか、チーム内で共通認識を作れません。
契約による設計の以下のポイントを抑えておけば、オブジェクトのライフサイクル中、これらのルールを確実に守るクラスを記述することができます。
- オブジェクト生成命令及びコマンドは、クラス不変表明を守る義務がある
オブジェクトが適切にカプセル化されていれば、まさにこのクラス不変表明を守るコードそのものが、ドメインモデルの性質を体現することになるのです。
複数のオブジェクトにまたがる不変表明をどう守るか?
一つのオブジェクトに関する不変表明を守る方法はわかりました。では複数のオブジェクトに言及する不変表明はどのようにコード化すれば良いでしょうか?
ここではECサイトにおける「注文」エンティティと「品目」エンティティについて考えてみます。
ここで以下の不変表明を設けたいと思います。
- 一つの注文は必ず一つ以上の品目を持っている
- 一つの注文内の品目の商品IDはユニークである
この不変表明を守るのはどのオブジェクトの責務になるのでしょうか?
注文エンティティは自分が所有する品目エンティティへの参照を持っているため、このルールを守ることができそうです。ですが、他のオブジェクトが品目エンティティのコマンドを直接呼び出した場合、注文エンティティはそれによって発生し得るルール違反を止めることができません。
そこで以下のような設計上の規約を設けてみるとどうでしょうか?
- オブジェクトグループ内の中心的なオブジェクトをルートオブジェクトとする
- ルートオブジェクトはオブジェクトグループ全体の不変表明を守る義務を持つ
- ルートオブジェクト以外のオブジェクトのコマンドは、オブジェクトグループの外から直接呼び出してはならないものとする
この規約を守っている限り、オブジェクトグループ全体がカプセル化され、オブジェクトグループが持つ不変表明を守ることができるはずです。
DDDの集約とは、こうした複数のオブジェクトにまたがる、いわば集約不変表明を記述するためのものと捉えることができそうです。
集約不変表明を守るための他のパターン
ファクトリとリポジトリの役割についても、この観点から捉え直すことができそうです。以下のように実装することで、集約のライフサイクルの期間中、常に集約不変表明が担保されるはずです。
- ファクトリは集約不変表明を守った状態で集約を作り出す
- リポジトリは必ず集約をまるごと永続化する(部分的に永続化すると不変表明が壊れるため)
契約による設計のルールと似ていると思いませんか?
オブジェクトの正しい状態を担保することは、OOPにおいて重要なテーマの一つです。DDDにおいては、集約の正しい状態を担保することが重要なテーマの一つになっているというわけです。
トランザクション境界の選択指標
さて、一旦DDDの話は置いておいて、今度はデータベースの話をします。
オブジェクトをデータベース(ここではRDBを想定)のレコードとして保存するなら、複数のレコードの状態が不整合にならなように、トランザクションをかけて更新をアトミックにすることが不可欠です。その際、一つのトランザクションで更新する範囲について、相反する二つの要求を踏まえて落とし所を決めることになります。
- 関連する複数のレコードが不整合にならないために十分広いこと
- 余分なレコードがロックされないように出来るだけ狭いこと
つまり、トランザクションの範囲は必要十分な広さである必要があるということです。ではその必要十分な広さとはどの範囲のことでしょう?
「関連する複数のオブジェクトがどうなっていれば正しい状態か?」というルールを見つけるのは、ドメインを分析してモデル化する際に重要なポイントです。このルールをドメインモデルに対応するクラス郡(ドメインレイヤー)の中に定義するなら、定義先の第一候補は集約の不変表明になると思います。関連するオブジェクトの整合性が壊れないことを集約が漏れなく保証してくれるなら、集約単位の更新さえアトミックになっていれば不整合は起きないはずです。つまり、
集約の境界と、必要十分なトランザクションの範囲は必然的に一致する
ということになるのではないでしょうか?
ヴァーン・ヴァーノンの実践DDDには以下のように書かれています。
適切に設計された集約では、業務で必要とするあらゆる変更に対して、トランザクション内での不変条件の整合性を完全に維持できる。また、境界づけられたコンテキストを適切に設計すれば、どんな場合でも、ひとつのトランザクション内で変更する集約をひとつだけに絞りこめる。(実践ドメイン駆動設計 p.340)
これを「ルール」として扱うのは厳しすぎるようにも見えますが、ここまで順序立てて考えてみると、理に適っているように思えてきます。
どこまでも広がる不変表明
と、ここまでの理屈で問題を解決できるアプリケーションなら何ら悩む必要もないのですが、現実は甘くありません。往々にして複数の集約にまたがる整合性をとりたい場面が出てきます。例えばC2Cのショップ作成サイトに以下のような「ショップ」エンティティがあるとします。
ここでは注文・品目の場合と異なり、ショップ自体が注文数と総売上の情報を持つこととします。そこで、以下のような不変表明が必要となります。
- ショップ.注文数 = ショップに対する全ての注文の数
- ショップ.総売上 = ショップに対する全ての注文.総計の合計
この不変表明を守る責務は誰が担えば良いのでしょうか?
集約のルートをショップにして、注文もその集約に入れるべきでしょうか? もしそんなことをすれば、客が買い物をする際にショップの集約をロックしなければならなくなります。これでは人気のショップに来た客が一人一人レジに並ばされるように、並行性を大きく損なうことになりかねません。
かといってショップの集約と注文の集約に分けてしまった場合、注文の集約が生成・更新された際にショップの集約の情報が更新されることを保証できません。
ですがよく考えてみると、ショップの集約と注文の集約は本当にリアルタイムで整合性が取れている必要があるのでしょうか? もしショップの総売上や注文数をオーナーが見る際、必ずしも「今まさにこの瞬間の数値」を見なくてもいいのであれば、まだ手立てはあります。
「あとでも良い」整合性
ここで出てくるのが結果整合性という考え方と、ドメインイベントという道具です。
結果整合性とはさきほど言ったような「一定時間経過後に整合性がとれていれば良い」という性質の整合性のことです。通常の整合性は整合か不整合かの二つの状態しかありえませんが、結果整合性であれば「今はまだ不整合だがまもなく整合性がとれる」という状態があり得ます。
ドメインイベントは、ドメインの分析の際に出てくる「◯◯が〜したとき」のような概念をオブジェクト化したものです。ここでは例えば「客が注文した」というドメインイベントを作り、注文の集約を永続化する際にエンキューしておきます。この「集約の永続化とイベントのエンキュー」はアトミックである必要があります。
発行されたドメインイベントはオブザーバーパターンで処理できます。発行者はこのイベントを誰がどのように処理するのかを知る必要はありません。なんらかのサブスクライバ(購読者)が「客が注文した」イベントを受け取り、注文のあったショップの総売上と注文数を更新すれば良いのです。
業務としては結果整合性で構わないが、UI上の都合で複数の集約を更新したいという要望もあるかもしれません。その場合、まずUIが適切かどうかを検討するのも意義があると思います。OOUX等のオブジェクト構造を意識したデザインであれば、自然と集約単位の更新に落ちつていく可能性もありそうです。DDDを効果的に実践するには、ドメインエキスパートと共にデザイナーを巻き込むことも必要になってくると思います。
理想の世界の外の話
いかがだったでしょうか? 今回説明した道具は以下の3つです。
これらを駆使すれば、アプリケーション開発で直面する複雑な整合性に関する問題に対して、一貫したアプローチで挑むことができるようになるはずです。
……というのはあくまで理想の世界の話。
現実にはこれらの方法が上手く当てはまらないケースもまま出てきます。ですが「理想論だから切り捨てる」のではなく、まずは理想の形を認識し、そこへ向かって何ができるかを試行錯誤するのが設計者の妙技というものではないでしょうか。
この辺りの議論については、アドベントカレンダー3日目の@k_bigwheelさんが詳しく書かれています。
ここまで読んでいただきありがとうございました。
明日は@smdmtsさんです。こうご期待。