多くのコードベースは、フレームワークを中心に始まります。コントローラーがサービスを呼び、サービスが ORM を呼び、ビジネスルールはその 3 つすべてに漏れ出します。すると、価格ルールをテストするだけで Web サーバーとデータベースが必要になり、決済プロバイダーを置き換えるには、注文の金額を決めるコードに手を入れることになります。
ヘキサゴナルアーキテクチャ(ポートとアダプターとも呼ばれます)は、これを逆転させます。この記事では、考え方と用語(ポート、駆動側と被駆動側のアダプター、依存関係のルール)を説明し、注文サービスの図を示して、このパターンが追加のファイルに見合うのはどんなときかを解説します。
1 文で言うと
ビジネスロジックを、HTTP やデータベースやベンダーについて何も知らないコアに置き、それ以外のすべてを、コアが所有するインターフェースを通じてコアにつなぎます。
Alistair Cockburn は 2005 年に、ヘキサゴナルアーキテクチャに関する最初の記事 でこのパターンを説明しました。その意図は、アプリケーションをユーザー、プログラム、自動テスト、バッチスクリプトのどれからでも同じように駆動できるようにし、最終的に使うデータベースやデバイスから切り離して開発・テストできるようにすることです。六角形という形には、辺に複数のポートを描く余地を残すという以上の意味はありません。
用語
| 用語 | 何か | 注文サービスでの例 |
|---|---|---|
| コア | ユースケースとドメインモデル | PlaceOrder、Order、Money、価格ルール |
| 入力(駆動側)ポート | コアが外部に提供するインターフェース | PlaceOrder.execute(command) |
| 出力(被駆動側)ポート | コアが外部に求めるインターフェース | OrderRepository、PaymentGateway |
| 駆動側アダプター | 外部からの入力をポートの呼び出しに変換する | REST コントローラー、CLI、メッセージコンシューマー、テスト |
| 被駆動側アダプター | 実際の技術で出力ポートを実装する | Postgres のリポジトリ、決済 API クライアント |
これらをまとめるのが 依存関係のルール です。ソースコードの依存関係は内側を向きます。アダプターはポートに依存し、ポートとコアは六角形の外側の何にも依存しません。実行時にはコアが決済ゲートウェイを呼び出しますが、コード上では決済アダプターがコアのインターフェースに依存しており、その逆ではありません。
具体例
注文を受けるには、注文を保存し、顧客に請求する必要があります。コアでは次のようになります。
// ports, owned by the core
interface OrderRepository { save(order: Order): Promise<void> }
interface PaymentGateway { authorize(amount: Money, token: string): Promise<AuthResult> }
// use case
class PlaceOrder {
constructor(private orders: OrderRepository, private payments: PaymentGateway) {}
async execute(cmd: PlaceOrderCommand): Promise<OrderId> {
const order = Order.create(cmd.items, cmd.customerId) // domain rules
const auth = await this.payments.authorize(order.total(), cmd.paymentToken)
if (!auth.ok) throw new PaymentDeclined(auth.reason)
await this.orders.save(order.markPaid(auth.id))
return order.id
}
}
コアの外では、PostgresOrderRepository が SQL で OrderRepository を実装し、PaymentApiGateway が HTTP 呼び出し、再試行、エラーの変換で PaymentGateway を実装します。REST コントローラーはリクエストを解析し、PlaceOrderCommand を組み立ててユースケースを呼び出します。テストもインメモリのアダプターで同じことを行い、Web サーバーもデータベースも必要ありません。
フォルダーへの対応付け
フォルダー構成がはっきり示していれば、この構造は保ちやすくなります。
src/
core/
domain/ Order, Money, pricing rules
usecases/ PlaceOrder, CancelOrder
ports/ OrderRepository, PaymentGateway
adapters/
http/ REST controllers (driving)
postgres/ repository implementations (driven)
payments/ payment API client (driven)
main.ts wiring: builds adapters and passes them to use cases
core/ 以下のものが adapters/ やフレームワークのパッケージから import したらビルドを失敗させる、lint ルールかアーキテクチャテストを追加しましょう。このルールはうっかり破りやすい一方、自動で強制するのは安上がりです。
よくある間違い
- 技術の形をしたポート。
OrderRepository.executeQuery(sql)は、名前を変えただけのデータベース API です。ポートには、コアが必要とするものの名前を付けます。saveやfindOpenOrdersFor(customer)のようにです。 - ORM のエンティティを兼ねたドメインオブジェクト。 永続化フレームワークのアノテーションが、データベースをコアに引き込みます。ドメインオブジェクトと永続化モデルの変換はアダプターで行いましょう。
- ビジネスルールを持つアダプター。 500 ドルを超える返金には承認が必要だと判断する決済アダプターは、ルールをコアの外に移してしまっています。アダプターの役割は変換と転送に限りましょう。
- クラスごとのポート。 ポートは境界のためにあるもので、コア内部の協力者ごとに作るものではありません。
アーキテクチャ図
ダイアグラムの内容
- PlaceOrder use case
- Domain model
- PlaceOrder port
- OrderRepository port
- PaymentGateway port
- REST controller
- Test driver
- Postgres adapter
- Payment API adapter
- Postgres
- Payment provider
- calls
- implements
- SQL
- HTTPS
図の読み方は次のとおりです。
- 六角形 はアプリケーションのコアです。PlaceOrder ユースケースとドメインモデルがあり、フレームワークやベンダーのコードはありません。
- 左、駆動側: REST コントローラーとテストドライバーが、どちらも入力ポート PlaceOrder を呼び出します。コアから見ると、両者は交換可能です。
- 右、被駆動側: ユースケースは 2 つの出力ポートを呼び出します。中抜きの矢じりが付いた破線のエッジは、Postgres アダプターと決済 API アダプターがそれらを実装していることを示しており、これがコードの依存関係の向きです。
- 右端: 実際の Postgres データベースと決済プロバイダーで、これらを知っているのはアダプターだけです。
- Markdown ブロック には依存関係のルールを書いています。レビュアーが確認すべき唯一のことです。
テストが安くなる
メリットの大部分はテストに表れます。
- コアのテスト はインメモリのアダプターを使い、数ミリ秒で実行されます。価格ルール、状態遷移、エラー処理を I/O なしでテストできます。
- アダプターのテスト は、1 つのアダプターを実際の技術に対して確認します。Postgres のリポジトリは本物の Postgres に対して、決済アダプターはプロバイダーのサンドボックスに対してテストします。
- 少数のエンドツーエンドテスト で、全体の接続を確認します。
この分け方はテストピラミッドを具体化したものであり、小さなサービスでもチームがこのパターンを採用する理由になっています。
新しい入口を追加する
入力ポートは、2 つ目のチャネルが来た最初のときに元が取れます。たとえば、Web に加えて、パートナーのメッセージキューからも注文を受けたいとビジネス側が言い出したとします。メッセージを読み、同じ PlaceOrderCommand を組み立てて、同じポートを呼び出すコンシューマーを書けば済みます。検証、価格計算、決済のロジックは変わらず、それらのテストも変わりません。ポートがなければ、たいていコンシューマーの中に注文ロジックの 2 つ目のコピーができ、数回のリリースのうちに 1 つ目からずれていきます。
見合わない場合
- ルールのない CRUD。 サービスが主に HTTP とテーブルの間でデータを移すだけなら、ポートは単なる儀式です。
- 短命なコード。 プロトタイプやスクリプトでは、追加のインターフェースの元が取れません。
- ずっと 1 つだけのアダプター。 データベースを入れ替えることがなく、どのみち本物のデータベースに対してテストするなら、出力ポートは見返りの少ない間接層を加えるだけです。それでも、コアにフレームワークのコードを入れないことは守りましょう。
ヘキサゴナルアーキテクチャは、オニオンアーキテクチャやクリーンアーキテクチャの近い親戚です。レイヤーの用語は異なりますが、依存関係のルールは共通です。
図に描くべきもの
- コアの境界: ドメインとユースケースだけをはっきり囲む形で描きます。
- 境界上のポート: コアが呼ぶとおりの名前を付けます。
- 外側のアダプター: 駆動側か被駆動側かと、包んでいる技術をラベルに書きます。
- 被駆動側アダプターの依存関係の向き: 呼び出しの向きとは異なります。
1 つのサービスではなくシステム全体については、C4 モデルの図の例 でコンテナからコンポーネントへのズームの仕方を、マイクロサービスのアーキテクチャ図 でこのようなサービスの位置付けを示しています。決済アダプターの裏側で起きることは 決済ゲートウェイ連携の図 で扱っています。
Archboard で作る
- Google または GitHub でサインインし、図を作成します。
- 基本パックからアプリケーションのコア用に 六角形 を追加し、テキスト のラベルを付けます。
- その中に、C4 モデルパックからユースケースとドメインモデル用の コンポーネント ノードを追加し、3 つのポート用の コンポーネント ノードを境界上に置きます。
- 外側に、駆動側と被駆動側のアダプター用の コンテナ ノードを追加し、データパックの Postgres と、決済プロバイダー用の 外部システム も追加します。
- 実装を表すエッジを中抜きの矢じり付きの破線で描き、依存関係のルールを書いた基本パックの Markdown ブロックを追加します。
この例で使うダイアグラムノードは 13 個で、Free プランの範囲に収まります。
よくある質問
ポートとアダプターの違いは何ですか? ポートはコアが所有するインターフェースです。アダプターは、そのインターフェースを REST フレームワークやデータベースのような特定の技術につなぐコードです。
駆動側と被駆動側のアダプターとは何ですか? 駆動側アダプターは、コントローラー、コンシューマー、テストのように、コアとのやり取りを開始します。被駆動側アダプターは、リポジトリや API クライアントのように、コアから呼び出されます。
ヘキサゴナルアーキテクチャはクリーンアーキテクチャと同じですか? 依存関係のルールは共通で、依存関係はドメインに向かいます。クリーンアーキテクチャとオニオンアーキテクチャはコアの中に名前の付いたレイヤーを加えますが、ヘキサゴナルアーキテクチャは内側と外側を区別するだけです。
すべてのサービスにヘキサゴナルアーキテクチャが必要ですか? いいえ。本物のビジネスルールがあり、それを駆動したり保存したりする方法が複数ある場所で効果を発揮します。薄い CRUD サービスでは、ほとんどが余分なファイルになります。
トランザクションはどこに置きますか? 通常はユースケースか Unit of Work のポートに置きます。何を一緒にコミットすべきかはコアが決め、どうやるかはアダプターが決めます。