mybook

エピローグ — パターンを組み合わせる力

12週間後

「アヤカさん、できました」

ユウキが自信に満ちた顔でPRのリンクを送った。プロローグで見た800行のファットコントローラー、orders_controller.rb のリファクタリングだ。

アヤカがレビューを開いた。コードを見るより前に、PRの説明文が目に入った。

PR #247: orders_controller.rb のリファクタリング

変更内容:
- CQRS: 読み取りをQueryオブジェクトに分離
- レイヤードアーキテクチャ: OrderCreationServiceへのビジネスロジック移行
- リポジトリパターン: OrderRepository, ProductRepository の導入
- イベント駆動: 注文確定後の処理をEventBus経由に変更
- バックプレッシャー: RateLimiterとOrderQueueServiceの導入
- サーキットブレーカー: 外部API呼び出しに適用

Before: 800行のモノリシックコントローラー
After:  35行のコントローラー + 各責任をもつクラス群

テスト: 47件追加(全てパス)

「いいね」アヤカが静かに言った。

コードを見ると:

# Before: 800行のファットコントローラー
class OrdersController < ApplicationController
  def create
    # バリデーション、在庫チェック、合計計算、クーポン、保存、
    # メール、ポイント、Slack...全部ここに(800行)
  end
end
 
# After: 各パターンが適切な場所に
class OrdersController < ApplicationController
  before_action :authenticate_user!
  before_action :check_rate_limit, only: [:create]
 
  # Query Side(CQRSパターン)
  def index
    @order_summaries = OrderListQuery.new.call(
      user_id: current_user.id,
      page: params[:page]&.to_i || 1,
      status: params[:status]
    )
  end
 
  def show
    @order = OrderDetailQuery.new.call(
      order_id: params[:id].to_i,
      user_id: current_user.id
    )
  rescue OrderNotFoundError
    redirect_to orders_path, alert: "注文が見つかりません"
  end
 
  # Command Side(CQRSパターン + バックプレッシャー + サーキットブレーカー)
  def create
    result = OrderQueueService.new.enqueue(
      user_id: current_user.id,
      items: order_params[:items],
      priority: current_user.vip? ? :vip : :normal
    )
 
    render json: {
      message: "注文を受け付けました",
      job_id: result[:job_id],
      status_url: order_status_path(result[:job_id])
    }, status: :accepted
 
  rescue QueueFullError
    render json: { error: "只今注文が集中しています。しばらくお待ちください。" },
           status: :service_unavailable
  rescue RateLimiter::ExceededError
    render json: { error: "注文は1分に5件まで" },
           status: :too_many_requests
  end
 
  def status
    result = OrderQueueService.new.status(params[:job_id])
    render json: result
  end
 
  private
 
  def check_rate_limit
    limiter = RateLimiter.new(key: "orders:#{current_user.id}", limit: 5, window: 60)
    raise RateLimiter::ExceededError unless limiter.allowed?
  end
 
  def order_params
    params.require(:order).permit(:coupon_code, items: [:product_id, :quantity])
  end
end

パターンの配置図

ユウキのリファクタリングには、12週間で学んだパターンが全て入っていた。

Loading diagram...

各層の責任:

パターン役割コードの場所
ネットワークバックプレッシャートラフィック制御API Gateway / Rack::Attack
コントローラーMVC / CQRSリクエスト振り分けcontrollers/
アプリケーションレイヤードアーキテクチャユースケースservices/
データリポジトリパターンDB抽象化repositories/
通信サーキットブレーカー外部障害遮断clients/
非同期イベント駆動疎結合な後処理events/, handlers/
整合性サーガパターン分散トランザクションsagas/

AWS全体アーキテクチャ

Loading diagram...

パターン選択の判断フレームワーク

12週間でユウキが身につけた「どのパターンを選ぶか」の判断基準:

問題の種類で選ぶ

「コードの整理がしたい、責任が混在している」
  → MVCパターン + レイヤードアーキテクチャ

「同じクエリが色々な場所に散らばっている」
  → リポジトリパターン + クエリオブジェクト

「コントローラーが300行を超えている」
  → サービスオブジェクト(Application Layer)

「注文一覧ページが遅い」
  → CQRSパターン → 読み取りレプリカ → キャッシュ

「処理が増えるたびにコードが増える、密結合」
  → イベント駆動アーキテクチャ

「データ変換ロジックが複雑で追いにくい」
  → パイプ&フィルタパターン

「外部APIが遅くてユーザーを待たせる」
  → 非同期化(ActiveJob)+ サーキットブレーカー

「マイクロサービス間で整合性が取れない」
  → サーガパターン(オーケストレーション or コレオグラフィ)

「レガシーを直したい(止めずに)」
  → ストラングラーフィグパターン

「高負荷でシステムが崩壊する、セール対策」
  → バックプレッシャー + レート制限 + キューイング

