T-CREATOR

Ruby で実践するクリーンアーキテクチャ:層分離・依存逆転の実装指針

Ruby で実践するクリーンアーキテクチャ:層分離・依存逆転の実装指針

Ruby アプリケーションの保守性と拡張性を高めたいと考えていませんか。クリーンアーキテクチャは、ビジネスロジックとフレームワークを分離し、テスタブルで変更に強いシステムを構築するための設計思想です。本記事では、Ruby でクリーンアーキテクチャを実践するための層分離と依存逆転の具体的な実装方法を、初心者の方にもわかりやすく解説していきます。

背景

クリーンアーキテクチャは、Robert C. Martin(通称 Uncle Bob)が提唱した設計思想で、ソフトウェアの関心事を円状のレイヤーに分離することで、ビジネスロジックを外部依存から独立させることを目指します。

Ruby コミュニティでは、Ruby on Rails のようなフルスタックフレームワークが主流となっていますが、ビジネスロジックがフレームワークに密結合すると、テストが困難になったり、別のフレームワークへの移行が難しくなるという課題が生じます。

以下の図は、クリーンアーキテクチャにおける各層の関係性と依存の方向を示しています。

mermaidflowchart TD
  subgraph outer["外側の層"]
    fw["フレームワーク<br/>DB・Web・デバイス"]
  end

  subgraph adapter["アダプター層"]
    ctrl["コントローラー<br/>プレゼンター"]
    repo["リポジトリ実装"]
  end

  subgraph usecase["ユースケース層"]
    uc["ユースケース<br/>アプリケーション<br/>ロジック"]
  end

  subgraph entity["エンティティ層"]
    ent["エンティティ<br/>ビジネスルール"]
  end

  fw -->|依存| adapter
  adapter -->|依存| usecase
  usecase -->|依存| entity

  style entity fill:#e1f5ff
  style usecase fill:#fff4e1
  style adapter fill:#ffe1e1
  style outer fill:#f0f0f0

この図から、依存関係が外側から内側へ一方向に向かっていることがわかります。内側の層(エンティティ、ユースケース)は外側の層(フレームワーク、DB)のことを知りません。

クリーンアーキテクチャの基本原則

クリーンアーキテクチャには、以下の 4 つの主要な原則があります。

#原則説明
1フレームワーク独立性ビジネスロジックはフレームワークに依存しない
2テスタビリティビジネスルールは外部要素なしでテスト可能
3UI 独立性UI はビジネスロジックを知らずに変更できる
4データベース独立性ビジネスルールは特定の DB に依存しない

これらの原則を守ることで、変更に強く、長期的に保守しやすいシステムを構築できるのです。

課題

従来の Ruby アプリケーション、特に Rails アプリケーションでは、以下のような課題が発生しやすい傾向にあります。

ActiveRecord への強い依存

Rails の ActiveRecord モデルは、データベースアクセスとビジネスロジックが混在しがちです。この結果、テストの際に必ずデータベースが必要となり、テストの実行速度が低下してしまいます。

Fat Controller の問題

コントローラーにビジネスロジックを書いてしまうと、同じロジックを他の場所で再利用できず、コードの重複が発生します。また、テストも複雑化していくでしょう。

技術的負債の蓄積

フレームワークに密結合したコードは、フレームワークのバージョンアップや別の技術スタックへの移行時に大きな障壁となってしまうのです。

以下の図は、従来の Rails アプリケーションにおける密結合の問題を示しています。

mermaidflowchart LR
  ctrl["Controller"] -->|直接呼び出し| model["ActiveRecord<br/>Model"]
  model -->|SQL| db[("MySQL")]
  view["View"] -->|直接参照| model

  subgraph problem["問題点"]
    p1["ビジネスロジックが<br/>DBに依存"]
    p2["テストにDB必須"]
    p3["再利用困難"]
  end

  model -.->|引き起こす| problem

  style problem fill:#ffe1e1

この図から、Model 層がデータベースと密結合し、Controller や View から直接参照されることで、様々な問題が発生していることが理解できます。

依存関係の複雑化

層の分離が不十分な場合、依存関係が双方向になったり、循環参照が発生したりします。これにより、コードの理解と変更が困難になるでしょう。

解決策

クリーンアーキテクチャでは、以下の 4 つの層に責務を分離し、依存の方向を制御することで、上記の課題を解決します。

4 層の構成と責務

#層名責務依存先
1エンティティ層ビジネスルール・ドメインロジックなし
2ユースケース層アプリケーション固有のロジックエンティティのみ
3アダプター層データ変換・外部とのインターフェースユースケース
4フレームワーク層外部ライブラリ・DB・Webアダプター

