mybook

エピローグ — パターンの組み合わせと過剰適用の罠

6ヶ月後のケンタ

入社から半年が経った。

会社のスラックに通知が飛んできた。「後輩のユウキくんが本番デプロイしました」というメッセージだ。ケンタは軽い気持ちでプルリクエストを開いた。

注文完了処理の実装——ケンタが先月作ったフローの改修だ。

# ユウキのコード
class OrderCompletionFacade
  include Singleton
 
  def initialize
    @observer = OrderEventObserver.new
    @strategy_factory = StrategyFactory.new
    @command_chain = CommandChain.new([
      RefundCalculationCommand,
      StockReductionCommand,
      PointGrantCommand,
      NotificationCommand
    ])
    @builder = OrderCompletionReportBuilder.new
    @proxy = CachedCompletionService.new(OrderCompletionService.new)
  end
 
  def complete(order)
    strategy = @strategy_factory.create_for(order.order_type)
    report = @builder.build_for(order)
    result = @proxy.complete(strategy.execute(@observer.notify(@command_chain.execute(order))))
    report.finalize(result)
  end
end

ケンタは思わず声が出た。「えっ……」

Singleton、Observer、StrategyFactory、CommandChain、Builder、Proxy——学んできたパターンが全部入っている。しかし何をやっているのか全く読めない。

チームのデイリースタンドアップで、ユウキはこう発表した。「昨日、デザインパターンを全部勉強したので、注文完了処理に全部適用してリファクタリングしました。」

ケンタは苦笑した。自分も半年前、パターンを覚えたばかりのときに同じことをやろうとした気がする。

「ユウキさん、ちょっといい?」

パターンの過剰適用(Overengineering)

「デザインパターンを全部使えば良いコードになると思って……」ユウキは言いにくそうに言った。「評価されると思ってたんですが、コードレビューで山田さんに『一旦戻して』と言われてしまいました。」

ケンタは山田さんに言われた言葉を思い出した。

「パターンは目的じゃない。問題を解決するための手段だ。」

「そもそも、このコードを書く前に何の問題があったの?」とケンタは聞いた。

「えーと……とくに問題はなかったんですが、もっと良くできると思って……」

「それが問題の原因だよ。」

過剰設計(Overengineering) または「パターン強迫」と呼ばれる現象だ。

# 過剰設計の典型例
# 「将来的に複数の割引戦略が必要かもしれない」という理由で作られたクラス群
 
class PriceCalculationStrategyFactory
  def self.create(strategy_type)
    case strategy_type
    when :simple then SimplePriceCalculationStrategy.new
    when :complex then ComplexPriceCalculationStrategy.new
    end
  end
end
 
class PriceCalculationStrategyInterface
  def calculate(price, options = {})
    raise NotImplementedError
  end
end
 
class SimplePriceCalculationStrategy < PriceCalculationStrategyInterface
  def calculate(price, options = {})
    price  # ただprice を返すだけ!
  end
end
 
class ComplexPriceCalculationStrategy < PriceCalculationStrategyInterface
  def calculate(price, options = {})
    price * 1.0  # これだけのために抽象化する必要はない
  end
end
 
# 実際に必要だったのはこれだけ
def calculate_price(amount)
  amount
end

WARNING