アンチパターン:よくある間違い

12週間で学んだ「やってはいけないこと」も重要だ。

# アンチパターン1: パターンの過剰適用
# 3行で済む処理にリポジトリ・サービス・コマンドを作る
class GetUserNameService
  def initialize(user_repository)
    @user_repository = user_repository
  end
 
  def call(user_id)
    @user_repository.find(user_id).name  # User.find(id).name でいい
  end
end
 
# アンチパターン2: レイヤーを飛び越える
class OrdersController
  def create
    # コントローラーがDBに直接書く
    Order.create!(params)        # NG: サービス層を飛ばしている
    Product.decrement!(:stock)   # NG: ビジネスロジックがコントローラーに
  end
end
 
# アンチパターン3: 間違ったパターン選択
# 小さなアプリにサーガパターン
class UserRegistrationSaga  # 1サービスの処理にサーガは不要
  # マイクロサービス間でないなら単なるトランザクションでよい
end
 
# アンチパターン4: パターン名の乱用
class UserService  # 何でもServiceと名付ける
  def create_user; end
  def send_email; end
  def calculate_discount; end
  def generate_report; end
  # 何をするクラスかわからない
end
 
# 正しい命名(責任が明確)
class UserCreationService; end      # ユースケース
class EmailNotificationService; end # インフラ
class DiscountCalculator; end       # ドメイン
class SalesReportQuery; end         # クエリ

WARNING

パターンは「問題が先、パターンが後」。問題がないのにパターンを適用すると、複雑さだけが増える。「これを使えば良いコードになる」ではなく、「この問題にはこのパターンが適切」という順番で考える。

進化的アーキテクチャ

「ユウキ、一番大事なことを教える」

アヤカが珍しく真剣な顔をした。

「アーキテクチャは一度作ったら終わりじゃない。システムが成長するにつれて、アーキテクチャも進化させる必要がある」

フェーズ1(初期: ~1万ユーザー)
  シンプルなMVC + モノリス
  単一のRDS、シンプルなスキーマ
  Heroku / Railway でも十分
  ↓ ユーザーが増えてクエリが遅くなる

フェーズ2(成長期: ~10万ユーザー)
  レイヤードアーキテクチャ + CQRS
  RDS Read Replica + ElastiCache
  ECS / Fargate への移行
  ↓ 機能が増えてコードが複雑になる

フェーズ3(拡大期: ~100万ユーザー)
  イベント駆動 + サーガ
  SNS/SQS でサービス間通信
  サーキットブレーカー導入
  ↓ トラフィックが急増する

フェーズ4(成熟期: ~1000万ユーザー)
  バックプレッシャー + レート制限
  CDN + WAF + API Gateway
  マルチAZ、DR設計
  ↓ 特定機能を独立させたい

フェーズ5(分散期)
  ストラングラーフィグで段階的分離
  サービスメッシュ(Istio等)
  分散トレーシング(OpenTelemetry)

「最初から全部やる必要はない?」

