アーキテクチャとは
ソフトウェアアーキテクチャとは、システムをどのような役割に分け、それぞれをどのような関係で接続するかを定めた基本設計です。
画面、業務処理、データベース、外部APIなどの責任範囲を明確にし、ある処理の変更がほかの処理へ与える影響を制御します。
代表的なアーキテクチャや設計パターンには、次のようなものがあります。
| 名前 | 概要 |
|---|---|
| レイヤードアーキテクチャ | 画面、業務処理、データアクセスなどを層に分け、原則として上位の層から下位の層を呼び出す |
| MVC | 表示を担当するView、データや処理を担当するModel、両者を仲介するControllerに分ける |
| ヘキサゴナルアーキテクチャ | 業務処理を中心に置き、HTTPやデータベースなどの外部技術をPortとAdapterで分離する |
| クリーンアーキテクチャ | 業務ルールを中心に置き、依存関係が外側から内側へ向かうように設計する |
| オニオンアーキテクチャ | ドメインを中心に配置し、外側へ向かってアプリケーションやインフラストラクチャを配置する |
| マイクロサービス | システムを機能単位の独立したサービスへ分割し、ネットワーク経由で連携させる |
| イベント駆動アーキテクチャ | 処理を直接呼び出す代わりに、発生したイベントを送信し、受信側が処理する |
| CQRS | データを更新する処理と参照する処理を分離する |
| イベントソーシング | 現在の状態だけでなく、状態を変更した出来事を履歴として保存する |
これらは比較する観点が異なります。例えばMVCは主に画面周辺の構成を整理するパターンであり、マイクロサービスはシステム全体をどの単位に分割するかを扱います。
ヘキサゴナルアーキテクチャとは
ヘキサゴナルアーキテクチャは、アプリケーションの業務処理と、HTTP、データベース、外部APIなどの技術的な処理を分離する設計です。
Ports and Adapters Architectureとも呼ばれます。
提唱者のAlistair Cockburnは、アプリケーションをUIやデータベースから切り離し、外部技術がなくても開発やテストを行える構成として説明しています。Hexagonal Architectureの原典
ヘキサゴナルという名称は、六角形そのものに技術的な意味があるわけではありません。アプリケーションが複数のPortを通じて、さまざまな外部技術と接続できることを表しています。
アプリケーションの内側と外側
内側に置くもの
内側には、そのアプリケーションが実現する業務処理を置きます。
例えば、注文を確定する、料金を計算する、データを検索するといった処理です。
内側の処理には、特定のHTTPフレームワーク、データベース製品、外部APIの通信方法などを直接書かないようにします。
外側に置くもの
外側には、具体的な技術に依存する処理を置きます。
HTTPリクエストの解析、データベースへのSQL実行、外部APIへのHTTP通信などです。
外側の処理は、内側が定めた接続方法に従ってアプリケーションと連携します。
Portとは
Portは、アプリケーションの内側と外側がやり取りするための接続方法を定義したものです。プログラムでは、インターフェースとして表現されることがあります。
外部APIからデータを取得する場合は、次のように必要な操作だけを定義します。
interface DataReader {
fetch(query: Query): Promise<Result>;
}
このインターフェースは、次の内容を定めています。
fetch()というメソッドを持つQueryを受け取るResultを非同期で返す
どのURLへアクセスするか、どのHTTPライブラリを使用するかといった具体的な通信方法は定めません。
Adapterとは
Adapterは、Portで定められた操作を具体的に実行する実装です。
アダプターにはプライマリーアダプターとセカンダリーアダプターがあります
プライマリーアダプターは、外部から入力を受け取り、アプリケーションの処理を呼び出します。Driving Adapterとも呼ばれます。
HTTPリクエストを受け付ける処理では、入力値の検証、業務処理の呼び出し、HTTPレスポンスへの変換などを担当します。
セカンダリーアダプターは、アプリケーションから呼び出され、データベースや外部APIなどへ接続します。Driven Adapterとも呼ばれます。