YAGNI原則(You Ain't Gonna Need It) — 「今必要ないものを作るな」。将来必要になるかもしれないから今抽象化する、というのは多くの場合間違いだ。本当に必要になったときにリファクタリングすれば良い。予測した未来の変更のためのコードは、予測が外れると負債になる。

よくある過剰設計の症状:

  • すべてのクラスにInterface/Strategy/Factoryを追加する
  • 将来使うかもしれない拡張性のために、今必要のない抽象を入れる
  • 「このパターンを使ったことがある」という満足感のためにパターンを使う
  • コードを読む人が混乱するのに、設計者だけが「エレガントだ」と思っている

パターンを選ぶ判断軸

「パターンは問題があって初めて使うんだ」とケンタはユウキに言った。

「どういうときに使うべきか、わかりやすい基準はありますか?」

「問題を先に言語化してみよう。『〇〇という問題がある』という文章が書けないとき、そのパターンは不要かもしれない。」

問題→パターンの対応表

感じる問題使うべきパターン典型的なRailsの場面
条件分岐が多く、新しい種別追加のたびに全体を修正するStrategy配送料計算・割引ロジック
何かが起きたとき、複数の場所に通知したいObserver注文完了後の各種処理
オブジェクトの生成方法が複雑、種類によって違うFactory決済プロバイダの切り替え
アプリ全体で設定・接続を共有したいSingleton(慎重に)DB接続・設定オブジェクト
外部ライブラリのインターフェースが使いにくいAdapterActiveStorage・Stripe
オブジェクトに表示用の機能を追加したいDecoratorDraper gem
複数サービスをまとめてコントローラを薄くしたいFacadeサービスオブジェクト
同じ骨格の処理で部分だけ違うTemplate MethodApplicationController
操作を遅延実行・取り消し・記録したいCommandActiveJob
ステータスによって振る舞いが変わるStateAASM gem
引数が多く、オブジェクト構築が複雑BuilderActiveRecordクエリ
別オブジェクトへのアクセスを制御したいProxyキャッシュ・権限チェック
複数のチェックを順番に実行したいChain of Responsibilityバリデーション・Rack

INFO

この表は「問題から入る」ためのものだ。パターン名を先に見て「このパターンを使いたい」と思うのではなく、コードを書いていて「何か問題を感じる」ときにこの表を参照する。感じている問題が左列のどれかに当てはまれば、右列を検討しよう。

リファクタリング:ユウキのコードを整理する

ケンタはユウキと一緒にコードを整理した。

「まず、注文完了処理で実際に何の問題があったかを確認しよう。元のコードを見てみると……」

# 元のコード(問題のある状態)
class OrdersController < ApplicationController
  def complete
    order = Order.find(params[:id])
 
    # 在庫を減らす
    order.order_items.each do |item|
      item.product.decrement!(:stock, item.quantity)
    end
 
    # ポイント付与
    order.user.increment!(:points, (order.total_price / 100).floor)
 
    # メール送信
    OrderMailer.completion(order).deliver_later
    AdminMailer.new_order_notification(order).deliver_later
 
    # 注文のステータス更新
    order.update!(status: :completed, completed_at: Time.current)
 
    render json: { message: "注文が完了しました" }
  end
end

「コントローラが太りすぎている。これはFacadeパターンの出番ですね」とユウキが言った。

「正しい。問題は『コントローラが複数の責務を持っている』だ。Facadeで整理しよう。ただし、今必要な最小限のFacadeを作る。」

# app/services/order_completion_service.rb(Facade)
class OrderCompletionService
  def initialize(order:)
    @order = order
  end
 
  def call
    ActiveRecord::Base.transaction do
      reduce_stock
      grant_points
      complete_order
    end
 
    send_notifications  # トランザクション外(失敗してもロールバックしない)
 
    @order
  end
 
  private
 
  def reduce_stock
    @order.order_items.each do |item|
      # lock!でデッドロック防止
      product = item.product.lock!
      raise InsufficientStockError, "#{product.name}の在庫が不足しています" if product.stock < item.quantity
 
      product.decrement!(:stock, item.quantity)
    end
  end
 
  def grant_points
    point_amount = (@order.total_price / 100).floor
    @order.user.increment!(:points, point_amount)
  end
 
  def complete_order
    @order.complete!  # AASMの状態遷移
  end
 
  def send_notifications
    OrderMailer.completion(@order).deliver_later
    AdminMailer.new_order_notification(@order).deliver_later
  end
end
# コントローラはシンプルに
class OrdersController < ApplicationController
  def complete
    order = current_user.orders.find(params[:id])
 
    result = OrderCompletionService.new(order: order).call
 
    render json: { message: "注文が完了しました", order_id: result.id }
  rescue InsufficientStockError => e
    render json: { error: e.message }, status: :unprocessable_entity
  end
end

「これだけでいい。今はこれ以上のパターンは必要ない。」

「もし将来、複数の決済プロバイダが必要になったら?」

「そのときにStrategyパターンを追加すれば良い。今は1プロバイダだから不要だ。」

実際のシステムでのパターンの自然な組み合わせ

良いコードでは、複数のパターンが問題が増えるにつれて自然に組み合わさる。

ケンタが6ヶ月かけて作り上げた注文システムの最終形を見ると、パターンが問題解決のために使われていた:

Loading diagram...
  • Facade — 「コントローラが太りすぎている」問題を解決
  • Chain of Responsibility — 「バリデーションがコントローラに集中している」問題を解決
  • Factory + Adapter — 「決済プロバイダが増えてif-elseが増えた」問題を解決
  • Observer — 「注文完了後の処理が増えるたびにサービスを修正している」問題を解決

それぞれのパターンに対応する「問題」がある。問題なしにパターンを使っていない。

アンチパターン: よく見るやり過ぎ

1. 神クラス(God Object)

# すべてを知っているクラス(アンチパターン)
class OrderManager
  def create_order(params); end
  def calculate_shipping(order); end
  def process_payment(order, payment); end
  def send_confirmation_email(order); end
  def update_inventory(order); end
  def generate_invoice(order); end
  def apply_discount(order, coupon); end
  def check_fraud(order); end
  def notify_warehouse(order); end
  def update_crm(order); end
  def log_analytics(order); end
  # ...800行のクラス
end

Facadeパターンと似ているが、決定的な違いがある。Facadeはサブシステムに処理を委ねる(薄いラッパー)のに対し、神クラスは自分で全部処理する(厚い実装)。

Facadeが正しい例:

class OrderCreationService  # Facade
  def call
    validate      # → OrderValidationChain に委ねる
    charge        # → PaymentGateway に委ねる
    create        # → Order.create! に委ねる
    notify        # → Notifications に委ねる
  end
end

2. 過剰な抽象化

# 必要のないStrategyインターフェース(アンチパターン)
class UserGreetingStrategyBase
  def greet(user)
    raise NotImplementedError
  end
end
 
class DefaultUserGreetingStrategy < UserGreetingStrategyBase
  def greet(user)
    "こんにちは、#{user.name}さん"  # ← これだけのために抽象化する必要はない
  end
end
 
# 素直に書けばいい
def greeting_for(user)
  "こんにちは、#{user.name}さん"
end

「将来的に挨拶のバリエーションが増えるかもしれない」という理由でStrategyを使うのは過剰だ。実際に2種類以上の戦略が必要になったときに初めて導入すれば良い。

3. Decoratorの乱用

# Decoratorを重ねすぎて何が何だかわからない(アンチパターン)
user = User.find(1)
  .then { |u| AdminRoleDecorator.new(u) }
  .then { |u| PremiumMemberDecorator.new(u) }
  .then { |u| LocaleDecorator.new(u) }
  .then { |u| TimezoneDecorator.new(u) }
  .then { |u| CurrencyDecorator.new(u) }
  .then { |u| NotificationPreferenceDecorator.new(u) }

Decoratorの適用は2〜3層が限界だ。それ以上になったら1つのDecoratorに統合するか、別の設計(例えばPresenteオブジェクト)を検討しよう。

4. Singletonの乱用

# グローバル状態を持つSingleton(テストが難しい)
class AppConfig
  include Singleton
 
  def initialize
    @config = YAML.load_file(Rails.root.join("config/app.yml"))
  end
 
  def late_fee
    @config["late_fee"]
  end
end
 
# テストが難しい — Singletonは状態が漏れる
class OrderService
  def calculate_late_fee(order)
    AppConfig.instance.late_fee * order.days_late
  end
end
# 改善: 設定を依存性注入で渡す
class OrderService
  def initialize(config: Rails.application.config_for(:app))
    @config = config
  end
 
  def calculate_late_fee(order)
    @config[:late_fee] * order.days_late
  end
end
 
# テストで差し替えられる
service = OrderService.new(config: { late_fee: 100 })

テスタビリティが良い設計の指標

「どのパターンを使うか迷ったとき、テストが書きやすいかを基準にしよう」とケンタはユウキに言った。

# テストしにくいコード(問題のある設計)
class OrderService
  def complete(order)
    # グローバル状態に依存
    fee = AppConfig.instance.late_fee
    # 外部APIに直接依存
    charge_id = Stripe::Charge.create(amount: order.total_price * 100, currency: "jpy")[:id]
    # メールを直接送信
    OrderMailer.confirmation(order).deliver_now
    order.update!(status: :completed, stripe_charge_id: charge_id)
  end
end
 
# テストするためには本物のStripeキーが必要……
# メールを実際に送ってしまう……
# テストしやすいコード(依存性注入を使う)
class OrderService
  def initialize(
    config: Rails.application.config_for(:app),
    payment_gateway: StripeGateway.new,
    mailer: OrderMailer
  )
    @config = config
    @payment_gateway = payment_gateway
    @mailer = mailer
  end
 
  def complete(order)
    charge = @payment_gateway.charge(
      amount: order.total_price,
      currency: "jpy"
    )
    order.update!(status: :completed, charge_id: charge[:id])
    @mailer.confirmation(order).deliver_later
  end
end
# テストでモックを差し込める
RSpec.describe OrderService do
  let(:mock_gateway) do
    instance_double(StripeGateway, charge: { id: "ch_test_123", success: true })
  end
  let(:mock_mailer) { class_double(OrderMailer, confirmation: double(deliver_later: true)) }
 
  subject(:service) do
    described_class.new(
      config: { late_fee: 100 },
      payment_gateway: mock_gateway,
      mailer: mock_mailer
    )
  end
 
  it "注文を完了させる" do
    order = create(:order, :paid)
    service.complete(order)
    expect(order.reload.status).to eq("completed")
  end
 
  it "支払いを処理する" do
    order = create(:order, :paid, total_price: 5000)
    service.complete(order)
    expect(mock_gateway).to have_received(:charge).with(amount: 5000, currency: "jpy")
  end
end

テストが書きにくいコードは、設計が悪いコードである可能性が高い。 テストを書くために「あのグローバル変数をなんとかしないといけない」「Stripeの実際のAPIキーが必要」と感じたら、それは設計の見直しサインだ。

パターンより大切なこと

ユウキのリファクタリングを終えて、ケンタは感じていたことを伝えた。

「僕も半年前、パターンを覚えたばかりのときに同じことをやろうとした。山田さんに言われたのは、『パターンは言語だ』ということだった。」

「言語?」

「GoFの23パターンを全部暗記しなくていい。でも問題を見たときに『これはあのパターンだな』と気づく感覚を育てることが大事だ、と。その感覚は、コードを書いて、レビューされて、古いコードを読んで、また書く——その繰り返しで育つ。」

「それに……」とケンタは続けた。「パターンを知っていると、チームで設計の議論ができるようになる。『ここはChain of Responsibilityで実装しましょう』と言えれば、全員が同じイメージで話せる。コードを書く前に認識を合わせられる。それが一番の価値だと思う。」

ユウキは頷いた。「パターンは会話の語彙なんですね。」

「そうだ。良い言葉を持つと、良い設計の議論ができる。」

設計の3つの問い

良い設計かどうかを判断するとき、ケンタはいつもこの3つを自問するようになった。

Loading diagram...
  1. 変更の理由は1つか?(単一責任原則)— このクラスを変更する理由が複数あれば、複数の責務を持っている
  2. テストを書けるか?(テスタビリティ)— テストが書きにくければ依存関係の問題がある
  3. 次に触れる人が理解できるか?(可読性)— 6ヶ月後の自分が読んでわかるか?

パターンはこの3つを達成するための手段だ。

この旅で学んだこと

ケンタが最初にコードレビューで「匂い」と言われてから半年。彼が学んだのはパターンの名前だけじゃなかった。

入社してすぐの頃、ケンタはコードを「動けばいい」と思っていた。テストを書かず、変数名は atmp、100行のメソッドに全部書いていた。

山田さんの最初のレビューコメントを今でも覚えている。

「動くコードを書くことは最低限。次に読む人(多くの場合、未来の自分)が理解できるコードを書くことが本当の仕事だ。」

最初は反発した。「動けばいいじゃないですか」と心の中で思っていた。でも半年で変わった。

自分が1ヶ月前に書いたコードを読んで、「これを書いたのは誰だ」と思うことがあった。コメントがない、変数名が不明瞭、条件分岐が5重になっている。それが自分が書いたコードだと気づいて、恥ずかしくなった。

「コードは一度書いて終わりじゃない。次の人(そして未来の自分)が読んで、変更できることが大事だ。」

デザインパターンは、それを助けるための先人の知恵だ。全部を覚える必要はない。問題に直面したとき、「こんな問題、先人たちはどう解決したのかな?」と考えるきっかけになれば十分だ。

ユウキのコードレビューを終えて、ケンタはGitHubにコメントを書いた。

「パターンの理解度が高い!各パターンの使い方は正しい。一つアドバイスすると、まずシンプルに書いて、複雑になったら段階的にパターンを適用してみよう。今回の注文完了処理は、まずFacadeだけで十分だと思う。良いコードは後から読む人のためにある。次のPRを楽しみにしています。」

山田さんが最初に言っていたことを、今度はケンタが伝える番だった。

各章で学んだパターンの一覧

#パターン解決する問題Railsでの代表例
2Strategy条件分岐の交換可能化配送料・割引計算の切り替え
3Observer変更の通知と疎結合ActiveSupport::Notifications
4Factoryオブジェクト生成の抽象化FactoryBot
5Singleton唯一のインスタンス保証Rails.application
6Adapterインターフェース変換ActiveStorage
7Decorator機能の動的追加Draper gem
8Facade複雑さの隠蔽サービスオブジェクト
9Template Method処理の骨格の共通化ApplicationController
10Command操作のオブジェクト化ActiveJob
11State状態ごとの振る舞い管理AASM gem
12Builder複雑なオブジェクトの段階的構築ActiveRecordクエリ
13Proxyアクセスの制御と代理キャッシュ・CloudFront
14Chain of Responsibility処理の連鎖と分離Rackミドルウェア

次のステップ

パターンを学んだ後、ケンタが次に目を向けたのはより大きな設計の世界だ。

ドメイン駆動設計(DDD) — ビジネスドメインをコードに直接表現する。ValueObject、Aggregate、Repositoryなどの概念でビジネスの複雑さに立ち向かう。

Clean Architecture — 依存の方向を制御する。ビジネスロジックはRailsに依存しない。インフラ(DB、Web)は外側にある。

Event Sourcing — 状態ではなくイベントを記録する。Commandパターン・Stateパターンを発展させた考え方。

これらはデザインパターンよりも大きな単位の「建築思想」だ。でも全部、今まで学んできたパターンの組み合わせと発展系だ。

ケンタはパソコンを閉じて伸びをした。

「山田さん、次は何を教えてもらえますか?」

「DDD?Clean Architecture?」山田さんは少し笑った。「でもまずは今まで学んだことを使い倒してみよう。パターンは知識じゃなくて経験で身につく。」

「わかりました。」

「良いコードを書き続けることだ。それが全てだよ。」


INFO

エピローグのまとめ

  • パターンは問題を解決するための手段。目的化してはいけない
  • YAGNI原則:今必要ないものは作らない
  • 問題から始める:「〇〇という問題がある」が言えないパターンは不要
  • 過剰設計より単純なコードの方が価値がある
  • テストが書きやすいコードは良い設計のサイン
  • パターンは組み合わせて使うが、自然に組み合わさるのが理想
  • 「変更の理由は1つか」「テストできるか」「次の人が理解できるか」の3問が判断軸
  • パターンはチームで設計を議論するための共通語彙

ケンタの旅はまだ続く。次はDDDかClean Architectureか——でもそれはまた別の話だ。