クリーンアーキテクチャの図は、多くのエンジニアが目にしています。中心に向かう矢印が付いた、4 つの色付きの輪です。しかし、実際の機能で各輪に何を入れるのか、import がどちらを向くべきか、SQL がどこにあるのかを説明できる人はずっと少なくなります。
このギャップはコードレビューで表れます。ORM を import する domain フォルダー、HTTP ステータスコードを返す use case、フォームとテーブルの間にある 5 つの mapper などです。
この記事では、Robert C. Martin の円を、注文を行うという 1 つの use case のコンポーネント図に変えます。取り上げるのは、レイヤー、依存関係のルール、コードとフォルダー、境界をまたぐ流れ、クリーンとヘキサゴナルおよびオニオンの違い、よくある間違い、過剰になる場面、そして図の描き方です。
4 つのレイヤー
Martin は 2012 年のブログ記事 The Clean Architecture でクリーンアーキテクチャを説明し、2017 年の著書 Clean Architecture で内容を広げました。ヘキサゴナルアーキテクチャやオニオンアーキテクチャを含む、それ以前の考え方の統合として提示しています。中心から外側に向かって、次のようになります。
| レイヤー | 保持するもの | 例では | 変更されるとき |
|---|---|---|---|
| Entities | 複数のアプリケーションで再利用できる、企業全体のビジネスルール | Order と、注文には少なくとも 1 行が必要というルール |
ビジネスルールが変わるとき |
| Use cases | アプリケーション固有のビジネスルール。1 つにつき 1 つのワークフロー | PlaceOrder、その 2 つのインターフェース、リクエストとレスポンスのモデル |
ワークフローが変わるとき |
| Interface adapters | コントローラー、presenter、ゲートウェイ、およびすべての SQL | OrderController、OrderPresenter、PostgresOrderRepository |
API またはスキーマが変わるとき |
| Frameworks and drivers | ツールとつなぎのコード:Web フレームワーク、データベース、ドライバー | HTTP フレームワーク、PostgreSQL、pg、main.ts |
ツールを置き換えるとき |
円は概念図であり、Martin は 4 つ以上必要になることもあると述べています。それらをつなぐルールは決して変わりません。
依存関係のルール
Martin の言葉では、「ソースコードの依存関係は内側にしか向けられない」ということです。内側の円にあるコードは、外側の円で宣言された関数、クラス、変数の名前を一切出さず、そのデータ形式も使いません。この例では次のようになります。
entitiesは、ほかのレイヤーから何も import しません。use-casesが import するのはentitiesだけです。Web フレームワーク、ORM、ドライバーは使いません。adaptersは内側を import し、さらに adapt 対象のライブラリを import します。- すべてを見渡せるのは
main.tsだけです。
このルールが対象とするのはソースコードであり、実行時の呼び出しではありません。実行時には、PlaceOrder は presenter とデータベースへ外向きに呼び出します。Martin はこれを依存性逆転の原則で解決します。use case は自分が所有するインターフェースを呼び出し、外側のクラスがそれを実装します。呼び出しは外へ向かい、依存関係は内を向きます。
1 つだけ、外を向く依存関係があります。adapter がデータベースドライバーを import することです。これは意図的な設計です。Martin はすべての SQL を adapter レイヤーに置き、それより内側のコードは「データベースについて何も知るべきではない」としています。
このルールは CI で強制します。JVM では ArchUnit の onionArchitecture() チェック、TypeScript では dependency-cruiser、Python では import-linter の layers contract を使います。
実践例:注文を行う
POST /orders リクエストは次の経路をたどります。
- Web フレームワークがそれを
OrderControllerにルーティングします。 - コントローラーは本文を
PlaceOrderRequestにパースします。これは use cases レイヤーが所有するプレーンなオブジェクトです。続いてOrderPresenterを作成し、PlaceOrder.execute()を呼びます。 PlaceOrderはOrder.place()にビジネスルールを適用させ、OrderRepository経由で保存し、PlaceOrderResponseをPlaceOrderOutputに渡します。OrderPresenterはそのインターフェースを実装し、view model を組み立てます。ステータス 201、Locationヘッダー、JSON の本文です。- コントローラーが view model を返し、フレームワークがそれを書き出します。
use cases レイヤーと、それが所有する 2 つのインターフェースです。
// src/orders/use-cases/place-order.ts
import { Order } from '../entities/order';
export interface OrderRepository { // implemented by an adapter
nextId(): Promise<string>;
save(order: Order): Promise<void>;
}
export interface PlaceOrderOutput { // implemented by the presenter
present(response: PlaceOrderResponse): void;
}
export type PlaceOrderRequest = {
customerId: string;
lines: { sku: string; quantity: number }[];
};
export type PlaceOrderResponse = { orderId: string; itemCount: number };
export class PlaceOrder {
constructor(private readonly orders: OrderRepository) {}
async execute(request: PlaceOrderRequest, output: PlaceOrderOutput): Promise<void> {
const order = Order.place(await this.orders.nextId(), request.customerId, request.lines);
await this.orders.save(order);
output.present({ orderId: order.id, itemCount: order.itemCount() });
}
}
import するのは entity だけです。Order.place() は、注文に行がない場合や数量が 1 未満の場合に例外を投げ、entity のファイルは何も import しません。adapter は内側を import し、さらにドライバーを import します。
// src/orders/adapters/postgres/postgres-order-repository.ts
import { randomUUID } from 'node:crypto';
import type { Pool } from 'pg'; // adapters may import the driver
import type { Order } from '../../entities/order';
import type { OrderRepository } from '../../use-cases/place-order';
export class PostgresOrderRepository implements OrderRepository {
constructor(private readonly pool: Pool) {}
async nextId(): Promise<string> { return randomUUID(); }
async save(order: Order): Promise<void> {
await this.pool.query('INSERT INTO orders (id, customer_id, lines) VALUES ($1, $2, $3)',
[order.id, order.customerId, JSON.stringify(order.lines)]);
}
}
すべての具象クラスを知っているのは、composition root だけです。
// src/main.ts
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const controller = new OrderController(new PlaceOrder(new PostgresOrderRepository(pool)));
app.post('/orders', async (req, res) => {
const view = await controller.post(req.body); // the presenter's view model
res.status(view.status).set(view.headers).json(view.body);
});
PlaceOrder のユニットテストに必要なのは、インメモリのリポジトリと、受け取った入力を記録する presenter だけです。サーバーもデータベースも要りません。
フォルダー構成
src/
orders/
entities/
order.ts Order and its rules
use-cases/
place-order.ts PlaceOrder, both interfaces, request and response models
adapters/
http/
order-controller.ts
order-presenter.ts
postgres/
postgres-order-repository.ts
main.ts composition root: builds and wires everything
まず機能で、次にレイヤーでグループ化します。Martin の screaming architecture の主張は、トップレベルのフォルダーは、どのフレームワークを使っているかではなく、システムが何をするか(orders、payments)を語るべきだ、というものです。同じ書籍にある Simon Brown の章は、整理とカプセル化を分けて考えます。フォルダーはコードを整理し、アクセスルールやビルド時のチェックがそれを強制します。
境界をまたぐ
境界をまたぐたびに、どちらかのレイヤーが所有する型が運ばれます。
| またぐ箇所 | 運ばれるもの | 型の所有者 | 仕組み |
|---|---|---|---|
| Framework → controller | HTTP リクエスト | Framework | main.ts のつなぎのコード |
| Controller → use case | PlaceOrderRequest |
Use cases | 直接呼び出し |
| Use case → presenter | PlaceOrderResponse |
Use cases | PlaceOrderOutput インターフェース |
| Use case → データベース | Order |
Entities | OrderRepository インターフェース |
| Presenter → framework | View model | Adapters | コントローラーが返す |
次の 3 つの習慣が、これらの境界越えをきれいに保ちます。
- プレーンなリクエストとレスポンスのモデル。 Martin ははっきり述べています。「ズルをして entity やデータベースの行を渡したくはない」。presenter が受け取るのは
PlaceOrderResponseであり、Orderではありません。そのため、entity のフィールド名を変えても API には影響しません。リポジトリはよくある例外です。多くのコードベースでは entity を受け取らせますが、それでも依存関係は内を向きます。より厳格なチームは、adapter の内部で永続化用のレコードにマッピングします。 - 外向きの呼び出しごとのインターフェース。 内側のレイヤーが所有し、技術名ではなく use case が必要とするものにちなんだ名前を付けます。
- 1 つの composition root。 Mark Seemann は composition root を、エントリーポイントの近くにある「モジュールを組み合わせる、(できれば)唯一の場所」と定義しています。Martin は
mainを「究極の詳細」と呼びます。それ以外のすべては、コンストラクター経由で依存関係を受け取ります。
Martin の図には、interactor の手前に Use Case Input Port インターフェースもあります。コントローラーも実装も 1 つだけなら、クラスを直接呼び出すのが一般的な簡略化です。
クリーン、ヘキサゴナル、オニオンの違い
| ヘキサゴナル(ports and adapters) | オニオン | クリーン | |
|---|---|---|---|
| 基になる文書 | Alistair Cockburn、2005 年の記事 | Jeffrey Palermo、2008 年のブログ連載 | Robert C. Martin、2012 年のブログ記事、2017 年の書籍 |
| 図 | 六角形の中にアプリケーション、外側に adapter | ドメインモデルを囲む輪 | 4 つの同心円 |
| 内部の構造 | 規定なし | ドメインモデル、ドメインサービス、アプリケーションサービス | Entities、use cases |
| リポジトリの実装 | セカンダリ(駆動される)adapter | インフラストラクチャ、外側の輪 | Interface adapters |
| 中心となるルール | 内側のコードを外に漏らさない。port を通じてやり取りする | すべての結合が中心を向く | ソースコードの依存関係が内側を向く |
3 つとも、ビジネスルールを中心に置き、外縁をそれに依存させます。違いは、内部にどれだけの構造を規定するかです。Cockburn の元の記事は内と外を分けるだけで、Palermo のオニオンアーキテクチャはドメインモデルを囲む輪に名前を付け、クリーンアーキテクチャはさらに presenter とリクエスト、レスポンスのモデルを加えます。ヘキサゴナルアーキテクチャ図では、似た注文の例を ports and adapters の用語で説明しています。
よくある間違い
- entities にフレームワークの型を持ち込む。
Orderに ORM のデコレーターやシリアライズ用の属性を付けると、最も内側のレイヤーが最も外側に結び付きます。マッピングは adapter で行います。 - 何にでも mapper を置く。 1 行を保存するだけのフォームに、同じフィールドのコピーが 6 つ。形が異なるところだけマッピングします。
- 振る舞いのないレイヤー。 getter と setter だけの entity、Martin Fowler が 2003 年に説明した貧血ドメインモデル、
save()を呼ぶだけの use case です。 - あらゆる場所にインターフェース。 インターフェースは境界に置くもので、すべてのクラスに置くものではありません。
- use case の中の HTTP。 ステータスコードは presenter に置きます。
過剰になる場面
Palermo は、オニオンアーキテクチャは「小さな Web サイトには適していない」と書き、Martin の部分的な境界の章は「本格的なアーキテクチャ境界は高価である」という一文で始まります。
| 状況 | より適した選択 |
|---|---|
| ルールの少ない CRUD サービス | 薄いレイヤードアプリ。少ないルールはコントローラーの外に置く |
| プロトタイプや短命なツール | レイヤーなし |
| 決済プロバイダーのような、変わりやすい依存先が 1 つ | その依存先に 1 つのインターフェースと 1 つの adapter |
| 本物のルールと複数のエントリーポイントを持つ長寿命のプロダクト | 4 つのレイヤーすべて |
この章では、より低コストな部分的な境界も説明しています。2 つ目の実装やテストが必要になったときに、1 つ昇格させます。
クリーンアーキテクチャ図
ダイアグラムの内容
- Frameworks and drivers
- Interface adapters
- Use cases
- Entities
- Web framework
- PostgreSQL
- OrderPresenter
- OrderController
- PostgresOrderRepository
- PlaceOrderOutput
- PlaceOrder
- OrderRepository
- Order
- POST /orders
- execute(request)
- Order.place()
- reads view model
- present(response)
- save(order)
- implements
- SQL via pg driver
図の読み方は次のとおりです。
- 4 つの UML パッケージ。最も外側が左です。 右へ進むことは、内側へ進むことを意味します。
- 実線の矢印は呼び出しと import です。 3 本が境界をまたいで entities に向かいます。残りの 3 本は 1 つのレイヤー内にとどまります。
present(response)とsave(order)は、use cases レイヤーが所有する 2 つのインターフェースを呼び出します。 - 白抜き三角の破線矢印は実装を表します。 presenter とリポジトリは、制御が外へ流れる一方で、それらのインターフェースを内向きに指します。
- 点線はドライバー経由の SQL で、adapter から始まります。
- ノートと Markdown ブロックは、
main.tsの置き場所、entity が強制すること、各線のスタイルの意味を示します。
クリーンアーキテクチャ図をわかりやすく描く
- 輪ではなく、1 つの use case を描きます。 実際の機能を、実際のクラス名でたどります。
- 各インターフェースを、それを所有するレイヤーに置きます。
OrderRepositoryは実装の隣ではなく、PlaceOrderの隣に置きます。 - 矢印のスタイルは 2 つにします。 呼び出しは実線、実装は白抜き三角の破線です。UML の実現(realization)と同じです。UML クラス図の例で表記を扱っています。
- 「内側」には、ページ上で 1 つの向きを与えます。 逆向きの矢印は、違反か、ラベルを付けるべき実行時の呼び出しです。
- 矢印には、メソッドと境界をまたぐモデルを書きます。
- 省くもの: DTO のフィールド、フレームワークの内部、DI コンテナ。
このコードレベルの図を取り巻くシステムについては、C4 モデル図の例とソフトウェアアーキテクチャ図の描き方ガイドを参照してください。
Archboard で作る
- Google または GitHub でサインインし、ダイアグラムを作成します。
- UML パックから パッケージ ノードを 4 つ横に並べて追加します。Frameworks and drivers、Interface adapters、Use cases、Entities で、アクセントは青、緑、赤、琥珀色です。
- Frameworks and drivers に、UML パックの コンポーネント を Web framework という名前で、データ パックの Postgres ノードを PostgreSQL という名前で追加します。
- Interface adapters に、コンポーネント ノードを 2 つ(OrderPresenter と OrderController)、クラス を PostgresOrderRepository という名前で追加します。
- Use cases に、クラス を PlaceOrder という名前で、PlaceOrderOutput と OrderRepository という 2 つの インターフェース ノードの間に追加します。
- Entities に、フィールドとメソッドを備えた クラス を Order という名前で追加します。
- 呼び出し用にラベル付きの実線エッジを 6 本、実装用に三角の矢じりが付いた破線エッジを 2 本、PostgresOrderRepository から PostgreSQL への点線エッジを 1 本描きます。
- 基本 パックの Markdown ブロックにルールと線のスタイルを書いて追加し、基本 パックの テキスト ノートを各レイヤー用に追加します。
レイヤーの Mermaid フローチャートは、Mermaid から編集可能なダイアグラムへの変換ツールで最初の下書きになります。Free ではダイアグラムごとにダイアグラムノードを 15 個まで使えるので、そこでは 1 つのダイアグラムにつき 1 つの use case をたどります。
この例で使うダイアグラムノードは 14 個で、Free プランの範囲に収まります。
よくある質問
クリーンアーキテクチャの 4 つのレイヤーとは何ですか? entities(企業全体のビジネスルール)、use cases(アプリケーション固有のビジネスルール)、interface adapters(コントローラー、presenter、ゲートウェイ)、frameworks and drivers(Web フレームワーク、データベース、その他のツール)です。Martin は円を概念図だとしており、4 つ以上必要になることもあります。
依存関係のルールとは何ですか? ソースコードの依存関係は内側にだけ向きます。内側のレイヤーのコードは、外側のレイヤーで宣言されたものを一切名指ししないので、use cases はコントローラーも SQL も Web フレームワークも知りません。
クリーンアーキテクチャとヘキサゴナルアーキテクチャの違いは何ですか? どちらもビジネスルールを中心に置き、adapter をそれに依存させます。ヘキサゴナルアーキテクチャは、port を通じて内側と外側を分けるだけです。クリーンアーキテクチャは、内側もさらに entities と use cases に分け、presenter とリクエスト、レスポンスのモデルに名前を付けます。
クリーンアーキテクチャでは、リポジトリのインターフェースはどこに置きますか? use cases レイヤーの、それを必要とする use case の隣に置きます。SQL を含む実装は interface adapters レイヤーに属し、composition root が起動時にそれを渡します。
クリーンアーキテクチャは小規模なプロジェクトには過剰ですか? 多くの場合はそうです。CRUD サービス、プロトタイプ、短命なツールでは、追加のインターフェースとマッピングのコストが見返りを上回ります。変わると予想される依存先にインターフェースを追加し、本物のビジネスルールが現れたらレイヤーを育てます。