レイヤードアーキテクチャからオニオンアーキテクチャへの道のり

に公開

はじめに

社内の勉強会にむけてオニオンアーキテクチャを実践的に学ぶためのハンズオンを用意しました。
オニオンアーキテクチャの記事としては様々なものがありますが、実際のところどういうパッケージ構成にするとオニオンアーキテクチャといえるのか、具体的な構成例を示した記事が少ない印象です。本記事では、Go言語を使用した実際のパッケージ構成を例に、レイヤードアーキテクチャからオニオンアーキテクチャへの段階的移行を経てオニオンアーキテクチャの具体的な実装方法を説明します。

以下は単純なユーザー情報を返すAPIを実装したシンプルなプログラムです。layered-architecture、hexagonal-architecture、onion-architectureの各構成で作成しています。本記事はこれを利用して説明していくためcloneして利用してください。
https://github.com/akki-F/architecture-by-go

そもそも

誤解を恐れずに言うと、アーキテクチャの構成は以下のように成り立ちます。そのため、大前提でレイヤードアーキテクチャとは何か、依存性逆転とは何か、DDDとは何かをそれぞれ理解する必要があります。

  1. レイヤードアーキテクチャ - 基本的な層分離
  2. ヘキサゴナルアーキテクチャ - レイヤードアーキテクチャ + 依存性逆転
  3. オニオンアーキテクチャ - レイヤードアーキテクチャ + 依存性逆転 + DDD

参考

構成や各説明にあたり以下を参考にしています。気が向いたら読んでみてください。
https://www.shoeisha.co.jp/book/detail/9784798126708

オニオンアーキテクチャへの道のり

オニオンアーキテクチャの基盤:レイヤードアーキテクチャ

オニオンアーキテクチャは、レイヤードアーキテクチャを基盤としたアーキテクチャパターンです。まずはレイヤードアーキテクチャを構成しましょう。

https://github.com/akki-F/architecture-by-go/tree/main/layered-architecture

層の構成

以下のような4層構造を例としています。参考記事のレイヤードアーキテクチャの例を利用しています。
レイヤードアーキテクチャの本質は層の役割を明確にさせることです。そのため、よくあるMVC+Sのような構成も典型的なレイヤードアーキテクチャです。

  • ドメイン層(Domain): ビジネスロジックとドメインルールを表現
  • アプリケーション層(Application): ユースケースの実装とアプリケーションサービス
  • インフラ層(Infrastructure): データベース、外部API、フレームワークとの連携
  • プレゼンテーション層(Presentation): ユーザーインターフェースとHTTPリクエスト/レスポンスの処理

How(どのように実装するのか)- 依存性逆転

構成したレイヤードアーキテクチャの依存性を逆転させましょう。そのために、まずはSOLID原則のIとDを正しく理解しましょう。混同して依存性の逆転が実現できていると錯覚してしまうためです。

SOLID原則のI(Interface Segregation Principle:interface分離の原則)

Iは「クライアントは自分が使用しないinterfaceに依存してはならない」という原則です。

まずは、interface分離の原則をパッケージ構成で理解してみましょう。

Step 1-1: infrastructure/repository.goを用意する。

package infrastructure

import "go-architecture/layered-architecture/domain"

type UserRepository interface {
	FindByID(id int) (*domain.User, error)
}

Before(interface分離前)

infrastructure/
└── user_db.go

After(interface分離後)

infrastructure/
├── user_db.go
└── repository.go

次に用意したinterfaceを利用するためにapplication層のuser_service.goを修正します。

Step 1-2: application層のuser_service.goを修正する

// Before: 具象クラスに直接依存
type UserService struct {
    userRepository *infrastructure.UserDb  // 具象クラスに依存
}

// After: interfaceに依存
type UserService struct {
    userRepository infrastructure.UserRepository  // interfaceに依存
}

Before(interface分離前)

After(interface分離後)

interface分離の原則に則った分離ができましたね。ただし、この段階ではまだ具象クラス(user_db.go)とinterface(repository.go)が同じ層にあります。依存性逆転は実現できていません。

SOLID原則のD(Dependency Inversion Principle:依存性逆転の原則)

Dは「上位モジュールは下位モジュールに依存してはならない。両方とも抽象に依存すべきである」という原則です。

