オニオンアーキテクチャ

ドメインロジックを中心に据え、外側の層が内側に依存する同心円状のアーキテクチャ

アーキテクチャDDD
オニオンアーキテクチャ」の技術書を見る →

オニオンアーキテクチャとは

オニオンアーキテクチャ (Onion Architecture) は、Jeffrey Palermo が 2008 年 7 月のブログ記事で提唱したアーキテクチャで、ドメインロジックを中心に据え、外側の層が内側の層に依存する同心円状の構造である。原典が掲げる基本ルールは 1 つだけで、コードは自分より中心側の層には依存してよいが、中心から見て外側にある層には依存できない。結合はすべて中心に向かう、という言い方をする。ここから「DB は中心ではなく外部の一部である」という主張が導かれる。

層構造

オニオンアーキテクチャは同心円状の層で構成される。最も内側にドメインモデル (エンティティ、値オブジェクト) を配置し、その外側にドメインサービス、アプリケーションサービス、最外層にインフラストラクチャ (DB、外部 API) を置く。依存の方向は常に外側から内側に向かい、内側の層は外側の層を知らない。層の数は固定ではなく、原典もアプリケーションコアの層数は場合により変わると述べている。下の 4 層は最小構成に近い一例と考えるとよい。

┌─────────────────────────────────┐
│ Infrastructure                  │
│ ┌─────────────────────────────┐ │
│ │ Application Services        │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ Domain Services         │ │ │
│ │ │ ┌─────────────────────┐ │ │ │
│ │ │ │ Domain Model        │ │ │ │
│ │ │ └─────────────────────┘ │ │ │
│ │ └─────────────────────────┘ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────┘
責務
Domain Modelエンティティ、値オブジェクトUser, Order, Money
Domain ServicesドメインロジックOrderService
Application Servicesユースケースの調整CreateOrderUseCase
Infrastructure外部依存DynamoDB, API, メール送信

依存の方向

層をまたぐ依存は一方向に限られる。ここで見落とされやすいのは、依存できる相手が「1 つ内側の層」に限らない点である。自分より内側であればどの層に依存してもよい。

Infrastructure ──→ Application Services ──→ Domain Services ──→ Domain Model
Infrastructure ───────────────────────────────────────────────→ Domain Model

逆向きの依存は 1 本も許されない。Domain Model は DynamoDB の存在を名前としても知らず、内側で宣言されたインターフェースを通じてデータにアクセスする。この 1 本が入った時点で、同心円は名前だけのものになる。

TypeScript での実装

実装で効いてくるのは、リポジトリのインターフェースを内側 (アプリケーションコア) に置き、その実装だけを最外層に出す点である。原典もリポジトリについて、インターフェースはアプリケーションコアに置くが保存処理そのものは外に出すと述べている。この配置ができていると、ユースケースのテストはインターフェースの差し替えだけで DB 抜きに書ける。

// Domain Model (最内層)
class Order {
  constructor(readonly id: string, private items: OrderItem[], private status: OrderStatus) {}
  get total() { return this.items.reduce((s, i) => s + i.price, 0); }
  cancel() { if (this.status !== 'pending') throw new Error('Cannot cancel'); this.status = 'cancelled'; }
}

// Domain Service: リポジトリのインターフェース (内側で定義)
interface OrderRepository {
  findById(id: string): Promise<Order | null>;
  save(order: Order): Promise<void>;
}

// Application Service: ユースケース
class CancelOrderUseCase {
  constructor(private orderRepo: OrderRepository) {}
  async execute(orderId: string) {
    const order = await this.orderRepo.findById(orderId);
    if (!order) throw new Error('Order not found');
    order.cancel();
    await this.orderRepo.save(order);
  }
}

// Infrastructure (最外層): DynamoDB 実装
class DynamoDBOrderRepository implements OrderRepository {
  async findById(id: string) { /* DynamoDB から取得 */ }
  async save(order: Order) { /* DynamoDB に保存 */ }
}

クリーンアーキテクチャとの違い

狙いはほぼ同じだが、クリーンアーキテクチャがオニオンアーキテクチャの拡張版として作られた、という理解は系譜として正しくない。Robert C. Martin が 2012 年 8 月の記事でクリーンアーキテクチャを示したとき、彼はヘキサゴナルアーキテクチャ、オニオンアーキテクチャ、スクリーミングアーキテクチャ、DCI、BCE の 5 つを「細部は違うがどれも層による関心の分離を目的にしている」として並べ、その共通項を 1 枚の同心円に束ねている。オニオンアーキテクチャはその材料の 1 つという位置にある。

実務で差が出るのは踏み込む深さである。オニオンアーキテクチャの原典は円と依存の向き、そしてリポジトリインターフェースの置き場所までを示す。クリーンアーキテクチャは境界をまたぐときの作法まで規定しており、制御の流れは外から内へ進むのに依存は内向きという食い違いを依存性逆転で解くこと、境界を越えて渡すのは単純なデータ構造に限りエンティティや DB の行をそのまま渡さないことを明示している。層を切ったあとに毎回迷うのはこの受け渡しの部分なので、実装の指針としてはクリーンアーキテクチャ側の記述が使える。

Lambda での現実的な判断

原典は適用範囲についても踏み込んでおり、小規模なサイトには向かず、長く使われる業務アプリケーションや振る舞いが複雑なアプリケーションに向くと明言している。Lambda で判断が分かれるのも同じ線の上にある。

ケース推奨
複雑なドメインロジックオニオンアーキテクチャ
単純な CRUD不要 (直接 DynamoDB を呼ぶ)

導入して最も多い失敗は、層を切ったのに中心が空になることである。エンティティが getter と setter の集まりに退化し、金額の計算や取り消せるかの判定がすべてアプリケーションサービス側へ流れ出すと、中心にドメインモデルを置いた意味は消え、ディレクトリが増えただけの CRUD に戻る。中心へ置くべきものは型ではなく、その型が守る規則である。上のコード例で cancel を Order 自身に持たせているのは、この一点のためである。

理論と実装の両面から学ぶなら関連書籍が参考になる。

この記事は役に立ちましたか?

関連用語

関連する記事