Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

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

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

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

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

Table of Contents

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

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

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

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

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

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

ツール

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

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

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

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

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

ポート

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

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

...

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

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

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

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

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

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

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

...

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

  • データの配列を保存

...

  • するメソッド
  • IDでテーブル内の行を削除するメソッド

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

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

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

制御の反転

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

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

...

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

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

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

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

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

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

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

...

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

どのアプローチを使用するかは、コンテキストに依存します。たとえば、次のようになります。どのアプローチを使用するかは場合によります。たとえば、次のようになります。

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

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

ドメインレイヤー

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

ドメインサービス

...

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

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

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

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

...

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

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

コンポーネント

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

...

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

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

コンポーネントの分離

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

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

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

...

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

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

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

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

...

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

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

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

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

...