little hands' lab

ドメイン駆動設計を布教したい

境界づけられたコンテキスト 実装編 - ドメイン駆動設計用語解説 [DDD]

little-hands.hatenablog.com

こちらの記事で説明できなかった、「境界づけられたコンテキストをどうやって実装に落とし込むのか?という話を書きます。

境界づけられたコンテキスト実装の基本イメージ

結論からいくと、基本的には、

1コンテキスト = 1アプリケーション

と思ってもらってOKです。
これを基本として、用途や実装コストと相談しながら少しずつ設計を組み替える検討が可能です。

1アプリケーション単位で、オニオンアーキテクチャ概略の記事で紹介したアーキテクチャを1セット揃えると思ってください。

つまり、こちらの記事で紹介した2つの境界づけられたコンテキストに対して、

f:id:little_hands:20171205190613p:plain:w400

以下のようにアプリケーションを2セット作ります。

f:id:little_hands:20171206232814p:plain:w400

ドメイン層を外界と隔離して、外部に公開するする操作を周りの層で定義するのです。

最終的に、マイクロサービス2つ作ると思ってもらって良いです。そうすると、DBの扱いや通信についてもおのずとイメージしていけると思います。

以下、細かい話を説明していきます

DBはどう扱うの?

基本的に1アプリケーション1DBと考えてください。これは設計上非常に重要です。

先述の通りマイクロサービスだと考えると、コンテキストのモジュール同士の依存性を極力無くし、リリースのサイクルなどを独立して扱えるようにしたくなります。

その際に、DBを共有しているとモジュール同士の結合度が当然高まります。設計上もうっかり異なるコンテキストをまたいで外部キー制約などを貼ってしまうなど、物理的に制約がかけられないとどんどん結合度を高めてしまうリスクが上がります。

また、概念的にも、コンテキストによって「商品」というのは異なる問題で存在しうるのに対して、DBを共有すると「Productというテーブルはどちらのモデルか?」などと矛盾が発生してしまいます。

よって、コンテキストを分けた時にDBを分けるというのは必ず検討してください。

なお、物理的にインスタンスを分けなくても、スキーマを分けるというのは一つの手です。インフラ構成、予算などの制約を考慮して検討しましょう。

コンテキスト同士はどうやって通信するの?

同期通信か、非同期通信かの2択です。

  • 同期通信:ネットワーク経由のダイレクトコール(REST APIなど)
  • 非同期通信:メッセージキューを利用したイベント通信

f:id:little_hands:20171207001225p:plain:w400

非同期通信の基盤は、今ではマネージドサービスであるSQSを使用することが多いのではないでしょうか。

この2択でどちらを選ぶかについては、「リクエスト結果を同期的に取得したい」「非同期にして先方のサービスが落ちていても大丈夫なようにしたい」といった、要件による検討の要素が大きいです。 これはDDD的な検討というよりは、マイクロサービスとしての検討範疇になると思います。

この辺りの話はまた広がるので一旦置いておきます。マイクロサービスとしては以下の本が非常に詳しいです。

マイクロサービスアーキテクチャ

ただし、その要件を実装するのにDDD的にどのレイヤでどう実装するのか、という話は検討が必要になります。この辺りの話は「実践ドメイン駆動」の第13章「境界づけられたコンテキストの結合」に詳しく書いてあります。(ここはまた長くなるので、別の記事でかければ書きます。)

実践ドメイン駆動設計

この本は詳細すぎて最初はどこから読んだらいいのか面食らってしまうところがありますが、概要が頭に入っていればあとはコードのサンプル込みでかなり丁寧に書いてあるので、DDDをやるのであれば1冊はあった方が良いと思います。
(Eric Evansの方はなくても良いです。笑)

(2017/12/7現在、kindle版が半額(¥5,616→¥2,808)の様です。いつまでだかわからないので値段で迷ってた方は買い時かも)

アプリケーションは必ず分けないといけないの?

基本は別アプリケーションがよいというのは前述の通りです。ただし、費用対効果を考えて簡略化することは可能です。

簡略化するなら、例えば以下のような構成です。

f:id:little_hands:20171206234116p:plain:w200

明らかにコンテキスト的には別だという設計でも、コンテキスト内のオブジェクト数が少なく今後の拡大もあまり見えない場合、ある程度パッケージ構成は意識しつつも1アプリケーション内に配置しても良いです。

別コンテキスト同一アプリケーションにした場合のDB

この場合、DBを同じインスタンスを使って良いですが、先述の理由で必ず別スキーマにしましょう。同一スキーマだと設計が悪くなりがちなのに加え、システムが大きくなってから別アプリケーションに分割することが難しくなります。

別コンテキスト同一アプリケーションにしたメソッド呼び出し

メソッドの呼び出しに関しては、これも同期通信と非同期通信に分けて考えます。

同期通信はマイクロサービスのようにネットワーク経由の通信は不要になり、いくつか選択肢が考えられます。

  • 別コンテキストのように、アダプターとインターフェイスを内部向けに作成して呼び出す
  • エンティティ同士の直接呼び出しを許可する

こちらもどこまできっちりやりたいか、ということを検討しながら判断しましょう。ただし、この場合は物理的に呼び出し方法を強制しにくいので、レビューなどで担保していくことが必要になります。

また、非同期の場合はイベントを利用してコミュニケーションするのは別アプリケーションの場合とあまり変わりません。

1コンテキスト、複数アプリケーションというケースはある?

これは私見になるのですが、実際に以下のような設計で開発をしました。

  • ドメイン層・アプリケーション層までを"applicationCoreモジュール"とする
  • applicationCoreモジュールを内包するWebAPIアプリケーション
  • applicationCoreモジュールを内包するWorkerアプリケーション
    • アプリケーションが発行するイベントをsubscribeして非同期処理する
    • スケジューラーが定期的に発行するイベントを処理する(時限実行)

という構成にし、APIアプリケーションとWorkerアプリケーションをそれぞれでサービスとして起動しています。

これは、「オニオンアーキテクチャのPresentation層がAPIとWorkerという2種類のものがあり、それがたまたま別プロセスで動いている」という解釈をしています。

設計意図としては、イベントをキューに大量に詰めて非同期的に処理したい時、Workerのみスケールアウトすることを可能にしたいとの狙いです。
applicationCoreを共有することにより、あくまで処理の口が違うだけで、業務的にな制約は同じものを共有することができます。

実例として、スケジュールされて定期実行していた処理を画面からもオンラインで行い、となったことがあり、その場合もAPIの口を追加して同じ処理を使いまわした、ということがありました。テストで品質も担保されているので、非常に気楽に口だけ追加することができました。

まとめ

以上でコンテキストの実装の概念、実装の概説を終わります。
実装が見えてから概念を振り変えると、結構具体的なイメージを持って設計に挑めるようになるはずです。

実は、このあとコンテキストをどう分けるか、という判断が難しいのですが、その参考になる方針などはまた別の機会に書きたいと思います。