翻訳 「DDD, Hexagonal, Onion, Clean, CQRS…これらをどうやり遂げるのか?」

DDD, Hexagonal, Onion, Clean, CQRS…これらをどうやり遂げるのか?

この記事は、ソフトウェアアーキテクチャについての一連の投稿、「ソフトウェア アーキテクチャ クロニクル」の1つです。
一連の投稿の中で、私がソフトウェアアーキテクチャについてどのように学び、どのように実践したのかを記しています。
この記事を読む前に以前の記事を読んでおくと、より理解しやすいと思われます。

私は大学を卒業したあと、高校教師として働いた数年後にフルタイムのソフトウェア開発者になりました。
それ以降、私は常に失った時間を取り戻し、出来る限り多くを早く学ぶ必要があると感じていました。
だから、私はやや中毒気味にソフトウェアの設計とアーキテクチャに関して重点を置いて検証や、読書、執筆を行ってきたのです。
それが自分の勉強のために私が記事を書いてきた理由です。

前回の記事では、私が学んできた多くの概念や原則について説明し、また私がどのように考えたかを少し書きました。
しかし、私はこれらを大きなパズルのピースにすぎないと考えています。

今回の記事は、私が明示的アーキテクチャ(Explicit Architecture)と呼ぶ、それらピースをどのように繋ぎ合わせるかについてです。
さらに、これらのコンセプトはすべていわゆる「戦闘試験」に合格していて、非常に要求の厳しいプラットフォームのプロダクションコードで実践されています。
1つは世界中に何千ものWebショップを抱えるe-comというSaaSプラットフォームで、もう1つは毎月2000万件以上のメッセージを処理するメッセージバスのあるマーケットプレイスです。

システムの基本的な構成要素

まずは、EBIポート&アダプタのおさらいから始めます。
どちらのパターンも「アプリケーションの内部コード」「外部コード」「内部コードと外部コードを接続するコード」を明確に区別できます。

さらに、ポート&アダプタアーキテクチャではシステム内のコードを3つの基本的なブロックを明示的に区別できます。

  • どんな種類のユーザインタフェースであっても、ユーザーインタフェースの実行を可能にするもの
  • ユーザインタフェースから呼び出される、実際に何かを起こすビジネスロジック、またはアプリケーションコア
  • アプリケーションコアをデータベース、検索エンジン、サードパーティAPIなどのツールへ接続するインフラストラクチャコード

アプリケーションコアは私達が一番関心を持つ部分です。これこそが私達の作るアプリケーションであり、アプリケーションが何を行うかを書いたコードです。
いくつかのユーザーインターフェイス(プログレッシブウェブアプリケーション、モバイル、CLI、APIなど)を使用するかもしれませんが、実際に作業を行っているコードは同じで、アプリケーションコアにあります。

下図のように、典型的なアプリケーションフローは、ユーザーインターフェイスのコードからアプリケーションコア、インフラストラクチャコード、アプリケーションコアに戻り、最終的にユーザーインターフェイスに応答します。

ツール

システムの最も重要なコードであるアプリケーションコアとは別に、データベースエンジン、検索エンジン、Webサーバー、CLIコンソールなどのアプリケーションで使用されるツールがあります(ただし、最後の2つは配信メカニズム)。

CLIコンソールなどユーザインタフェースをデータベースエンジンなどと同じ括りで扱うことは奇妙に感じられるかもしれません。
しかし、目的の種類は異なりるものの、実際にはアプリケーションによって使用されるツールです。
主な相違点は、CLIコンソールとWebサーバーはアプリケーションに何かを行わせるために使われますが、データベースエンジンはアプリケーションによって何かを実行するよう指示されます
これは、アプリケーションコアとそれらのツールを結びつけるコードをどのように作るかという点において非常に重要な違いとなります。

ツールと配信メカニズムのアプリケーションコアへの接続

ツールをアプリケーションコアに接続するコードユニットはアダプタと呼ばれます。
アダプタは、ビジネスロジックが特定のツールとやりとりするためのコードで、その逆のパターンのアダプタもあります。

