第 3 章: 関数型アーキテクチャ

“3. A Functional Architecture” from “Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#” 📚

次の課題: これまで得てきたドメインに対する理解を、どのように関数型プログラミングに基づいたソフトウェアアーキテクチャに翻訳するか

  • ⚠️ 注意点: 現時点ではまだ具体的なアーキテクチャについて考えすぎないようにすること
  • 時間を使うならば: 引き続きイベントストーミングや聞き取り調査などによってドメインと要件をより深く理解する

本章で学ぶ内容:

  • 関数指向ドメインモデルのための典型的なソフトウェアアーキテクチャ
  • DDD の概念(境界づけられたコンテキスト、ドメインイベントなど)がどのようにソフトウェアに翻訳されるのか
  • ラフな実装方針

本書が使うアーキテクチャ概念:

  • システムンテキスト: 業務システム全体
    • コンテナ1: システムコンテキストを構成するデプロイ可能単位
      • コンポーネント: 各コンテナを構成するコードの大きい構成単位
        • クラス(あるいはモジュール): コンポーネントの構成要素

自律的なソフトウェアコンポーネントとしての境界づけられたコンテキスト

DDD の概念である “境界づけられたコンテキスト” とソフトウェアアーキテクチャとの関連を示す。

コンテキストとは: うまく定義された境界に基づく自律的なサブシステム

  • このように定義を定めたとしても、アーキテクチャとしての構成方法はいくつものパターンをとりうる
    • コンテナとドメインのマッピング例:
      • コンテナ = 業務: システム=コンテナでもある。このとき、境界づけられたコンテキストはインターフェイスによって定義された個別のモジュール(.NET でいう “アセンブリ”)
      • コンテナ = 境界づけられたコンテキスト: 伝統的なサービス指向アーキテクチャ
      • コンテナ = ワークフロー: こうなるとマイクロサービスアーキテクチャ

ただし、ドメインについて理解しきれていないフェーズでは、まだ特定のアーキテクチャパターンを選ぶ必要はない。 最重要なのは境界づけられたコンテキストを疎結合に設計し、自律可能に保つこと

境界を正しく定めることの重要性は述べたが、実際にはプロジェクト初期から正しく定めるのは難しい

  • 理由: 知識を得るにつれて境界は変わりうるから
  • よって、開発初期にはまずはモノリスパターンで始めることをおすすめする
    • マイクロサービスパターンを使う明確なメリットが見えているならばこの限りではない
    • 本当に疎結合なマイクロサービスアーキテクチャを実現するのは難しいからでもある
      • あるマイクロサービスを止めたときに別の場所が壊れるならば、それはただの “分散モノリス”

境界づけられたコンテキスト間におけるコミュニケーション

どうコミュニケーションする?→ ドメインイベントを通じて

  • 注文処理を終えた受注部門が配送部門に対して配送を依頼する場合の例:
    • 受注コンテキストは注文が確定されたイベントを発行する
    • 注文が確定されたイベントはキューに入るか、直接発行される
    • 配送コンテキストは注文が確定されたイベントをリッスンする
    • 配送コンテキストがイベントを受け取ると注文を配送せよコマンドが作られる
    • 注文を配送せよコマンドは注文配送ワークフローをトリガーする
    • 注文配送ワークフローが完了すると注文が配送されたイベントが発行される

このように設計すれば、受注・配送の両コンテキストは互いについて知る必要なく、イベントを通じてコミュニケーション可能。

  • イベントを実際に発行する方法
    • 選定したアーキテクチャによる
      • キューや関数呼び出しなどが考えうるが、今はまだ決めなくていい
    • 各コンテキストが自律できるよう、設計を疎結合に保つことが重要
  • イベントをコマンドに変換するハンドラ
    • これも複数の実装パターンが考えうる
      • 下流コンテキストの一部としたり、独立したルータやプロセスマネージャにしてもいい
      • 選定するアーキテクチャや、イベントとコマンドの対応づけをどこでしたいかによって決めればいい

📝 イベントを利用したコミュニケーションの特徴は互いを意識せずに済むところ。 確かにイベント経由通信を使わなくとも、ある程度の疎結合性は「インターフェイス契約+非同期コミュニケーション」で獲得できる。 それでもやはり、通信時には相手にデータを投げるなり、相手のメソッドを呼び出すなり、コード中に必ず「相手」が出てきてしまう。

境界づけられたコンテキスト間におけるデータのやりとり

コミュニケーションに使われるイベントは、下流コンテキストが処理に必要としている全てのデータを含む。

  • 例:
    • 注文が確定されたイベントは注文データ全体を含む
      • 注文を配送せよコマンドを作るのに必要な情報全てを配送コンテキストに渡すため

データがイベントに含めるのには大きすぎるなら、共有データストレージを利用してもよい。

