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 | テスタビリティ | ビジネスルールは外部要素なしでテスト可能 |
| 3 | UI 独立性 | 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、あるいは将来の新しいフレームワークにも容易に対応できる柔軟なシステムを構築できるのです。
関連リンク
- The Clean Architecture (Robert C. Martin) - クリーンアーキテクチャの原典
- Ruby 公式ドキュメント - Ruby の基本文法とベストプラクティス
- RSpec 公式ドキュメント - Ruby のテストフレームワーク
- Dependency Inversion Principle - 依存性逆転の原則の詳細
- Active Record Pattern - ActiveRecord パターンの解説
articleRuby で実践するクリーンアーキテクチャ:層分離・依存逆転の実装指針
articleRuby 構文チートシート:ブロック・イテレータ・Enumerable 早見表
articleRuby とは?2025 年版の特徴・強み・最新エコシステムを徹底解説
article「Windows」に「Ruby」をインストールしてみました。その時のやり方や環境変数などいろいろ
articlePlaywright Debug モード活用:テストが落ちる原因を 5 分で特定する手順
articleVue.js でメモリリーク?watch/effect/イベント登録の落とし穴と検知法
articleTailwind CSS のクラスが消える/縮む原因を特定:ツリーシェイクと safelist 完全対策
articlePHP 構文チートシート:配列・クロージャ・型宣言・match を一枚で把握
articleSvelte ストアエラー「store is not a function」を解決:writable/derived の落とし穴
articleNext.js の 観測可能性入門:OpenTelemetry/Sentry/Vercel Analytics 連携
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来