YAGNI(You Ain't Gonna Need It)。今必要でないものは作らない。でも、パターンを知っていれば、必要になったときに素早く導入できる。知っているが使わない知らないは全然違う」

コードベースの健康指標

「パターンを適切に使えているか、どうやって確認しますか?」ユウキが聞いた。

「コードメトリクスで確認できる」

# lib/tasks/code_health.rake
namespace :code_health do
  desc "コントローラーの行数チェック"
  task controller_size: :environment do
    fat_controllers = []
 
    Dir.glob("app/controllers/**/*.rb").each do |path|
      lines = File.readlines(path).count
      if lines > 100
        fat_controllers << { path: path, lines: lines }
      end
    end
 
    if fat_controllers.any?
      puts "警告: ファットコントローラーが検出されました"
      fat_controllers.each { |c| puts "  #{c[:path]}: #{c[:lines]}行" }
    else
      puts "OK: 全コントローラーが100行以下です"
    end
  end
 
  desc "循環参照チェック"
  task circular_deps: :environment do
    # 上位レイヤーが下位レイヤーを参照していないか確認
    violations = []
 
    Dir.glob("app/models/**/*.rb").each do |path|
      content = File.read(path)
      if content.match?(/require.*controllers|include.*Controller/)
        violations << "#{path}: モデルがコントローラーを参照"
      end
    end
 
    violations.empty? ? puts("OK: 循環参照なし") : violations.each { |v| puts "警告: #{v}" }
  end
end

学習の次のステップ

# これからユウキが学ぶこと
next_steps = {
  architecture: [
    "ヘキサゴナルアーキテクチャ(ポートとアダプター)",
    "クリーンアーキテクチャ",
    "ドメイン駆動設計(DDD)"  # 本サイトに別トピックあり
  ],
  aws: [
    "EKS(Kubernetes on AWS)",
    "AWS CDK でインフラをコードで管理",
    "OpenTelemetry による分散トレーシング",
    "AWS AppMesh / Service Mesh"
  ],
  rails: [
    "Hotwire / Turbo でリアルタイム更新",
    "Action Cable WebSocket",
    "Rails Engine でのモジュール分割",
    "Zeitwerk オートローディング"
  ],
  golang: [
    "Go でのマイクロサービス実装",
    "gRPC によるサービス間通信",
    "Go の並行処理パターン"
  ]
}

INFO

アーキテクチャパターンは手段であり、目的はユーザーへの価値提供。「良いコード」を書くことではなく「良いプロダクト」を作ることが最終目標。パターンはそのための道具。道具の使い方を学んだ今、次は「いつ、どの道具を使うか」の判断力を磨く番。

ユウキの12週間

「PRのコメントを読んで」

アヤカがPRにコメントを入れていた。

LGTM 

12週間前のあなたなら、これを全部コントローラーに書いていた。
今のあなたは、問題を見て「これはCQRSで解ける」「ここにサーキットブレーカーが必要」
「この非同期処理はイベント駆動にすべき」と言える。

それがアーキテクチャを学んだということ。

コードを書く力は大事。でも、設計を議論できる力はもっと大事。
チームで同じ言葉で話せるようになったこと——それが今日の一番の成果。

次は DDD(ドメイン駆動設計)に挑戦してみて。
ユビキタス言語、境界付きコンテキスト、集約……
今あなたが持っている基礎の上に、さらに大きな概念が積み重なる。

ユウキはしばらくコメントを読んでいた。

12週間前、800行のコードを見てただ「読みにくい」と思っていた。今は違う。どこに何があるべきかが見える。変更がどこに影響するかが見える。新しい機能をどこに追加すべきかが見える。

そして何より、設計の話がチームメンバーと5分でできるようになった。

「MVCで言うとここがController相当で……」「これはCQRSを適用すべき場所だと思います」「サーキットブレーカーがないとカスケード障害が起きます」——そういった言葉が自然に出るようになっていた。

それが、パターンを学んだということだった。


まとめ:12のパターン

パターン解決する問題キーワード学んだ章
MVCパターン責任の混在モデル・ビュー・コントローラー2章
レイヤードアーキテクチャロジックの散在関心の分離3章
リポジトリパターンクエリの散在データアクセスの抽象化4章
イベント駆動密結合な後処理疎結合・非同期5章
パイプ&フィルタデータ変換の複雑さフィルターチェーン6章
CQRSパターン読み取りのパフォーマンス読み書きの分離7章
サーガパターン分散トランザクション補償トランザクション8章
ストラングラーフィグレガシーの刷新段階的移行9章
サーキットブレーカーカスケード障害遮断・フォールバック10章
バックプレッシャー過負荷崩壊レート制限・キューイング11章

次のプロジェクトへの持ち帰りチェックリスト

「次の案件でどう使うか、具体的なチェックリストを作っておくといい」とアヤカが言った。

# 新しいプロジェクト参画時のチェックリスト(Rakeタスク形式)
namespace :architecture do
  desc "プロジェクト健康診断"
  task :health_check do
    puts "=== アーキテクチャ健康診断 ===\n\n"
 
    checks = {
      "MVCの責任分離"        => -> { check_fat_controllers },
      "レイヤー構造の存在"    => -> { check_service_layer },
      "リポジトリパターン"    => -> { check_repository_usage },
      "イベントの疎結合"      => -> { check_event_driven },
      "CQRSの適用箇所"       => -> { check_cqrs_pattern },
      "障害対応の仕組み"      => -> { check_circuit_breaker },
    }
 
    checks.each do |name, check|
      result = check.call
      status = result[:ok] ? "✓" : "!"
      puts "  #{status} #{name}: #{result[:message]}"
    end
  end
 
  private
 
  def check_fat_controllers
    fat = Dir.glob("app/controllers/**/*.rb").select { |f| File.readlines(f).count > 100 }
    fat.empty? ? { ok: true, message: "全コントローラー100行以下" }
               : { ok: false, message: "ファット: #{fat.map { |f| File.basename(f) }.join(', ')}" }
  end
 
  def check_service_layer
    has_services = Dir.exist?("app/services") && Dir.glob("app/services/**/*.rb").any?
    has_services ? { ok: true, message: "app/services/ あり" }
                 : { ok: false, message: "サービス層なし — レイヤードアーキテクチャを検討" }
  end
 
  def check_repository_usage
    has_repos = Dir.glob("app/repositories/**/*.rb").any?
    has_repos ? { ok: true, message: "リポジトリパターン採用済み" }
              : { ok: false, message: "リポジトリなし — クエリがモデルに散在していないか確認" }
  end
 
  def check_event_driven
    has_events = Dir.glob("app/events/**/*.rb").any? || defined?(ActiveSupport::Notifications)
    has_events ? { ok: true, message: "イベント機構あり" }
               : { ok: false, message: "イベント駆動なし — 後処理が密結合になっていないか確認" }
  end
 
  def check_cqrs_pattern
    has_queries = Dir.glob("app/queries/**/*.rb").any?
    has_queries ? { ok: true, message: "Queryオブジェクトあり" }
                : { ok: false, message: "Queryオブジェクトなし — 重い読み取りクエリがサービスに埋まっていないか確認" }
  end
 
  def check_circuit_breaker
    has_cb = Gem.loaded_specs.key?("circuitbox") || Gem.loaded_specs.key?("resilient")
    has_cb ? { ok: true, message: "サーキットブレーカーライブラリあり" }
           : { ok: false, message: "障害対応なし — 外部API呼び出しがある場合は要検討" }
  end
end

INFO

このチェックリストをプロジェクト参画初日に実行すると、技術的な会話のたたき台ができる。「リポジトリがない」「サービス層がない」という発見は改善提案の起点になる。

パターンを使った設計議論の例

パターンを学ぶと、設計の議論が変わる。ユウキが実際に経験した変化を振り返った。

学ぶ前

ユウキ: 「この処理、なんかコードが長くて読みにくいですね…」
上司: 「直せる?」
ユウキ: 「えっと、ちょっと難しいかも…」

学んだ後

ユウキ: 「OrdersControllerが800行あります。バリデーション、DB保存、
        メール送信、ポイント計算が混在しています。
        まずApplication Serviceを切り出してコントローラーを
        薄くしましょう。次のスプリントで着手可能です。」
上司:   「工数は?」
ユウキ: 「4時間で完了できます。リグレッションリスクは低く、
        既存のRSpecがカバーしています。」

語彙があると、会話が具体的になる。具体的になると、行動できる。

Golang版の設計視点

Railsで学んだパターンは言語を超える。Goプロジェクトでも同じパターンが使われる。

// パターンの対応関係
 
// Rails: app/services/order_creation_service.rb
// Go:    internal/usecase/order_usecase.go
 
package usecase
 
type OrderUseCase struct {
    repo      repository.OrderRepository   // リポジトリパターン
    publisher event.Publisher              // イベント駆動
    cb        circuitbreaker.CircuitBreaker // サーキットブレーカー
}
 
func (u *OrderUseCase) CreateOrder(cmd CreateOrderCommand) (*Order, error) {
    // Application Service と同じ責任
    if err := cmd.Validate(); err != nil {
        return nil, err
    }
 
    order, err := u.repo.Save(NewOrder(cmd))
    if err != nil {
        return nil, err
    }
 
    // イベント発行(疎結合)
    u.publisher.Publish(OrderCreatedEvent{OrderID: order.ID})
 
    return order, nil
}

「RailsとGoは文法が違う。でも OrderUseCase を見た瞬間に、Railsの OrderCreationService と同じ構造だとわかる。パターンの名前を知っているから」

INFO

パターンを学ぶことは、特定の言語やフレームワークへの依存から脱することでもある。設計の考え方は普遍的で、RailsエンジニアがGoへ転向するとき、Djangoエンジニアがに移行するときも、パターンの知識は新しい言語の設計を読む地図になる。

ユウキの次のステップ

「12章を終えたユウキへ。次に学ぶとよいことを残しておく」アヤカが Notion にページを作った。

■ DDDへの入り口
  - ユビキタス言語: チームとコードで同じ用語を使う
  - 集約: 整合性の境界を定義する
  - 境界付きコンテキスト: 大きなシステムを分割する

■ マイクロサービス設計
  - サービス分割の基準(境界付きコンテキストが基礎)
  - サービス間通信(同期/非同期の使い分け)
  - 分散トレーシング(Datadog APM, OpenTelemetry)

■ 可観測性
  - ログ設計(構造化ログ + コンテキスト)
  - メトリクス設計(何を計測するか)
  - SLO/SLAの設計と運用

■ チームのための設計
  - ADR(Architecture Decision Records)の書き方
  - 技術的負債の管理と可視化
  - コードレビューでの設計フィードバックの与え方

「全部を一度にやる必要はない。今のプロジェクトで1つ問題を見つけ、1つパターンを適用する。それを繰り返す」

ユウキはノートを閉じた。

12週間で変わったのは、コードの書き方だけではなかった。問題の見方が変わった。「なぜこうなっているか」「どうすればよくなるか」を考える習慣がついた。

それが、エンジニアとして成長するということだった。


「パターンは知識ではなく、筋肉だ。使うことで身につく。」 — アヤカ

「設計の議論ができるようになったとき、エンジニアは一段階成長する。」 — ユウキ(12週間後)