このデータオブジェクト──Data Transfer Objects(DTO)と呼ぶ──は、境界づけられたコンテキスト内で定義されたオブジェクトと同じに見えるが、厳密には違う。

  • シリアライズしてコンテキスト間で共有できるよう設計されている
    • 上流コンテキストの境界付近でドメインオブジェクトは DTO に変換される
      • JSON や XML などにシリアライズされる
  • 下流コンテキストでは、このプロセスが逆方向に実行されてドメインオブジェクトが作られる

(例) 注文が確定されたイベントの DTO の構造:

  • イベント DTO
    • 注文 DTO
      • 注文明細 DTO のリスト

信頼性境界と検証

境界づけられたコンテキストの輪郭は信頼性境界としてはたらく

  • 境界の内側: 全て信頼可能で、有効であるとみなされる
  • 境界の外側: いかなるものも未検証で、不正になり得る

このような境界が機能するように、ワークフローの入口と出口にゲートを設置する必要がある。

入力ゲート

渡された DTO がドメインの制約(例: ある属性が 非null かつ 50 文字以内であること)を満たしていることを保証するために、入力を必ず検証する。 検証失敗時には、残りのワークフローはスキップされて例外が送出される。

出力ゲート

非公開情報が境界の外に出ないようにする役割を担う。

目的:

  • コンテキスト間の意図しないカップリング(なるほど!)
  • セキュリティ向上

これらの目的を果たすため、出力ゲートはドメインオブジェクトを DTO に変換する際に、情報を意図的に捨てる。

境界づけられたコンテキスト間の契約

境界づけられたコンテキスト間のカップリングをできるだけ減らしたい

  • しかし共有コミュニケーションは常に何らかの結合をもたらす
    • イベントと、関連する DTO は境界づけられたコンテキスト間に一種の契約をもたらす
    • コミュニケーションを可能にするにはコミュニケーションに使う共通形式について両コンテキストが同意している必要がある

誰が契約の締結を決定する?→コンテキスト間の関係性による

関係性の例:

  • 共有カーネル: 二つのコンテキストが共通のドメイン設計をいくつかもつ
    • よって関係するチームは協働する必要がある
      • 例: 受注コンテキストは配送先住所を受け取り、検証する。配送コンテキストは配送にその住所を使う
    • イベントや DTO の定義を変えるときには関係者のレビューが必要
  • 顧客/供給者(または顧客駆動契約): 下流のコンテキストが上流コンテキストに供給してほしいものを定義する
    • 上流が契約を守っている限り、両ドメインは独立に進化できる
      • 例: 請求コンテキストが契約を定義し、受注コンテキストがそれを供給する
  • 順応者: 顧客駆動とは逆に、上流の定義を下流が受け入れ、自らのドメインモデルをそれに合うように適応させる
    • 例: 受注コンテキストは製品カタログが決めた契約に従っている

腐敗防止層

外部システムと通信したいとき、利用可能なインターフェイスが自コンテキストのドメインモデルと全く合わないことがある

  • 腐敗防止層(入力ゲートがこの役割を果たすことが多い)を使って、やりとりやデータをコンテキストに合うものに変換する必要がある
    • 理由: ドメインモデルが外部のシステムに合わせようとして「壊れて」しまうのを防ぐため

コンテキストマップと関係性

設計が進み、コンテキスト間の関係が決まったとする:

  • 受注・配送コンテキスト間: 共有カーネル
    • なのでコミュニケーション契約を一緒に決める
  • 受注・請求コンテキスト間: 顧客駆動契約
    • 請求コンテキストが契約を決め、受注コンテキストがそれを提供する
  • 受注・製品カタログ間: 適合者パターン
    • 受注コンテキストは製品カタログが定めたモデルを使う
  • 外部の住所確認サービス: 本システムのドメインモデルと全然合わない
    • 腐敗防止層を介して利用する

コンテキストマップコンテキストをもつチームがどのように連携すべきかも示す

  • 技術的な課題であると同時に組織的な課題でもある

境界づけられたコンテキスト内におけるワークフロー

ビジネスワークフローの表現:

  • DDD の概念に基づく表現: コマンドにより起動され、いくつかのドメインイベントを発するミニプロセス
  • 関数型アーキテクチャにおける表現: コマンドオブジェクトを入力とし、イベントオブジェクトのリストを出力とする一つの関数

本文中の模式図を使った説明:

  • ワークフローは常にただ一つの境界づけられたコンテキストに含まれる
  • 公開ワークフロー(コンテキスト外からトリガーされる)は境界の外側に少しはみ出している

ワークフローの入力と出力

  • 入力: 常にコマンドとひもづくデータ
  • 出力: 他のコンテキストと通信するためのイベントのリスト