ヘキサゴナルアーキテクチャの依存関係について
依存関係は typescript であれば import を見ればいい
import を見ればいい。A.ts が B.ts を import していれば、A が B に依存している。矢印は A → B。B を消すと A が壊れ、A を消しても B は無傷。この非対称が向き。

業務処理からデータベースの実装クラスを直接呼び出すと、業務処理がそのデータベースに依存します。
const repository = new SpecificDatabaseRepository();
const result = await repository.find();
この状態では、データベースの種類や接続方法を変更すると、業務処理側にも変更が及ぶ可能性があります。
ヘキサゴナルアーキテクチャでは、内側が必要とする操作をPortとして定義し、外側のAdapterがそのPortを実装します。
内側の業務処理
↓ 利用する
Port
↑ 実装する
外側のAdapter
通常の実行時には、内側から外側の処理を呼び出します。しかし、ソースコード上では、外側のAdapterが内側で定義されたPortに従います。
これが依存関係の逆転です。内側が外側の具体的な実装へ依存するのではなく、外側の実装が内側で定められたPortへ依存します。
依存性注入とは
PortとAdapterを定義しただけでは、実行時にどのAdapterを使用するかは決まりません。
そこで、アプリケーションの起動時などにAdapterのインスタンスを生成し、利用する処理へ渡します。
newは、クラスからインスタンスを生成し、コンストラクターで初期化する処理です。
利用側は、渡されたインスタンスを使用します。
このように、利用側が実装クラスを自身で生成せず、外部から受け取る方法を依存性注入と呼びます。
依存性注入の要点は、インスタンスを起動時に生成することではありません。実装を設定・生成する処理と、その実装を利用する処理を分離することです。Martin Fowlerによる依存性注入の解説
Registryの役割
使用するAdapterが増えた場合、複数のインスタンスをRegistryへまとめて保持することがあります。
class ServiceRegistry {
readonly dataReader: DataReader;
constructor(dataReader: DataReader) {
this.dataReader = dataReader;
}
}
利用側は、Registryから必要なインスタンスを取得します。
await registry.dataReader.fetch(query);
Registryは、関連するインスタンスを一元管理して利用側へ提供するための仕組みです。
ただし、Registryはヘキサゴナルアーキテクチャの必須要素ではありません。必要なインスタンスを個別に渡す構成でも、依存性注入は実現できます。
HTTPリクエストから外部処理までの流れ
HTTPを入口とし、外部APIからデータを取得する場合、処理は次のように進みます。
HTTPリクエスト
↓
HTTPを扱うプライマリーアダプター
↓
入力値を検証
↓
アプリケーションの処理を呼び出す
↓
Portのメソッドを呼び出す
↓
セカンダリーアダプターが外部APIと通信
↓
結果をアプリケーションへ返す
↓
HTTPレスポンスへ変換
HTTPを扱う処理は、外部APIとの通信方法を知る必要がありません。Portで定義されたメソッドを呼び出すだけです。
外部APIのURL、ヘッダー、リクエスト形式、レスポンス変換などは、セカンダリーアダプターが担当します。
採用時の注意点
ヘキサゴナルアーキテクチャを採用すると、PortやAdapterなどのインターフェースと実装が増えます。
小規模で外部接続がほとんど変わらないシステムでは、分離による利点よりもコード量の増加が上回る場合があります。
一方、次のようなシステムでは効果を得やすくなります。
- 業務処理を長期間維持する
- 複数の外部APIやデータベースを利用する
- 外部技術が変更される可能性がある
- 業務処理を外部システムから切り離してテストしたい
- HTTP以外の入力方法にも対応する可能性がある
重要なのは、すべての処理を機械的にPortとAdapterへ分割することではありません。変更から守りたい業務処理と、交換される可能性がある外部技術の境界を見極めることです。