アプリケーションに何をするのかを伝えるアダプタをプライマリアダプタ(または駆動アダプタ)と呼び、アプリケーションから何をするのかを伝えられるアダプタをセカンダリアダプタ(または被駆動アダプタ)と呼びます。

ポート

アダプタは行き当たりばったりに作られるわけではありません。
アプリケーションコアのエントリーポイントであるポートに合わせてアダプタは作成されます。
ポートは、ツールがアプリケーションコアをどのように使用できるか、またはアプリケーションコアによってどのように使用されるかの仕様に過ぎません
ほとんどの言語における最も単純な形式ではポートはいわゆるインタフェースで、実際にはいくつかのインタフェースとDTOで構成されます。

ポート(インタフェース)はビジネスロジック内に属し、アダプタは外部に属していることに注意することが重要です。
このパターンにおいてポートは、ツールのAPIを模倣するのではなく、アプリケーションコアのニーズに応じて作成することが最も重要です。

プライマリ(または駆動)アダプタ

プライマリアダプタ、または駆動アダプタはポートをラップし、アプリケーションコアに何をするかを伝えます。 
それらは、ユーザインタフェースなどの配信メカニズムから来るものをアプリケーションコアのメソッド呼び出しに変換します。

言い換えると、駆動アダプタはインタフェース(ポート)の実装クラスのオブジェクトが注入されるコントローラー(またはコンソールコマンド)です。

より具体的な例では、ポートは、コントローラが必要とするサービスインタフェースまたはリポジトリインタフェースです。
そして、サービス、リポジトリまたはクエリの実装が注入されコントローラで使用されます。

代わりに、ポートはコマンドバス(またはクエリバス)インタフェースであっても良いです。
この場合、コマンド(またはクエリバス)の実装がコントローラに注入され、コントローラはコマンド(またはクエリ)を構築し関連するバスに渡します。

セカンダリ(または被駆動)アダプタ

ポートをラップする駆動アダプタとは異なり、被駆動アダプタはポート(インターフェイス)を実装、アプリケーションコアにおいてポートが必要な場所(タイプヒンティング)に注入されます。

例として、データを永続化する必要のある素朴なアプリケーションについて考えてみましょう。
まず、アプリケーションの以下のニーズを満たすデータ永続化のためのインタフェースを作成します。

  • データの配列を保存するメソッド
  • IDでテーブル内の行を削除するメソッド

アプリケーションがデータを保存または削除する必要がある箇所で、このインタフェースの実装クラスのオブジェクトが必要になります。

そこで、このインタフェースを実装するMySQL固有のアダプタを作成します。
そのアダプタは配列を保存するメソッドと、テーブル内の行を削除するメソッドを備えています。
そして、インタフェースが必要な場所にオブジェクト(アダプタ)は注入されアプリケーションに利用されます。

もしある時点でデータベース製品をPostgreSQLやMongoDBへ変更することになった場合、永続化のためのインタフェースをPostgreSQLに合わせて実装するだけで良いことがわかります。

制御の反転

このパターンの特徴は、アダプタが(インタフェースを実装することによって)特定のツールと特定のポートに依存することです。
ビジネスロジックは、ビジネスロジックのニーズに合わせて設計されたポート(インターフェイス)のみに依存するため特定のアダプタやツールに依存しません。

これは、依存関係の方向が中心に向いていることを意味します。これは、アーキテクチャレベルでの制御の逆転です。
繰り返しになりますが、ポートは、ツールのAPIを模倣するのではなく、アプリケーションコアのニーズに応じて作成することが最も重要です。

アプリケーションコアの構成

オニオンアーキテクチャはDDDのレイヤーをポート&アダプタアーキテクチャへと組み込んだものです。
レイヤーはポート&アダプタの六角形の内側、ビジネスロジックへと何らかの編成をもたらすもので、ポート&アダプタと同様に依存関係の方向は中心へと向かいます。