受注ワークフローにおける例:

  • 入力:
    • 注文を確定せよコマンド
    • 紐づくデータ
  • 出力: 注文が確定されたイベントなどのイベントのリスト

請求コンテキストにおける例: (顧客/供給者パターンであることに注意)

  • ただの注文が確定されたイベントを出すだけでなく、イベントに含めるデータはは請求に必要なデータのみに限る必要がある
    • 例: 請求先住所と合計金額だけ。配送先住所と商品リストは入れない

つまり、我々は新しいイベント(言うなれば請求可能な注文が確定したイベント)を発行する必要があることを意味する。

また注文確認が送信されたイベントも送りたい なので前に作ったダイアグラムはもう古いので新しくしたい

ワークフローの模式図の中で重要なのは、関数はイベントを発行してはおらず、単に返しているだけであるところ

  • 理由: どのように発行するかは別の関心事なため

境界づけられたコンテキスト内のドメインイベントを避ける

オブジェクト指向設計でよく見られるパターン: 境界づけられたコンテキスト内で、ドメインイベントを受けたリスナーが別のドメインイベントを発行 ⚠️

関数型アプローチではやらない

  • 理由: 隠れた依存を生み出してしまうから
    • リスナーが必要な場合にはワークフローの終端に置く
      • 効果:
        • ミュータブルなグローバルイベントマネージャがいないことが明示される ✅
        • 理解・メンテナンスしやすい ✅

境界づけられたコンテキスト内のコード構造

伝統的な「レイヤーアプローチ」では、コードは下記のような層に分けられる:

  • コアドメインまたはビジネスロジック
  • データベース
  • サービス
  • API またはユーザーインターフェイス

ワークフローは表面のインターフェイスから始まって最深部のデータベースに到達し、また表面に戻る

この手法の問題点 ⚠️:

  • 「同時に変更されるコードは同じ場所に属するべき」という設計原則を破っている
  • 層が「水平に」組み合わさっているので、ワークフローの動きを変えようとする場合には全ての層を変更する必要がある

よりよい方法 ✅: 「垂直な」構成に移行

  • それぞれのワークフローは各自の仕事を終えるために必要なコードを全て含む
  • ワークフローに対する要求が変わったら特定の垂直スライスだけを変えればいい

これは理想論ではなく実際に可能

オニオンアーキテクチャ

オニオンアーキテクチャ:

  • ドメインを真ん中に置く
  • その周りにその他の側面を配置する
    • 配置ルール: それぞれの層は外側にでなく、内側にだけ依存する

この構造に対して関数型の依存注入を使う方法は第 9 章で述べる

I/O を外側に保つ

関数型プログラミングの主な狙い: 予測可能で、内部を見ることなく簡単に説明できる関数を使って仕事をすること

  • そのために、関数を下記のように設計する:
    • 可能な限りイミュータブルなデータを使うことで依存を明示する
    • ランダム性・関数外の変数の更新などの副作用や、いかなる I/O も持たないようにする

DB やファイルシステムを使って読み書きするシステムは「不純」とみなされる → そのような関数はコアドメインから取り除く

❓ データの読み書きはどうやったらいい? → I/O をオニオンアーキテクチャの最外殻に保つ

  • データベースにアクセスするのは、ワークフローの内部ではなく最初と最後のみ
    • ドメインモデルに DB が登場しないのなら、ワークフローでも内部から DB にアクセスできてはいけない
  • 利点:
    • 関心が異なるもの同士を分離できる
      • コアドメインはビジネスロジックのみに集中
      • 永続化や他の I/O はインフラが管理する

まとめ

DDD に関する概念と定義をいくつか紹介した

  • ドメインオブジェクト: BC 内側でのみ使われるオブジェクト
  • DTO: シリアライズされて bc 間で共有されるためのオブジェクト
  • 共有カーネル、顧客/サプライヤ、適合者: 境界づけられたコンテキスト間の関係パターン
  • 腐敗防止層: あるドメインの概念を他のドメインのものに翻訳するコンポーネント。カップリングを減らし、各ドメインが独自に進化できるようにする
  • 永続化の無視: ドメインモデルはドメイン自体の概念のみ基づくべきであり、永続化メカニズムへの意識は含むべきでない

次に学ぶこと

ドメインを理解し、解決策を設計するアプローチを理解したことで、それぞれのワークフローの設計と実装に取り組めるようになった。

続く数章でやること:

  • F# の型システムを使ってワークフローとデータを定義する
  • 実際にコンパイル可能ながら、ドメインエキスパートや非開発者にも理解可能なコードを書く

そのために次の章で学ぶこと:

  • 関数プログラマにとって型とは何を意味するのか
  • クラスとどう違うのか

  1. Docker などの仮想環境技術の文脈における “コンテナ” ではないので注意 ↩︎


comments powered by Disqus