ヘキサゴナルアーキテクチャとは?Port・Adapter・依存性注入をわかりやすく解説

目次

アーキテクチャとは

ソフトウェアアーキテクチャとは、システムをどのような役割に分け、それぞれをどのような関係で接続するかを定めた基本設計です。

画面、業務処理、データベース、外部APIなどの責任範囲を明確にし、ある処理の変更がほかの処理へ与える影響を制御します。

代表的なアーキテクチャや設計パターンには、次のようなものがあります。

名前概要
レイヤードアーキテクチャ画面、業務処理、データアクセスなどを層に分け、原則として上位の層から下位の層を呼び出す
MVC表示を担当するView、データや処理を担当するModel、両者を仲介するControllerに分ける
ヘキサゴナルアーキテクチャ業務処理を中心に置き、HTTPやデータベースなどの外部技術をPortとAdapterで分離する
クリーンアーキテクチャ業務ルールを中心に置き、依存関係が外側から内側へ向かうように設計する
オニオンアーキテクチャドメインを中心に配置し、外側へ向かってアプリケーションやインフラストラクチャを配置する
マイクロサービスシステムを機能単位の独立したサービスへ分割し、ネットワーク経由で連携させる
イベント駆動アーキテクチャ処理を直接呼び出す代わりに、発生したイベントを送信し、受信側が処理する
CQRSデータを更新する処理と参照する処理を分離する
イベントソーシング現在の状態だけでなく、状態を変更した出来事を履歴として保存する

これらは比較する観点が異なります。例えばMVCは主に画面周辺の構成を整理するパターンであり、マイクロサービスはシステム全体をどの単位に分割するかを扱います。

FW・ドライバ アダプタ ユースケース エンティティ 依存 内側ほど方針、外側ほど詳細
アプリケーション (中身は自由) ポート ポート UI・API 駆動する側 DB・外部 駆動される側 アダプタがポートに依存する(両側とも内向き)

ヘキサゴナルアーキテクチャとは

ヘキサゴナルアーキテクチャは、アプリケーションの業務処理と、HTTP、データベース、外部APIなどの技術的な処理を分離する設計です。

Ports and Adapters Architectureとも呼ばれます。

提唱者のAlistair Cockburnは、アプリケーションをUIやデータベースから切り離し、外部技術がなくても開発やテストを行える構成として説明しています。Hexagonal Architectureの原典

アプリケーション 業務処理 内側 入力側 HTTP・CLI 出力側 DB・外部API Port Port プライマリーAdapter セカンダリーAdapter

ヘキサゴナルという名称は、六角形そのものに技術的な意味があるわけではありません。アプリケーションが複数の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.tsB.ts を import していれば、A が B に依存している。矢印は A → B。B を消すと A が壊れ、A を消しても B は無傷。この非対称が向き。

A.ts import { X } from “./B” B.ts A を知らない A が B に依存 矢印は import する側 → される側

業務処理からデータベースの実装クラスを直接呼び出すと、業務処理がそのデータベースに依存します。

const repository = new SpecificDatabaseRepository();
const result = await repository.find();

この状態では、データベースの種類や接続方法を変更すると、業務処理側にも変更が及ぶ可能性があります。

ヘキサゴナルアーキテクチャでは、内側が必要とする操作をPortとして定義し、外側のAdapterがそのPortを実装します。

内側の業務処理
  ↓ 利用する
Port
  ↑ 実装する
外側のAdapter

通常の実行時には、内側から外側の処理を呼び出します。しかし、ソースコード上では、外側のAdapterが内側で定義されたPortに従います。

依存関係の逆転 業務処理 Portを利用する Port 外部処理のAdapter 利用 実装

これが依存関係の逆転です。内側が外側の具体的な実装へ依存するのではなく、外側の実装が内側で定められたPortへ依存します。

外側 = 詳細(UI・DB・FW・外部API) 内側 = 方針 業務ルール 依存 依存

依存性注入とは

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へ分割することではありません。変更から守りたい業務処理と、交換される可能性がある外部技術の境界を見極めることです。

目次