アプリケーション レイヤー

ユースケースとは、ユーザインタフェースより呼び出すことができるアプリケーションコアの一連の処理です。
たとえば、CMSでは、一般ユーザーが使用する実際のアプリケーションUI、管理者用の別の独立したUI、別のCLI UI、およびWeb APIを持つことができます。
これらのUI(アプリケーション)は、(ユーザインタフェース毎に専用ではない)再利用されるユースケースを呼び出します。

ユースケースはアプリケーションレイヤーで定義される一番最初のレイヤーであり、オニオンアーキテクチャで使われるものです。

アプリケーションレイヤーには第一級市民としてアプリケーションサービス(とそのインタフェース)が含まれますが、ORMインタフェースや検索エンジンインタフェース、メッセージングインタフェースなどのポート&アダプタのポート(インタフェース)も含まれます。
コマンドバスやクエリバスを使う場合では、このレイヤーはコマンドやクエリのそれぞれのハンドラーが属する場所です。

アプリケーションサービス(またはコマンドハンドラ)にはビジネスプロセスであるユースケースを展開するためのロジックが含まれています。通常、役割は次の通りです。

  1. リポジトリを使用して1つまたは複数のエンティティを検索する。
  2. それらのエンティティにいくつかのドメインロジックを実行するよう指示する。
  3. リポジトリを使用してエンティティを再び永続化し、効果的にデータの変更を保存する。

コマンドハンドラは、次の2つの方法で使用できます。

  1. ユースケースを実行する実際のロジックを含むことができる
  2. 単純な配線として使用することができ、コマンドを受け取り、単にアプリケーションサービスに存在するロジックを呼び出すことができる

どのアプローチを使用するかは場合によります。たとえば、次のようになります。

  • すでにアプリケーションサービスを導入しており、現在コマンドバスを追加しているか?
  • コマンドバスは、どんなクラス/メソッドをハンドラとして指定することを許可するか、既存のクラスまたはインタフェースを拡張または実装する必要があるか?

このレイヤーにはユースケースの結果を表すアプリケーションイベントのトリガーも含まれて います。これらのイベントは、電子メールの送信、サードパーティAPIの通知、プッシュ通知の送信、アプリケーションの別のコンポーネントに属する別のユースケースの開始など、ユースケースの副作用であるロジックを呼び出します。

ドメインレイヤー

さらに内側にはドメイン層があります。このレイヤー内のオブジェクトにはデータと、そのデータを操作するためのロジックが含まれています。
ドメイン層のオブジェクトはドメイン自体に固有であり、ユースケースとは独立しており、アプリケーションレイヤーからも独立していてアプリケーションレイヤーの知識を含みません。

ドメインサービス

上記のとおり、アプリケーションサービスの役割は次のとおりです。

  1. リポジトリを使用して1つまたは複数のエンティティを検索する。
  2. それらのエンティティにいくつかのドメインロジックを実行するよう指示する。
  3. リポジトリを使用してエンティティを再び永続化し、効果的にデータの変更を保存する。

しかし、時には2つ以上の同種のエンティティか、異なる種類のエンティティを横断するようなドメインロジックに遭遇することがあります。
そして、そのドメインロジックはエンティティ自体に属していないと感じ、また、そのロジックはそれらエンティティの直接の責務ではないと感じることがあるでしょう。

したがって、我々が最初によくやってしまうのはそのロジックをエンティティの外であるアプリケーションサービスとすることです。
しかし、これは他のユースケースではそのドメインロジックが再利用できないことを意味します。
ドメインロジックはアプリケーションレイヤーにあってはいけません!

解決策はドメインサービスを作成することです。ドメインサービスは一連のエンティティを受け取り、それらにいくつかのビジネスロジックを実行する役割を担います。
ドメインサービスはドメインレイヤーに属しているため、アプリケーションサービスやリポジトリのように、アプリケーションレイヤーのクラスについては何も知りません。
一方、他のドメインサービス、もちろんドメインモデルオブジェクトを使用することができます。

