本文へスキップ

クリーンアーキテクチャ図:レイヤー、依存関係のルール、実践例

Robert C. Martin のクリーンアーキテクチャを、4 つのレイヤー、依存関係のルール、PlaceOrder の実例で図にします。

· 創業者・開発者

公開日:

横に並んだ 4 つの UML パッケージ:frameworks and drivers、interface adapters、use cases、entities。Web framework が OrderController を呼び、それが PlaceOrder を呼び、PlaceOrder が Order を作ります。OrderPresenter と PostgresOrderRepository は use cases レイヤーのインターフェースを実装します。

クリーンアーキテクチャの図は、多くのエンジニアが目にしています。中心に向かう矢印が付いた、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 リクエストは次の経路をたどります。

  1. Web フレームワークがそれを OrderController にルーティングします。
  2. コントローラーは本文を PlaceOrderRequest にパースします。これは use cases レイヤーが所有するプレーンなオブジェクトです。続いて OrderPresenter を作成し、PlaceOrder.execute() を呼びます。
  3. PlaceOrder は Order.place() にビジネスルールを適用させ、OrderRepository 経由で保存し、PlaceOrderResponse を PlaceOrderOutput に渡します。
  4. OrderPresenter はそのインターフェースを実装し、view model を組み立てます。ステータス 201、Location ヘッダー、JSON の本文です。
  5. コントローラーが 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 つ昇格させます。

クリーンアーキテクチャ図

横に並んだ 4 つの UML パッケージ:frameworks and drivers、interface adapters、use cases、entities。Web framework が OrderController を呼び、それが PlaceOrder を呼び、PlaceOrder が Order を作ります。OrderPresenter と PostgresOrderRepository は use cases レイヤーのインターフェースを実装します。

20 個のコンポーネント · 9 個の接続

Archboard で開く
ダイアグラムの内容
  • 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
1 つのユースケース PlaceOrder を、最も外側を左にして、クリーンアーキテクチャの 4 つのレイヤーにわたってたどります。外向きの矢印はありません。presenter と Postgres リポジトリは use cases レイヤーが所有するインターフェースを実装するので、制御は外へ流れ、依存関係は内を向きます。

図の読み方は次のとおりです。

  • 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 で作る

  1. Google または GitHub でサインインし、ダイアグラムを作成します。
  2. UML パックから パッケージ ノードを 4 つ横に並べて追加します。Frameworks and drivers、Interface adapters、Use cases、Entities で、アクセントは青、緑、赤、琥珀色です。
  3. Frameworks and drivers に、UML パックの コンポーネント を Web framework という名前で、データ パックの Postgres ノードを PostgreSQL という名前で追加します。
  4. Interface adapters に、コンポーネント ノードを 2 つ(OrderPresenter と OrderController)、クラス を PostgresOrderRepository という名前で追加します。
  5. Use cases に、クラス を PlaceOrder という名前で、PlaceOrderOutput と OrderRepository という 2 つの インターフェース ノードの間に追加します。
  6. Entities に、フィールドとメソッドを備えた クラス を Order という名前で追加します。
  7. 呼び出し用にラベル付きの実線エッジを 6 本、実装用に三角の矢じりが付いた破線エッジを 2 本、PostgresOrderRepository から PostgreSQL への点線エッジを 1 本描きます。
  8. 基本 パックの 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 サービス、プロトタイプ、短命なツールでは、追加のインターフェースとマッピングのコストが見返りを上回ります。変わると予想される依存先にインターフェースを追加し、本物のビジネスルールが現れたらレイヤーを育てます。

無料で始める — 1 シート、クレジットカード不要。

関連記事

← ブログに戻る

1分でキャンバスを開く

Google または GitHub でサインイン。1シートは無料で、チームが加わったらアップグレード。