依存関係は必ず内側(エンティティ)に向かい、外側の層は内側の層に依存しますが、内側は外側を知りません。

依存性逆転の原則(DIP)

依存性逆転の原則は、クリーンアーキテクチャの核心です。具体的な実装ではなく、抽象(インターフェース)に依存することで、外側の層の変更が内側の層に影響しないようにします。

以下の図は、依存性逆転の原則の仕組みを示しています。

mermaidflowchart TD
  uc["UseCase<br/>ユースケース層"]
  iface["Repository<br/>Interface<br/>(抽象)"]
  impl["Repository<br/>Implementation<br/>(具象)"]
  db[("Database")]

  uc -->|依存| iface
  impl -->|実装| iface
  impl -->|アクセス| db

  note1["依存の方向が<br/>逆転している"]

  impl -.->|通常はこちら| uc
  note1 -.-> impl

  style iface fill:#e1f5ff
  style note1 fill:#fff4e1

この図のポイントは、UseCase が具体的な Repository 実装ではなく、Repository Interface という抽象に依存している点です。実装クラスがインターフェースを実装することで、依存の方向が逆転しています。

Ruby におけるインターフェースの実現

Ruby には厳密なインターフェースの概念がありませんが、ダックタイピングやモジュールを使って同様の効果を実現できるのです。

具体例

ここからは、ユーザー登録機能を例に、Ruby でクリーンアーキテクチャを実装する具体的な手順を見ていきましょう。

ディレクトリ構成

まず、プロジェクトのディレクトリ構成を以下のように設計します。

bashlib/
├── entities/           # エンティティ層
│   └── user.rb
├── use_cases/          # ユースケース層
│   ├── create_user.rb
│   └── interfaces/
│       └── user_repository.rb
├── adapters/           # アダプター層
│   ├── repositories/
│   │   └── active_record_user_repository.rb
│   └── controllers/
│       └── users_controller.rb
└── frameworks/         # フレームワーク層
    └── models/
        └── user_model.rb

この構成により、各層の責務が明確になり、コードの配置場所で層を判断できます。

エンティティ層の実装

エンティティ層は、ビジネスルールを表現する最も内側の層です。外部の技術的詳細に一切依存しません。

以下のコードは、User エンティティの基本的な実装を示しています。

ruby# lib/entities/user.rb

# Userエンティティ: ビジネスルールを表現
# ActiveRecordなどのフレームワークには一切依存しない
class User
  # 属性の定義
  attr_reader :id, :name, :email, :created_at

  # 初期化メソッド
  def initialize(id:, name:, email:, created_at: Time.now)
    @id = id
    @name = name
    @email = email
    @created_at = created_at

    # バリデーションを実行
    validate!
  end

次に、ビジネスルールとしてのバリデーションロジックを実装します。

ruby  # ビジネスルールとしてのバリデーション
  def validate!
    # 名前の必須チェック
    raise ArgumentError, '名前は必須です' if name.nil? || name.empty?

    # メールアドレスの必須チェック
    raise ArgumentError, 'メールアドレスは必須です' if email.nil? || email.empty?

    # メールアドレスの形式チェック
    unless email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
      raise ArgumentError, 'メールアドレスの形式が不正です'
    end
  end

さらに、ドメインロジックとしてユーザー名の表示形式を定義します。

ruby  # ドメインロジック: ユーザー名の表示形式
  def display_name
    # 敬称をつけた表示名を返す
    "#{name}様"
  end

  # ドメインロジック: メールアドレスのドメイン部分を取得
  def email_domain
    # @以降の文字列を返す
    email.split('@').last
  end
end

このエンティティクラスは、ActiveRecord やその他のフレームワークに依存せず、純粋な Ruby オブジェクトとして実装されています。そのため、高速にテストでき、任意の環境で再利用可能です。

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

ユースケース層では、データの永続化方法を知る必要がありません。そこで、リポジトリのインターフェースを定義します。

以下のコードは、UserRepository インターフェースの実装例です。

ruby# lib/use_cases/interfaces/user_repository.rb

# UserRepositoryインターフェース
# データの永続化方法を抽象化する
module UserRepository
  # ユーザーを保存する抽象メソッド
  # @param user [User] 保存するユーザーエンティティ
  # @return [User] 保存されたユーザーエンティティ
  def save(user)
    raise NotImplementedError, 'save メソッドを実装してください'
  end

次に、ユーザー検索のためのメソッドを定義します。