ドメインモデル

ドメインモデルは一番の中心にあり、外部には何も依存していません。
ドメインモデルにはドメイン内の何かを表すビジネスオブジェクトが含まれます。
それらのオブジェクトの例としては、エンティティ、値オブジェクト、列挙型、およびドメインモデルで使用されるすべてのオブジェクトがあります。

ドメインモデルはドメインイベントが息づく場所でもあります。
ドメインイベントは特定のデータセットが変更されたときに呼び出され、それらの変更と共に実行されます。
つまり、エンティティが変更されるとドメインイベントが呼び出され、変更されたプロパティの新しい値が伝達されます。
これらのイベントはたとえばイベントソーシングで使用するのに最適です。

コンポーネント

これまでは、レイヤーに基づいてコードを分離していましたが、それはきめ細かなコードの分離です。
Robert C. MartinがScreaming Architectureで記したように、サブドメインや境界付けられたコンテクストに沿った粗い粒度でのコードの分離も重要です。
これはしばし「レイヤーによるパッケージ」とは対照的に、「機能によるパッケージ」「コンポーネントによるパッケージ」と呼ばれ、Simon Brown氏のブログ記事「Package by component and architecturally-aligned testing」でよく説明されています。

私は「コンポーネントによるパッケージ」のアプローチの支持者で、Simon Brownのコンポーネントによるパッケージの図に手を加え下図のようにしました。

上図のコンポーネントのパッケージ化の方法は前述したレイヤーとは分野横断的な考え方になっています。
構成要素の例は、課金、ユーザー、レビュー、アカウントのいずれかになりますが、常にドメインに関連しています。
また、認証や認可のような束縛されたコンテキストは、アダプタを作成してある種のポートの背後に隠れる外部ツールと見なすべきです。

コンポーネントの分離

クラスやインタフェース、トレイト、ミックスインのようなきめの細かい粒度のコードと同様に、粗い粒度のコード(コンポーネント)には疎結合で凝集性が高いという利点があります。

クラスを分離するために、クラス内に依存性を具体的に記述するのではなくクラスへ依存性を注入するDependency Injectionと、具象クラスの代わりにインタフェースや抽象クラスなどの抽象へと依存させる Dependency Inversionを使います。
つまり、あるインタフェースを必要とするクラスは、依存するクラスの完全修飾クラス名など、使用する具体的なクラスについての知識は必要ありません。

同様に、完全に分離されたコンポーネントとは、コンポーネントが他のコンポーネントを直接認識していないことを意味します。
言い換えれば、別のコンポーネントからの参照はなく、インタフェースすら含まないということです!これはDependency InjectionとDependency Inversionがコンポーネントを分離するのに十分ではないことを意味し、何らかのアーキテクチャ構築が必要となり、イベント、共有カーネル、結果整合性、ディスカバリサービスさえ必要とするかもしれません!

他コンポーネントのロジックの呼び出し

あるコンポーネントBが別のコンポーネントAで何か他のことが起こるたびに何かをする必要がある場合、コンポーネントAからコンポーネントBのクラス/メソッドに直接呼び出すことはできません。
なぜなら、そうするとコンポーネントAがコンポーネントBと結合してしまうからです。

しかし、Aはイベントディスパッチャーを使用して、Bを含む任意のコンポーネントに配信されるアプリケーションイベントをディスパッチし、Bのイベントリスナーが目的のアクションをトリガーするようにすることができます。
つまり、コンポーネントAはイベントディスパッチャに依存しますがBからは切り離せます。

