mybook

プロローグ — なぜアーキテクチャパターンを学ぶのか

入社初日のコード

ユウキが初めてプロダクションコードのリポジトリを開いたのは、入社から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種類の責任が混在している。

Loading diagram...

「問題は何ですか?」

「テストが書けない。変更が怖い。新しいメンバーが理解できない——全部だよ」

アヤカは続けた。「たとえば、クーポンの割引ロジックを変えようとする。コントローラーの中にあるから、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 を全部直せるようになる」

Loading diagram...

ユウキはノートを開いた。

「パターンを学ぶと、何が変わりますか?」

アヤカは少し考えてから言った。

「コードの見え方が変わる。問題を見たとき、『これはあのパターンで解ける』って気づけるようになる。設計の話を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クエリがコントローラーにある

「スコアが出ると、改善の達成感がある」アヤカが言った。「リファクタリングは長期的なプロジェクト。進捗を可視化することが大事」

なぜ今パターンを学ぶのか

「パターンを学ぶのに、今が最適なタイミングって、なぜですか?」

ユウキが聞いた。独学でコードを書いてきた自分にとって、パターンは「上級者がやること」に見えていた。

「プロダクションコードを初めて見た直後だから」アヤカが答えた。「問題意識がある状態でパターンを学ぶと、理解が違う。数学の公式を最初に習うより、問題を解こうとして行き詰まってから習う方が定着する」

「あと、今のあなたにはコードを書く基礎がある。そこにパターンという設計の語彙が加わる。語彙を持つと、考えられる設計の幅が広がる」

パターンを知ることで得られるもの:

  1. 語彙:チームメンバーと設計を5分で話せる
  2. 判断力:問題を見てパターンを選べる
  3. 予測力:変更の影響を予測できる
  4. スピード:「前にやった」パターンを素早く実装できる

INFO

パターンは言語を超える。RailsエンジニアがGoのコードを読んだとき、「ここはリポジトリパターンを使っている」と気づけば、コードの意図が素早く理解できる。パターンは設計の共通語。

今日の宿題

「まず今日は、この orders_controller.rb を読み込んで、どんな責任が混在しているかリストアップしてみて」

アヤカがそう言ってユウキに手を振った。

ユウキはエディタに向かいながら思った。コードは読めた。でも何かが違う、と感じていた。その「何か」の名前を、これから学ぶことになる。

30分後、ユウキはSlackにメッセージを送った。

アヤカさん、リストアップしました:
1. パラメーターのバリデーション
2. ユーザーの存在確認
3. 在庫チェック
4. 合計金額の計算
5. クーポン処理
6. 注文の保存
7. メール送信
8. 在庫の更新
9. ポイント付与
10. Slack通知

10個ありました…

アヤカからの返信はすぐに来た。

正解。1つのメソッドに10個の責任。
これを分離する方法を、これから一緒に学ぼう。

ユウキはノートを新しいページに開いた。

パターン学習ロードマップ

10個のパターンの関係を整理した図を、アヤカがホワイトボードに描いた。

Loading diagram...

「まずMVCで基礎を学ぶ。そこにレイヤードで層を足す。リポジトリでDBを抽象化する。CQRSで読み書きを分離する。イベント駆動で非同期を扱う。順番に積み上がる」

「スキャフォールド的に考えるんですね」

「そう。パターンは独立して存在しない。プロダクションのコードは複数のパターンが組み合わさっている」

パターン選択フローチャート

問題を見てどのパターンを使うか、判断のフローをアヤカが教えてくれた。

Loading diagram...

「全部当てはまることも多いけど、まず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パターンを学びます。ユウキは「モデル」「ビュー」「コントローラー」という言葉の本当の意味を理解していきます。