ruby  # IDでユーザーを検索する抽象メソッド
  # @param id [Integer] ユーザーID
  # @return [User, nil] 見つかったユーザーエンティティ、または nil
  def find_by_id(id)
    raise NotImplementedError, 'find_by_id メソッドを実装してください'
  end

  # メールアドレスでユーザーを検索する抽象メソッド
  # @param email [String] メールアドレス
  # @return [User, nil] 見つかったユーザーエンティティ、または nil
  def find_by_email(email)
    raise NotImplementedError, 'find_by_email メソッドを実装してください'
  end
end

このインターフェースにより、ユースケースは具体的なデータベース実装を知らずに、データの永続化を要求できます。

ユースケース層の実装

ユースケース層では、アプリケーション固有のビジネスロジックを実装します。リポジトリインターフェースに依存し、エンティティを操作します。

以下のコードは、ユーザー作成ユースケースのクラス定義とコンストラクタです。

ruby# lib/use_cases/create_user.rb
require_relative '../entities/user'
require_relative 'interfaces/user_repository'

# CreateUserユースケース: ユーザー登録のビジネスロジック
class CreateUser
  # コンストラクタ
  # @param user_repository [UserRepository] リポジトリの実装
  def initialize(user_repository)
    # リポジトリをインジェクション(依存性注入)
    @user_repository = user_repository
  end

次に、ユースケースの実行メソッドを実装します。

ruby  # ユースケースの実行
  # @param name [String] ユーザー名
  # @param email [String] メールアドレス
  # @return [Hash] 結果のハッシュ(success: 成功フラグ、user: ユーザー、error: エラーメッセージ)
  def execute(name:, email:)
    # メールアドレスの重複チェック
    existing_user = @user_repository.find_by_email(email)
    if existing_user
      return {
        success: false,
        error: 'このメールアドレスは既に登録されています'
      }
    end

ユーザーエンティティを生成し、バリデーションを実行します。

ruby    # 新しいUserエンティティを生成
    # バリデーションはエンティティ内で実行される
    begin
      user = User.new(
        id: nil, # IDは保存時にDBが生成
        name: name,
        email: email
      )
    rescue ArgumentError => e
      # バリデーションエラーをキャッチ
      return {
        success: false,
        error: e.message
      }
    end

最後に、リポジトリを使ってユーザーを永続化します。

ruby    # リポジトリを使って永続化
    saved_user = @user_repository.save(user)

    # 成功結果を返す
    {
      success: true,
      user: saved_user
    }
  rescue => e
    # 予期しないエラーをキャッチ
    {
      success: false,
      error: "ユーザーの作成に失敗しました: #{e.message}"
    }
  end
end

このユースケースは、データベースの具体的な実装を知らず、リポジトリインターフェースを通じてデータを操作しています。そのため、モックオブジェクトを使った高速なユニットテストが可能です。

アダプター層:リポジトリの実装

アダプター層では、インターフェースを実装し、実際のデータベースアクセスを行います。ここでは、ActiveRecord を使った実装例を示します。

以下のコードは、ActiveRecord を使った UserRepository の実装です。

ruby# lib/adapters/repositories/active_record_user_repository.rb
require_relative '../../use_cases/interfaces/user_repository'
require_relative '../../frameworks/models/user_model'
require_relative '../../entities/user'

# ActiveRecordを使ったUserRepositoryの実装
class ActiveRecordUserRepository
  # UserRepositoryインターフェースをinclude
  include UserRepository

  # ユーザーを保存する実装
  # @param user [User] 保存するユーザーエンティティ
  # @return [User] 保存されたユーザーエンティティ
  def save(user)
    # ActiveRecordモデルを使ってDB保存
    user_model = UserModel.create!(
      name: user.name,
      email: user.email
    )

    # DB保存後のデータでエンティティを再生成
    entity_from_model(user_model)
  end

次に、検索メソッドの実装を行います。

ruby  # IDでユーザーを検索する実装
  # @param id [Integer] ユーザーID
  # @return [User, nil] 見つかったユーザーエンティティ、または nil
  def find_by_id(id)
    # ActiveRecordで検索
    user_model = UserModel.find_by(id: id)
    return nil unless user_model

    # モデルからエンティティに変換
    entity_from_model(user_model)
  end

  # メールアドレスでユーザーを検索する実装
  # @param email [String] メールアドレス
  # @return [User, nil] 見つかったユーザーエンティティ、または nil
  def find_by_email(email)
    # ActiveRecordで検索
    user_model = UserModel.find_by(email: email)
    return nil unless user_model

    # モデルからエンティティに変換
    entity_from_model(user_model)
  end