それにもかかわらず、イベント自体がAに存在する場合、BはAの存在を知っていることを意味し、Aに結合されています。
この依存を取り除くために、アプリケーションコアの機能を使った全コンポーネントで共有される共有カーネルというライブラリを作ることができます。
つまり、コンポーネントは共に共有カーネルに依存しますが、それらは互いに分離されています。
共有カーネルには、アプリケーションやドメインイベントなどの機能が含まれますが、インタフェースなどの仕様にまつわるオブジェクトや、共有するのに意味のあるものも含めることができます。
ただし、共有カーネルの変更はすべてのコンポーネントに影響するため、できるだけ最小限に抑える必要があります。
さらに、さまざまな言語で書かれたマイクロサービスのエコシステムを考えてみましょう。
共有カーネルはすべてのコンポーネントで理解できるようにする必要があるため、言語に依存しないようにする必要があります。
たとえば、イベントクラスを含む共有カーネルの代わりに、言語によらないJSONでイベントの説明(例えば、名前、プロパティ、メソッド)が含まれます。
コンポーネント/マイクロサービスはそれを解釈して、独自の具体的な実装を自動生成することさえできます。

このアプローチはモノリシックアプリケーションとマイクロサービスエコシステムのような分散アプリケーションの両方で機能します。
ただし、イベントを非同期にしか配信できない場合、他のコンポーネントのトリガロジックをすぐに実行する必要のあるコンテキストではこの方法では十分ではありません。
コンポーネントAは、コンポーネントBに直接HTTPコールを行う必要があります。
この場合、コンポーネントを分離するには、Aが目的のアクションをトリガーする要求を送信する先を問い合わせるディスカバリサービスか、もしくは、関連するサービスにリクエストをプロキシし、最終的にリクエスト送信元にレスポンスを返すディスカバリサービスが必要です。
このアプローチでは、コンポーネントをディスカバリサービスに結合しますが、それらを互いに分離した状態にします。

他コンポーネントからのデータ取得

私に言わせれば、コンポーネントは「所有していない」データを変更することはできませんが、どのデータも問い合わせをして使用することができます。

コンポーネント間で共有されるデータストレージ

コンポーネントが別のコンポーネントに属するデータを使用する必要がある場合、請求コンポーネントがアカウントコンポーネントに属する顧客名を使用する必要があるとします。
請求コンポーネントには、そのデータのデータストレージを照会するクエリオブジェクトが含みます。
つまり、請求コンポーネントはどのデータも知ることができますが、クエリによって読み取り専用として「所有していない」データを使用する必要があります。

コンポーネント毎に分離されたデータストレージ

この場合、同じパターンが適用されますが、データストレージレベルでは複雑さが増します。
独自のデータ記憶手段を有するコンポーネントをあることにより、各データストレージは以下を含むことになります。

  • コンポーネントが所有するデータセットで、コンポーネントが変更することができる唯一のデータセットであり、単一のソースとする。
  • コンポーネントの機能に必要な他のコンポーネントデータのコピーで、コンポーネント自身では変更できない。所有者コンポーネントで変更されるたびに更新する必要があるデータのセット

各コンポーネントは、必要なときに使用するために、他のコンポーネントから必要なデータのローカルコピーを作成します。
データを所有するコンポーネントでデータが変更されると、その所有者コンポーネントはデータ変更を伴うドメインイベントをトリガーします。
そのデータのコピーを保持するコンポーネントは、そのドメインイベントをリッスンし、それに応じてローカルコピーを更新します。

処理の流れ

上で述べたように、処理の流れは、ユーザーからアプリケーションコアに、インフラストラクチャツールに、アプリケーションコアに戻って、最終的にユーザーに戻ってきます。
しかし、どのようにクラスが当てはまるのでしょうか?どれがどちらに依存するのでしょうか?どのようにそれらを構成するのでしょうか?

Bobおじさんのクリーンアーキテクチャの記事にならい、UML風の図を使って処理の流れがどうなるのか説明してみます。

コマンド/クエリバスなし

コマンドバスを使用しない場合、コントローラはアプリケーションサービスまたはクエリオブジェクトのいずれかに依存します。

アプリケーションサービスはアプリケーションの一部であり、他の実装に差し替えることはないだろうという考えから、上の図でアプリケーションサービスのインタフェースを使っていることについては議論があるかもしれませんが、もしかしたら全体をリファクタリングするかもしれません。

