🧅

DDDとオニオンアーキテクチャ入門:肥大化したMVCから脱却するために

に公開

はじめに

Webアプリ開発でよく採用される「MVC」ですが、開発が進むと次のような問題に悩まされることはありませんか?

  • モデル(ActiveRecordなど)が肥大化する
  • ビジネスロジックがコントローラやサービスに分散する
  • 仕様変更のたびに影響範囲が広がる
    こうした問題を解決するアプローチの一つが DDD(ドメイン駆動設計) と、それを支える オニオンアーキテクチャ です。
    本記事では、初心者にもわかるようにこれらの概念を整理し、Goなどでの適用例を紹介します。

※チームや開発者によって、考え方やルールは、異なります。

DDD(ドメイン駆動設計)とは?

DDD(Domain Driven Design) は「ソフトウェアを業務知識(ドメイン)に沿って設計する」アプローチです。
単にコードを整理するだけでなく、現実の業務を反映したモデルを中心に設計すること が特徴です。

キーワード

  • エンティティ (Entity) : ユニークなIDを持ち、ライフサイクルを持つオブジェクト
  • 値オブジェクト (Value Object) : IDを持たず、属性で定義される(例:金額、座標)
  • 集約 (Aggregate) : 複数のエンティティや値オブジェクトをまとめる単位
  • リポジトリ (Repository) : 集約を永続化・取得するインターフェース
  • ドメインサービス : モデルに属さないビジネスルールを表現する

例)ECサイトの「注文(Order)」

  • エンティティ:User, Order
  • 値オブジェクト:Address, Money
  • リポジトリ:OrderRepository

なぜDDDが必要なのか?

従来のMVC構成では、以下のような課題があります。

  • モデルにビジネスロジックを詰め込みすぎる
  • コントローラやサービスクラスに処理が散らばる
  • 仕様変更に弱い

DDDでは「ドメイン(業務ルール)をコードの中心に据える」ことで、変更に強く、業務をコードに落とし込みやすくなります。

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

DDDを支える設計思想の一つが オニオンアーキテクチャ です。
名前の通り「玉ねぎの層構造」を持ち、内側の層ほどドメインに近く、外側の層ほどインフラ依存になります。

構造イメージ

[ UI層 ]        ← Web, API, CLIなど
[ アプリ層 ]    ← ユースケース、サービス
[ ドメイン層 ]  ← エンティティ、値オブジェクト、ドメインサービス
[ インフラ層 ]  ← DB, 外部API, フレームワーク

特徴

  • 依存方向は 外側 → 内側 のみ
  • ドメイン層はフレームワークやDBに依存しない
  • インフラを差し替えてもドメイン層のコードは影響を受けない

コード例(Goの場合)

ここでは簡単に ユーザー登録のユースケース をオニオンアーキテクチャで表現します。

ドメイン層

domain/user.go
package domain

type User struct {
  ID   uint
  Name string
  Email string
}

リポジトリのインターフェース

domain/user_repository.go
package domain

type UserRepository interface {
  FindByEmail(email string) (*User, error)
  Save(user *User) error
}

ユースケース(アプリケーション層)

usecase/user_service.go
package usecase

import "peer-match-ai/domain"

type UserService struct {
  repo domain.UserRepository
}

func NewUserService(r domain.UserRepository) *UserService {
  return &UserService{repo: r}
}

func (s *UserService) Register(name, email string) (*domain.User, error) {
  existing, _ := s.repo.FindByEmail(email)
  if existing != nil {
    return nil, fmt.Errorf("user already exists")
  }
  user := &domain.User{Name: name, Email: email}
  err := s.repo.Save(user)
  return user, err
}

インフラ層(DB実装)

infra/user_repository.go
package infra

import (
  "gorm.io/gorm"
  "peer-match-ai/domain"
)

type GormUserRepository struct {
  db *gorm.DB
}

func (r *GormUserRepository) FindByEmail(email string) (*domain.User, error) {
  var user domain.User
  err := r.db.Where("email = ?", email).First(&user).Error
  return &user, err
}

func (r *GormUserRepository) Save(user *domain.User) error {
  return r.db.Create(user).Error
}

オニオンアーキテクチャのメリット

  • ドメイン中心:業務ロジックが整理され、仕様変更に強い
  • テスト容易性:インフラをモック化できる
  • 疎結合:DB・フレームワークの変更に強い

導入のコツ

  • 最初から完璧なDDD/オニオンを目指さない
  • まずは「重要なドメイン」から切り出す
  • Controllerを薄くし、Usecase層を作ってみる

よくある落とし穴

  • 設計に時間をかけすぎて開発速度が落ちる
  • 不必要な抽象化でコードが読みにくくなる
  • 「DDDを導入すること自体」が目的化する

まとめ

  • DDD は「業務知識をコードに落とし込む設計思想」
  • オニオンアーキテクチャ は「ドメイン中心のアーキテクチャ」
  • 小さな範囲から導入すれば、肥大化したMVCを改善できる
  • 実務でもテスト容易性・変更容易性が高まり、長期的にメリットが大きい
  • 「MVCが限界かも」と思ったら、まずはユースケース層やリポジトリを切り出すところから始めてみましょう。

Discussion