依存性逆転の原則を実現するために、interfaceを上位層に移動させましょう。

Step 2-1: interfaceをアプリケーション層に移動

application/
├── repository.go      # interfaceを上位層に移動
└── user_service.go

Step 2-2: application層のuser_service.goを修正

// Before: infrastructure層のinterfaceに依存
import "go-architecture/infrastructure"
type UserService struct {
    userRepository infrastructure.UserRepository  // infrastructure層に依存
}

// After: application層のinterfaceに依存
import "go-architecture/application"
type UserService struct {
    userRepository UserRepository  // 同じ層のinterfaceに依存
}

Before(interface移動前)

After(interface移動後)

これで依存性逆転が実現でき、interfaceの配置先がいかに重要であるかが直感的に理解できたと思います。
完成例は以下となります。
https://github.com/akki-F/architecture-by-go/tree/main/hexagonal-architecture

Why(なぜオニオンアーキテクチャが必要なのか)- DDD

オニオンアーキテクチャの最大の特徴は、ドメイン駆動設計(DDD)の概念を活用することです。

ドメイン駆動設計の本質

ドメイン駆動設計とは、ドメイン層を最上位層にとらえて考える設計手法です。従来のレイヤードアーキテクチャでは、プレゼンテーション層やアプリケーション層が上位層として扱われがちですが、ドメイン駆動設計ではビジネスロジックを表現するドメイン層こそが最も重要であると考えます。

Step 3-1: interfaceをドメイン層に移動

domain/
├── repository.go      # interfaceを上位層に移動
└── user.go

Step 3-2: application層のuser_service.goを修正

// Before: application層のinterfaceに依存
import "go-architecture/application"
type UserService struct {
    userRepository UserRepository  // application層に依存
}

// After: domain層のinterfaceに依存
import "go-architecture/domain"
type UserService struct {
    userRepository domain.UserRepository  // domain層のinterfaceに依存
}

Before(interface移動前)

After(interface移動後)

ドメイン層を中心とした構成に変わり、依存性の逆転もされています。

Step 3-3: ドメイン層にentitiesとvalueobjectsを用意する。

Before(entity/valueobject分離前)

domain/
├── repository.go         #  interface
└── user.go               # ユーザーエンティティ

After(entity/valueobject分離後)

domain/
├── repository.go         # interface
├── entities/
│   ├── user.go          # ユーザーエンティティ
│   └── errors.go        # ドメインエラー
└── valueobjects/
    └── email.go         # メールアドレスバリューオブジェクト

DDDの概念を活用した設計

1. Entity(エンティティ)
エンティティは、一意な識別子(ID)を持つオブジェクトです。同一性で比較され、ライフサイクルを持ちます。例としてuser.goを見てみましょう。
https://github.com/akki-F/architecture-by-go/blob/main/onion-architecture/domain/entities/user.go

2. Value Object(バリューオブジェクト)
バリューオブジェクトは、不変で値によって比較されるオブジェクトです。例としてemail.goを見てみましょう。
https://github.com/akki-F/architecture-by-go/blob/main/onion-architecture/domain/valueobjects/email.go

3. Domain Errors(ドメインエラー)
ドメイン層では、ビジネスルールの違反を表現するエラーも定義します。例としてerrors.goを見てみましょう。
https://github.com/akki-F/architecture-by-go/blob/main/onion-architecture/domain/entities/errors.go

このように、ドメイン層ではEntityDomain Errorsを中心とした構成になります。Value Objectsは、メールアドレスのように複雑なバリデーションロジックが必要な場合や、不変性が重要な値の場合は必要に応じて作成します。

以上のように、ドメイン層を中心とした設計により、ビジネスロジックの配置場所とその構成が明確になりました。
完成例は以下となります。
https://github.com/akki-F/architecture-by-go/tree/main/onion-architecture

まとめ

プロジェクトの規模、チームの経験、ビジネス要件に応じて適切なアーキテクチャを選択することが重要です。小規模なプロジェクトではレイヤードアーキテクチャで十分ですが、大規模で複雑なドメインを持つシステムでは、オニオンアーキテクチャの恩恵を大きく受けることができます。

本記事で紹介した段階的な移行プロセスを参考に、プロジェクトに適したアーキテクチャを選択していただければと思います。

Discussion