最後に、ActiveRecord モデルをエンティティに変換するプライベートメソッドを実装します。

ruby  private

  # ActiveRecordモデルからエンティティへの変換
  # @param model [UserModel] ActiveRecordモデル
  # @return [User] ユーザーエンティティ
  def entity_from_model(model)
    User.new(
      id: model.id,
      name: model.name,
      email: model.email,
      created_at: model.created_at
    )
  end
end

このリポジトリ実装は、ActiveRecord の詳細を隠蔽し、ユースケース層にエンティティという純粋な Ruby オブジェクトを渡しています。

フレームワーク層:ActiveRecord モデル

フレームワーク層では、ORM や Web フレームワークなどの外部ライブラリを使用します。

以下のコードは、ActiveRecord モデルの最小限の実装です。

ruby# lib/frameworks/models/user_model.rb
require 'active_record'

# ActiveRecordモデル: データベーステーブルとのマッピング
# ビジネスロジックは含まず、純粋なデータアクセス層
class UserModel < ActiveRecord::Base
  # テーブル名を指定
  self.table_name = 'users'

  # ActiveRecordの基本的なバリデーション(DB制約に対応)
  validates :name, presence: true
  validates :email, presence: true, uniqueness: true
end

このモデルは、データベーステーブルとのマッピングのみを担当し、ビジネスロジックは含まれていません。ビジネスルールはすべてエンティティ層に集約されています。

アダプター層:コントローラー

Web フレームワーク(例:Sinatra, Rails)のコントローラーも、アダプター層に配置します。

以下のコードは、Sinatra を使ったコントローラーの実装例です。

ruby# lib/adapters/controllers/users_controller.rb
require 'sinatra/base'
require_relative '../../use_cases/create_user'
require_relative '../repositories/active_record_user_repository'

# UsersController: HTTPリクエストを受け取り、ユースケースを実行
class UsersController < Sinatra::Base
  # POST /users - ユーザー作成エンドポイント
  post '/users' do
    # リクエストボディからパラメータを取得
    request_body = JSON.parse(request.body.read)
    name = request_body['name']
    email = request_body['email']

次に、リポジトリとユースケースをインスタンス化します。

ruby    # リポジトリの実装をインスタンス化
    user_repository = ActiveRecordUserRepository.new

    # ユースケースをインスタンス化(依存性注入)
    create_user = CreateUser.new(user_repository)

ユースケースを実行し、結果に応じて HTTP レスポンスを返します。

ruby    # ユースケースを実行
    result = create_user.execute(name: name, email: email)

    # 結果に応じてレスポンスを返す
    if result[:success]
      # 成功時: 201 Created
      status 201
      content_type :json
      {
        id: result[:user].id,
        name: result[:user].name,
        email: result[:user].email,
        display_name: result[:user].display_name
      }.to_json
    else
      # 失敗時: 400 Bad Request
      status 400
      content_type :json
      { error: result[:error] }.to_json
    end
  end
end

このコントローラーは、HTTP リクエストの詳細をユースケースから隠蔽し、ユースケースの実行結果を HTTP レスポンスに変換する役割を担っています。

依存性注入の流れ

以下の図は、各層がどのように連携し、依存性が注入されるかを示しています。

mermaidsequenceDiagram
  participant C as Controller<br/>アダプター層
  participant R as Repository実装<br/>アダプター層
  participant UC as UseCase<br/>ユースケース層
  participant E as Entity<br/>エンティティ層
  participant DB as Database

  C->>R: new()
  C->>UC: new(repository)
  Note over UC: リポジトリを<br/>注入

  C->>UC: execute(name, email)
  UC->>E: new(name, email)
  E->>E: validate!()
  E-->>UC: userエンティティ

  UC->>R: save(user)
  R->>DB: INSERT
  DB-->>R: 保存完了
  R-->>UC: 保存されたuser
  UC-->>C: 成功結果

  C-->>C: JSONレスポンス生成

この図から、外側の層(Controller)が内側の層(UseCase)に依存性を注入し、UseCase はインターフェースを通じて Repository を利用していることがわかります。

図で理解できる要点:

  • Controller がリポジトリ実装をインスタンス化し、UseCase に注入している
  • UseCase はエンティティを生成・バリデーションし、リポジトリで永続化している
  • 各層が明確に分離され、依存関係が一方向になっている

テストの実装

クリーンアーキテクチャの大きな利点は、テスタビリティの高さです。以下は、モックリポジトリを使ったユースケースのテスト例を示します。

まず、テスト用のモックリポジトリを定義します。

