Stage 6: アーキテクチャスタイル — MVC・レイヤード・クリーン
アーキテクチャとは何か
「今まではクラスやコンポーネントの話だった。今日はさらにズームアウトして、システム全体の構造を考える。」
ヒロシはうなずいた。最近、チームで「クリーンアーキテクチャにすべきか」という議論があった。でも何がどう違うのか、よくわからなかった。
「アーキテクチャスタイルは、コードをどう分割し、どう通信させるかの大きな判断枠組み。それぞれに向いている問題と、向いていない問題がある。」
MVC: Railsの基盤
Railsが採用するMVCから始めよう。
# 教科書的なRails MVC
class ArticlesController < ApplicationController # Controller
def show
@article = Article.find(params[:id]) # Model呼び出し
render :show # View呼び出し
end
def create
@article = Article.new(article_params)
if @article.save
redirect_to @article
else
render :new, status: :unprocessable_entity
end
end
end
class Article < ApplicationRecord # Model
validates :title, presence: true, length: { maximum: 100 }
validates :body, presence: true
scope :published, -> { where(status: "published") }
def reading_time_minutes
(body.split.count / 200.0).ceil
end
endMVCの落とし穴: Fat Model, Skinny Controller
「RailsコミュニティはSkinny Controller, Fat Modelを推奨してきた。でも、これが問題を引き起こす。」
# Fat Modelのアンチパターン
class User < ApplicationRecord
# バリデーション ✓
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
# 関連 ✓
has_many :orders
has_many :reviews
# ドメインロジック ✓
def premium?
subscription_tier == "premium"
end
# 外部API呼び出し ✗
def sync_to_crm
CrmApi.create_contact(name: name, email: email)
end
# メール送信 ✗
def send_welcome_email
UserMailer.welcome(self).deliver_later
end
# 複雑なビジネスプロセス ✗
def upgrade_to_premium!
update!(subscription_tier: "premium")
sync_to_crm
send_upgrade_email
grant_premium_perks
notify_sales_team
end
end「このUserモデル、何のテストをすれば良いか分かる?CRMのモックも必要、メールのモックも必要。変更の理由が多すぎる。」
レイヤードアーキテクチャ: 責務の層分け
「MVCの問題を解決するのがレイヤードアーキテクチャ。責務を層に分ける。」
# Presentation Layer: コントローラーは薄く
class UsersController < ApplicationController
def create
result = UserRegistrationService.new(registration_params).call
if result.success?
render json: { user: UserSerializer.new(result.user).as_json }, status: :created
else
render json: { errors: result.errors }, status: :unprocessable_entity
end
end
private
def registration_params
params.require(:user).permit(:name, :email, :password)
end
end
# Application Layer: ユースケースを担う
class UserRegistrationService
Result = Struct.new(:success?, :user, :errors, keyword_init: true)
def initialize(params)
@params = params
end
def call
user = User.new(@params)
unless user.valid?
return Result.new(success?: false, errors: user.errors.full_messages)
end
ActiveRecord::Base.transaction do
user.save!
CrmRepository.create_contact(user)
WelcomeEmailJob.perform_later(user.id)
end
Result.new(success?: true, user: user)
rescue ActiveRecord::RecordInvalid => e
Result.new(success?: false, errors: [e.message])
end
end
# Domain Layer: ドメインロジックのみ
class User < ApplicationRecord
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :name, presence: true, length: { minimum: 2 }
def premium?
subscription_tier == "premium"
end
def display_name
name.presence || email.split("@").first
end
end
# Infrastructure Layer: 外部依存を隠蔽
class CrmRepository
def self.create_contact(user)
CrmApi.new(ENV["CRM_API_KEY"]).contacts.create(
name: user.name,
email: user.email,
source: "webapp"
)
rescue CrmApi::Error => e
Rails.logger.error("CRM sync failed for user #{user.id}: #{e.message}")
false
end
endINFO
Railsでのディレクトリ構成
app/
controllers/ # Presentation Layer
services/ # Application Layer
models/ # Domain Layer
repositories/ # Infrastructure Layer
serializers/ # Presentation Layer (レスポンス整形)
jobs/ # Application Layer (非同期処理)
クリーンアーキテクチャ: 依存関係を内向きに
「レイヤードアーキテクチャの発展形がクリーンアーキテクチャ。最大の特徴は依存関係が常に内向きなこと。」
「つまり、ドメインモデルはRailsを知らない。ActiveRecordを知らない。データベースを知らない。純粋なビジネスロジックだけで成り立つ。」
# ドメインエンティティ: フレームワークに依存しない純粋なRubyクラス
module Domain
class User
attr_reader :id, :name, :email, :subscription_tier
def initialize(id:, name:, email:, subscription_tier: "free")
@id = id
@name = name
@email = email
@subscription_tier = subscription_tier
validate!
end
def premium?
@subscription_tier == "premium"
end
def upgrade_to_premium
# 新しいUserエンティティを返す(イミュータブル)
Domain::User.new(
id: @id,
name: @name,
email: @email,
subscription_tier: "premium"
)
end
private
def validate!
raise ArgumentError, "名前は必須" if @name.blank?
raise ArgumentError, "メールは必須" if @email.blank?
end
end
end
# ユースケース: ドメインロジックとインフラを繋ぐ
module UseCase
class UpgradeUserToPremium
def initialize(user_repository:, payment_gateway:, notification_service:)
@user_repository = user_repository
@payment_gateway = payment_gateway
@notification_service = notification_service
end
def execute(user_id:, payment_token:)
user = @user_repository.find(user_id)
raise "ユーザーが見つかりません" unless user
@payment_gateway.charge(amount: 9_800, token: payment_token)
upgraded_user = user.upgrade_to_premium
@user_repository.save(upgraded_user)
@notification_service.notify_premium_upgrade(upgraded_user)
upgraded_user
end
end
end
# インフラストラクチャ: 具体的な実装
class ActiveRecordUserRepository
def find(id)
record = UserRecord.find_by(id: id)
return nil unless record
to_domain(record)
end
def save(user)
record = UserRecord.find_or_initialize_by(id: user.id)
record.update!(
name: user.name,
email: user.email,
subscription_tier: user.subscription_tier
)
to_domain(record)
end
private
def to_domain(record)
Domain::User.new(
id: record.id,
name: record.name,
email: record.email,
subscription_tier: record.subscription_tier
)
end
endどのアーキテクチャを選ぶか
WARNING
オーバーエンジニアリングに注意
クリーンアーキテクチャはファイル数が増え、最初の実装コストが高くなります。シンプルなCRUDアプリに適用するのは過剰です。「今のコードが変更しにくくなってきた」というタイミングで段階的に導入しましょう。
AWSでのアーキテクチャスタイル
インフラレベルでも同じ考え方が適用できる。
# レイヤードアーキテクチャをAWSで表現
# Presentation Layer
CloudFront → ALB → ECS (Railsアプリ)
# Application Layer
ECS (Railsアプリ) → SQS → ECS Workers
# Domain Layer
ECS内のドメインロジック
# Infrastructure Layer
RDS Aurora PostgreSQL # メインDB
ElastiCache Redis # キャッシュ・セッション
S3 # ファイルストレージ# config/initializers/dependencies.rb
# 依存性の組み立て(Composition Root)
Rails.application.config.after_initialize do
# 本番環境: 本物の実装を使う
if Rails.env.production?
Container = {
user_repository: ActiveRecordUserRepository.new,
payment_gateway: StripePaymentGateway.new(ENV["STRIPE_SECRET_KEY"]),
notification_service: MultiChannelNotificationService.new
}.freeze
else
# テスト/開発環境: フェイク実装
Container = {
user_repository: InMemoryUserRepository.new,
payment_gateway: FakePaymentGateway.new,
notification_service: LogNotificationService.new
}.freeze
end
endStage 6 のまとめ
「アーキテクチャスタイルには正解がない。現在の問題に合った構造を選ぶことが大切。」マイが言った。
| スタイル | 向いている場面 | Railsでの実現 |
|---|---|---|
| 標準MVC | シンプルなCRUD | Railsデフォルト |
| レイヤード | ビジネスロジックが増えてきた | Service Object, Repository |
| クリーン | 複雑なドメイン、長期メンテナンス | Domain層の分離、UseCase |
「一番危険なのは、アーキテクチャの話で議論ばかりして実装が進まないこと。最初はシンプルに始めて、必要になったときに改善する。」
「次はデータベース設計。どんなアーキテクチャを選んでも、データをどう永続化するかは避けられない。」
ヒロシのチームのアーキテクチャ議論が、急に明確になった気がした。