プロローグ — なぜアーキテクチャパターンを学ぶのか
入社初日のコード
ユウキが初めてプロダクションコードのリポジトリを開いたのは、入社から3日目の朝だった。
「まず、この orders_controller.rb を読んでみて」
先輩エンジニアのアヤカがそう言ってPCの画面を指差した。ユウキはファイルを開き、スクロールし始めた。100行、200行、300行——コードは終わらない。
class OrdersController < ApplicationController
def create
# バリデーション
if params[:user_id].blank?
render json: { error: "user_id is required" }, status: 400
return
end
user = User.find(params[:user_id])
if user.nil?
render json: { error: "User not found" }, status: 404
return
end
# 在庫チェック
params[:items].each do |item|
product = Product.find(item[:product_id])
if product.stock < item[:quantity]
render json: { error: "#{product.name}の在庫が不足しています" }, status: 422
return
end
end
# 合計金額計算
total = 0
params[:items].each do |item|
product = Product.find(item[:product_id])
total += product.price * item[:quantity]
end
# クーポン適用
if params[:coupon_code].present?
coupon = Coupon.find_by(code: params[:coupon_code])
if coupon && coupon.valid? && coupon.expires_at > Time.current
if coupon.discount_type == "percentage"
total = total * (1 - coupon.discount_value / 100.0)
else
total = total - coupon.discount_value
end
end
end
# 注文作成
order = Order.new(
user_id: user.id,
total_amount: total,
status: "pending"
)
params[:items].each do |item|
product = Product.find(item[:product_id])
order.order_items.build(
product_id: product.id,
quantity: item[:quantity],
price: product.price
)
end
if order.save
# メール送信
OrderMailer.confirmation(order).deliver_now
# 在庫を減らす
params[:items].each do |item|
product = Product.find(item[:product_id])
product.update!(stock: product.stock - item[:quantity])
end
# ポイント付与
user.update!(points: user.points + (total * 0.01).floor)
# Slack通知
SlackNotifier.notify("#orders", "新しい注文が入りました: ##{order.id}")
render json: order, status: 201
else
render json: { errors: order.errors.full_messages }, status: 422
end
end
endユウキは画面から目を離してアヤカを見た。
「これ……全部コントローラーに書いてあるんですか?」
「そう」アヤカは苦笑いした。「これがファットコントローラーの典型例。2年前に急いで作ったやつ。今、私たちはこれを直しているところ」
なぜコードは太るのか
「どうしてこうなったんでしょう?」
ユウキの疑問はシンプルだった。アヤカはコーヒーを一口飲んでから答えた。
「締め切りがあるから。機能を追加するたびに、一番簡単な場所——コントローラーの中——に書いていった。最初は100行だった。今は800行を超えてる」
このコントローラーには少なくとも6種類の責任が混在している。
「問題は何ですか?」
「テストが書けない。変更が怖い。新しいメンバーが理解できない——全部だよ」
アヤカは続けた。「たとえば、クーポンの割引ロジックを変えようとする。コントローラーの中にあるから、HTTP周辺のコードを全部読みながら探さないといけない。変更したあと、どこに影響するかわからない。テストを書こうにも、コントローラーのテストはHTTPリクエストが必要だから重い」
「じゃあ、最初から分けて書けばよかった?」
「そう。でもそれにはどう分けるかを知っている必要がある。それがアーキテクチャパターン」
パターンとは「先人の知恵」
アヤカはホワイトボードに向かった。
「アーキテクチャパターンっていうのはね、同じ問題を何度も何度も解いてきた先人たちが、『この問題にはこのアプローチが効く』とまとめたもの。料理のレシピみたいなもの」
「レシピ?」
「カレーを作るとき、ゼロから『野菜を切って、炒めて……』って考えないでしょう? レシピがある。アーキテクチャパターンも同じで、設計の問題に対する実績のある解法のこと」
INFO
アーキテクチャパターンとは、ソフトウェア設計で繰り返し現れる問題に対する、再利用可能な解法のこと。車輪の再発明を防ぎ、チーム内で共通の語彙を持てるようにする。
パターンには3つの要素がある。
- 問題(Context): どんな状況でこのパターンが必要か
- 解法(Solution): どう設計するか
- トレードオフ(Consequences): 何を得て、何を失うか
たとえばファットコントローラーという問題には、「レイヤードアーキテクチャ」という解法がある。得るものは保守性とテスト容易性、失うものはファイル数の増加と初期設計コスト。
コードの「においの嗅ぎ方」
「パターンを学ぶ前に、問題を問題として認識できるようになることが大事」アヤカが言った。
「どうやって?」
「コードスメル(Code Smell)を覚える。ファットコントローラーはその一つ」
主なコードスメルをユウキはノートに書き留めた:
| スメル | 症状 | パターン解法 |
|---|---|---|
| ファットコントローラー | コントローラーが300行超え | レイヤードアーキテクチャ |
| 散在したクエリ | 同じActiveRecordが複数箇所 | リポジトリパターン |
| 密結合な後処理 | 注文→メール→ポイント→Slack | イベント駆動 |
| 読み取りの遅さ | includesしても遅い | CQRSパターン |
| 分散トランザクション | 複数サービスで整合性が必要 | サーガパターン |
| カスケード障害 | 外部APIが遅くて全体が止まる | サーキットブレーカー |
# コードスメルの例:同じProductクエリが3箇所に散らばっている
# controllers/orders_controller.rb
product = Product.where(active: true).find(params[:product_id])
# services/cart_service.rb
product = Product.where(active: true).find(product_id)
# jobs/inventory_check_job.rb
products = Product.where(active: true).where("stock < ?", threshold)これはリポジトリパターンで解決できる。クエリを一箇所に集めることで、active: true の条件を後から追加するとき1ファイルだけ直せばよくなる。
実際のプロジェクトでの体験
「昔、私が経験した実話を話す」
アヤカがホワイトボードを消して新しく書き始めた。
「前の会社で、決済処理を変更しようとしたことがあった。変更自体は3行だった。でも、その3行が10箇所のコントローラーにコピーされていて、全部を探して直すのに2日かかった。しかも1箇所見逃して、本番でバグが出た」
「パターンを使っていたら?」
「決済ロジックが PaymentService という1つのクラスにあれば、3行の変更が3行で済む。テストも1箇所。バグが出ても原因がすぐわかる」
これがパターンを学ぶ実利的な理由だ。抽象的な「良いコード」のためではなく、変更のコストを下げるため。
WARNING
パターンは銀の弾丸ではない。「このパターンを使えば全部解決」という思考は危険。問題を理解し、適切なパターンを選ぶ判断力こそが本当のスキル。
この本で学ぶこと
「これから12週間、私がパターンを教える」アヤカが言った。「実際のコードで、実際のAWSで。最後には今の orders_controller.rb を全部直せるようになる」
ユウキはノートを開いた。
「パターンを学ぶと、何が変わりますか?」
アヤカは少し考えてから言った。
「コードの見え方が変わる。問題を見たとき、『これはあのパターンで解ける』って気づけるようになる。設計の話を5分でできるようになる。——それが一番大きいかな」
「あと、採用面接でも話せる。『ファットコントローラーをリポジトリパターンとサービス層に分離しました』って言えると、面接官に伝わる。言語を超えた共通語が使えるようになる」
学習ロードマップ
| 章 | パターン | キーワード | 解決する問題 |
|---|---|---|---|
| 2章 | MVCパターン | モデル・ビュー・コントローラー | 責任の混在 |
| 3章 | レイヤードアーキテクチャ | 関心の分離 | ロジックの散在 |
| 4章 | リポジトリパターン | データアクセスの抽象化 | クエリの散在 |
| 5章 | イベント駆動アーキテクチャ | 非同期・疎結合 | 密結合な後処理 |
| 6章 | パイプ&フィルタ | データ変換の流れ | 複雑な変換ロジック |
| 7章 | CQRSパターン | 読み書きの分離 | 読み取りパフォーマンス |
| 8章 | サーガパターン | 分散トランザクション | マイクロサービスの整合性 |
| 9章 | ストラングラーフィグ | レガシー移行 | 止めずに作り直す |
| 10章 | サーキットブレーカー | 障害の連鎖を断つ | カスケード障害 |
| 11章 | バックプレッシャー | 過負荷の制御 | 高負荷時の崩壊 |
| 12章 | 総まとめ | パターンの組み合わせ | 実践的な設計 |
アーキテクチャパターンとSOLIDの関係
「パターンを学ぶと、SOLID原則という言葉もよく聞くと思うけど、どう違うんですか?」
「SOLIDは設計の原則。パターンは原則を具体化した手法。関係性はこんな感じ」
| SOLID原則 | 対応するパターン | 内容 |
|---|---|---|
| S: 単一責任原則 | レイヤードアーキテクチャ | 1クラス1責任 |
| O: 開放閉鎖原則 | イベント駆動 | 拡張に開き、変更に閉じる |
| L: リスコフ置換原則 | リポジトリパターン | 実装を差し替え可能に |
| I: インターフェース分離 | CQRS | 読み書きを別インターフェースに |
| D: 依存性逆転 | リポジトリパターン | 抽象に依存、具体に依存しない |
「プロローグのコードはSOLIDの全部に違反してた」ユウキが言った。
「そう。コントローラーがメール送信もポイント計算もSlack通知も担当していた(S違反)。処理を追加するたびにコントローラーを変更する必要があった(O違反)。テスト用に差し替えられるものが何もなかった(L・D違反)」
Railsコードの「健康チェック」方法
パターンを学ぶ前に、今のコードの問題を計測する方法を知っておこう。
# bin/code_health_check.rb
# 実行: ruby bin/code_health_check.rb
require 'find'
class CodeHealthCheck
THRESHOLDS = {
controller_lines: 100, # コントローラーの行数上限
model_lines: 200, # モデルの行数上限
method_lines: 20, # メソッドの行数上限
}.freeze
def run
puts "=== コード健康チェック ===\n\n"
check_fat_controllers
check_fat_models
check_scattered_queries
check_mixed_concerns
end
private
def check_fat_controllers
puts "【コントローラーサイズ】"
fat = []
Dir.glob("app/controllers/**/*.rb").each do |path|
lines = File.readlines(path).count
fat << { path: path.sub("app/controllers/", ""), lines: lines } if lines > THRESHOLDS[:controller_lines]
end
if fat.empty?
puts " ✓ 全コントローラーが#{THRESHOLDS[:controller_lines]}行以下"
else
puts " ! ファットコントローラー検出:"
fat.each { |f| puts " #{f[:path]}: #{f[:lines]}行" }
end
puts
end
def check_scattered_queries
puts "【クエリの散在チェック】"
model_names = Dir.glob("app/models/**/*.rb").map { |f|
File.basename(f, ".rb").camelize
}
scattered = {}
model_names.each do |model|
files = `grep -rn "#{model}\.where\|#{model}\.find\|#{model}\.joins" app/ --include="*.rb" -l 2>/dev/null`.split("\n")
files = files.reject { |f| f.include?("models/") || f.include?("spec/") }
scattered[model] = files if files.size > 2
end
if scattered.empty?
puts " ✓ クエリの散在なし"
else
puts " ! クエリが散在しているモデル:"
scattered.each { |m, files| puts " #{m}: #{files.size}箇所" }
end
puts
end
def check_mixed_concerns
puts "【コントローラーの責任混在チェック】"
issues = []
Dir.glob("app/controllers/**/*.rb").each do |path|
content = File.read(path)
controller = File.basename(path, ".rb")
# メール送信がコントローラー内にある
if content.match?(/Mailer\.\w+.*deliver/)
issues << "#{controller}: メール送信がコントローラーにある"
end
# DBアクセスが直接ある(belongs_to等でなく)
if content.match?(/\.where\(|\.find_by\(|\.joins\(/)
issues << "#{controller}: 複雑なDBクエリがコントローラーにある"
end
end
if issues.empty?
puts " ✓ 関心の混在なし"
else
puts " ! 改善が必要:"
issues.each { |i| puts " #{i}" }
end
end
end
CodeHealthCheck.new.runこのスクリプトを実行すると、今どこが問題かが可視化できる。
=== コード健康チェック ===
【コントローラーサイズ】
! ファットコントローラー検出:
orders_controller.rb: 823行
users_controller.rb: 145行
【クエリの散在チェック】
! クエリが散在しているモデル:
Order: 7箇所
Product: 5箇所
【コントローラーの責任混在チェック】
! 改善が必要:
orders_controller.rb: メール送信がコントローラーにある
orders_controller.rb: 複雑なDBクエリがコントローラーにある
「スコアが出ると、改善の達成感がある」アヤカが言った。「リファクタリングは長期的なプロジェクト。進捗を可視化することが大事」
なぜ今パターンを学ぶのか
「パターンを学ぶのに、今が最適なタイミングって、なぜですか?」
ユウキが聞いた。独学でコードを書いてきた自分にとって、パターンは「上級者がやること」に見えていた。
「プロダクションコードを初めて見た直後だから」アヤカが答えた。「問題意識がある状態でパターンを学ぶと、理解が違う。数学の公式を最初に習うより、問題を解こうとして行き詰まってから習う方が定着する」
「あと、今のあなたにはコードを書く基礎がある。そこにパターンという設計の語彙が加わる。語彙を持つと、考えられる設計の幅が広がる」
パターンを知ることで得られるもの:
- 語彙:チームメンバーと設計を5分で話せる
- 判断力:問題を見てパターンを選べる
- 予測力:変更の影響を予測できる
- スピード:「前にやった」パターンを素早く実装できる
INFO
パターンは言語を超える。RailsエンジニアがGoのコードを読んだとき、「ここはリポジトリパターンを使っている」と気づけば、コードの意図が素早く理解できる。パターンは設計の共通語。
今日の宿題
「まず今日は、この orders_controller.rb を読み込んで、どんな責任が混在しているかリストアップしてみて」
アヤカがそう言ってユウキに手を振った。
ユウキはエディタに向かいながら思った。コードは読めた。でも何かが違う、と感じていた。その「何か」の名前を、これから学ぶことになる。
30分後、ユウキはSlackにメッセージを送った。
アヤカさん、リストアップしました:
1. パラメーターのバリデーション
2. ユーザーの存在確認
3. 在庫チェック
4. 合計金額の計算
5. クーポン処理
6. 注文の保存
7. メール送信
8. 在庫の更新
9. ポイント付与
10. Slack通知
10個ありました…
アヤカからの返信はすぐに来た。
正解。1つのメソッドに10個の責任。
これを分離する方法を、これから一緒に学ぼう。
ユウキはノートを新しいページに開いた。
パターン学習ロードマップ
10個のパターンの関係を整理した図を、アヤカがホワイトボードに描いた。
「まずMVCで基礎を学ぶ。そこにレイヤードで層を足す。リポジトリでDBを抽象化する。CQRSで読み書きを分離する。イベント駆動で非同期を扱う。順番に積み上がる」
「スキャフォールド的に考えるんですね」
「そう。パターンは独立して存在しない。プロダクションのコードは複数のパターンが組み合わさっている」
パターン選択フローチャート
問題を見てどのパターンを使うか、判断のフローをアヤカが教えてくれた。
「全部当てはまることも多いけど、まず1つ問題を選んで解決する。完璧な設計より、1つ改善された設計の方が価値がある」
INFO
パターンは「正解の設計」ではなく「問題に対する選択肢」。チームの状況、コードの現状、ビジネスの優先度によって、最適なパターンは変わる。「なぜこのパターンか」を説明できることが設計力の証明。
技術的負債の返済計画の立て方
「コードスメルを発見したとして、全部一気に直せますか?」
「無理ですね…。機能開発も続くし」
「だから計画が必要。技術的負債の返済をプロジェクト管理する方法がある」
# db/seeds/tech_debt_register.rb
# 技術的負債をDBで管理する例
TechDebt.create!([
{
title: "OrdersControllerの分割",
severity: :critical, # critical / high / medium / low
estimated_hours: 8,
affected_files: ["app/controllers/orders_controller.rb"],
pattern_to_apply: "layered_architecture",
business_impact: "デプロイ時に毎回800行のファイルを確認する必要がある",
created_at: 1.month.ago
},
{
title: "Orderモデルのバリデーション整理",
severity: :high,
estimated_hours: 3,
affected_files: ["app/models/order.rb"],
pattern_to_apply: "domain_model",
business_impact: "バリデーションエラーが本番で週3件発生",
created_at: 2.weeks.ago
},
{
title: "クエリのN+1問題",
severity: :high,
estimated_hours: 4,
affected_files: ["app/models/order.rb", "app/controllers/orders_controller.rb"],
pattern_to_apply: "repository_pattern",
business_impact: "注文一覧ページが3秒かかる",
created_at: 1.week.ago
}
])「severity: :critical のものから着手する。ビジネスインパクトが明確なものを優先する。これをプロダクトマネージャーに見せると、リファクタリングのための時間を確保してもらいやすい」
WARNING
「技術的負債をいつか全部直す」という計画は機能しない。スプリントごとに負債返済タスクを正式に組み込み、機能開発と並行して進めることが継続的な改善の鍵。
「わかりました。記録して可視化することが大事なんですね」
「そう。見えない問題は解決されない。これからのパターン学習も同じ。コードの問題を言語化できてこそ、パターンで解決できる」
次章では、Railsの基礎を支えるMVCパターンを学びます。ユウキは「モデル」「ビュー」「コントローラー」という言葉の本当の意味を理解していきます。