ruby# spec/mocks/mock_user_repository.rb
require_relative '../../lib/use_cases/interfaces/user_repository'

# テスト用のモックリポジトリ
class MockUserRepository
  include UserRepository

  def initialize
    # メモリ上にユーザーを保存
    @users = {}
    @next_id = 1
  end

モックリポジトリの save メソッドを実装します。

ruby  # ユーザーを保存(メモリ上)
  def save(user)
    # IDを生成
    id = @next_id
    @next_id += 1

    # メモリに保存
    saved_user = User.new(
      id: id,
      name: user.name,
      email: user.email,
      created_at: user.created_at
    )
    @users[id] = saved_user
    saved_user
  end

検索メソッドも実装します。

ruby  # IDで検索
  def find_by_id(id)
    @users[id]
  end

  # メールアドレスで検索
  def find_by_email(email)
    @users.values.find { |user| user.email == email }
  end
end

次に、RSpec を使ったユースケースのテストを記述します。

ruby# spec/use_cases/create_user_spec.rb
require 'rspec'
require_relative '../../lib/use_cases/create_user'
require_relative '../mocks/mock_user_repository'

# CreateUserユースケースのテスト
RSpec.describe CreateUser do
  let(:repository) { MockUserRepository.new }
  let(:use_case) { CreateUser.new(repository) }

  # 正常系: ユーザーが正常に作成される
  describe '正常系' do
    it 'ユーザーが正常に作成される' do
      result = use_case.execute(
        name: '山田太郎',
        email: 'taro@example.com'
      )

      expect(result[:success]).to be true
      expect(result[:user].name).to eq '山田太郎'
      expect(result[:user].email).to eq 'taro@example.com'
      expect(result[:user].id).not_to be_nil
    end
  end

異常系のテストも記述します。

ruby  # 異常系: メールアドレスが重複している場合
  describe '異常系: メールアドレス重複' do
    it 'エラーメッセージが返される' do
      # 事前に同じメールアドレスで登録
      use_case.execute(
        name: '山田太郎',
        email: 'duplicate@example.com'
      )

      # 同じメールアドレスで再度登録
      result = use_case.execute(
        name: '鈴木花子',
        email: 'duplicate@example.com'
      )

      expect(result[:success]).to be false
      expect(result[:error]).to eq 'このメールアドレスは既に登録されています'
    end
  end

バリデーションエラーのテストも追加します。

ruby  # 異常系: バリデーションエラー
  describe '異常系: バリデーション' do
    it '名前が空の場合エラーになる' do
      result = use_case.execute(
        name: '',
        email: 'test@example.com'
      )

      expect(result[:success]).to be false
      expect(result[:error]).to eq '名前は必須です'
    end

    it 'メールアドレスの形式が不正な場合エラーになる' do
      result = use_case.execute(
        name: '山田太郎',
        email: 'invalid-email'
      )

      expect(result[:success]).to be false
      expect(result[:error]).to eq 'メールアドレスの形式が不正です'
    end
  end
end

このテストは、データベースを使用せずにメモリ上で実行されるため、非常に高速です。また、外部依存がないため、CI パイプラインでも安定して実行できます。

まとめ

本記事では、Ruby でクリーンアーキテクチャを実践するための具体的な実装方法を解説しました。

クリーンアーキテクチャの核心は、以下の 3 つのポイントに集約されます。

第一に、層の明確な分離です。エンティティ、ユースケース、アダプター、フレームワークという 4 つの層に責務を分離することで、各層が単一の責任を持ち、変更の影響範囲を局所化できます。

第二に、依存性逆転の原則(DIP)の適用です。インターフェースを定義し、具体的な実装ではなく抽象に依存することで、外側の層の変更が内側の層に影響しないようにできるのです。

第三に、テスタビリティの向上です。モックやスタブを使って、データベースや外部サービスなしで高速なユニットテストが実行できます。これにより、開発サイクルが短縮され、リファクタリングも安心して行えるでしょう。

Ruby のような動的型付け言語では、厳密なインターフェースがない代わりに、ダックタイピングとモジュールを活用することで、柔軟かつ実用的なクリーンアーキテクチャを実現できます。

最初は層の分離や依存性の管理に慣れが必要かもしれませんが、中長期的にはコードの保守性と拡張性が大きく向上します。小さなモジュールから始めて、徐々にクリーンアーキテクチャの考え方を取り入れていくことをお勧めします。

ビジネスロジックをフレームワークから独立させることで、Ruby on Rails だけでなく、Sinatra、Hanami、あるいは将来の新しいフレームワークにも容易に対応できる柔軟なシステムを構築できるのです。

関連リンク