クエリーオブジェクトには、ユーザーに表示される生データを単に返すだけの最適化されたクエリが含まれます。
そのデータはViewModelに注入されることになるDTOとして返されます。このViewModelはいくつかのビューロジックを含み、DTOをビューを生成するために使います。

一方、アプリケーションサービスにはユースケースのロジックが含まれます。
これはデータを表示するロジックではなく、システムで何かを実行するときに呼び出されるロジックです。アプリケーションサービスは、呼び出される必要があるロジックを含むエンティティを返すリポジトリに依存します。
また、複数のエンティティに渡るドメインロジックを調整するドメインサービスに依存することもありますが、これはほとんどありません。

ユースケースを展開した後、アプリケーションサービスはユースケースが呼び出されたことをシステム全体に通知したい場合があります。
その場合、イベントディスパッチャーによってイベントがトリガーされます。

興味深いのは、永続エンジンとリポジトリの両方にインターフェイスを配置していることです。
冗長に見えるかもしれませんが、さまざまな目的を果たします。

  • PersistenceインターフェイスはORM上の抽象レイヤであるため、アプリケーションコアに変更を加えずに使用されているORMを交換することができます。
  • Repositoryインタフェースは、永続性エンジン自体の抽象化です。MySQLからMongoDBに切り替えるとしましょう。Persistenceインタフェースは同じであっても構いません。同じORMを使用し続ける場合は、Persistenceアダプタも同じままです。ただし、クエリ言語はまったく異なりますので、同じ永続性メカニズムを使用する新しいリポジトリを作成し、同じRepositoryインターフェイスを実装しますが、SQLではなくMongoDBクエリ言語を使用してクエリを構築します。

コマンド/クエリバスあり

私たちのアプリケーションがコマンド/クエリバスを使用する場合、コントローラはバスと、コマンドまたはクエリに依存することを除けば、図はほとんど変わりません。
コマンドまたはクエリをインスタンス化し、コマンドを受け取って処理するための適切なハンドラを見つけるバスに渡します。

以下の図では、コマンドハンドラがアプリケーションサービスを使用しています。ただし、必ずしもそうである必要はありません。
実際、ほとんどの場合、ハンドラにはユースケースのすべてのロジックが含まれています。
同じロジックを別のハンドラで再利用する必要がある場合は、ハンドラからロジックを抽出して、アプリケーションサービスに分割するだけで済みます。

バスとコマンド、クエリとハンドラの間に依存関係がないことに気づいたかもしれません。
これは疎結合を保つために
互いを知るべきではないからです。ハンドラがコマンドやクエリをどのように扱うべきかをバスが知る方法は単なる設定とすべきです。

ご覧のように、どちらの場合も、アプリケーションコアの境界を横切るすべての矢印や依存関係は内向きです。
前に説明したように、これはポート&アダプタアーキテクチャ、オニオンアーキテクチャ、クリーンアーキテクチャの基本ルールです。

まとめ

目標はコードベースの疎結合さと高凝集性を保ち、迅速で安全な変更を容易にすることです。

描いた図はコンセプトマップです。
これらの概念をすべて理解し理解することは、健全なアーキテクチャ、健全なアプリケーションの計画に役立ちます。

それでも、

これらはただのガイドラインです!アプリケーションが私たちの知識を適用する必要がある具体的なユースケースです。それが実際のアーキテクチャの外観を定義します。

これらのパターンをすべて理解する必要がありますが、アプリケーションが必要とするものを正確に考え、どこまで疎結合と凝集性にすべきなのか、理解する必要があります。
これらの決定には、プロジェクトの機能要件から始まる多くの要素にもよりますが、アプリケーションを構築する時間や、アプリケーションの寿命、開発チームの経験なども要素として含めることができます。

そのためのこの記事であり、私が理解する方法であり、私が頭の中で合理化する方法です。

しかし、このすべてをコードベースで明示的にするにはどうすればよいでしょうか?
コードでアーキテクチャとドメインをどのように表現するかについては次の記事のテーマとします。