エピローグ — パターンの組み合わせと過剰適用の罠
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
endWARNING
YAGNI原則(You Ain't Gonna Need It) — 「今必要ないものを作るな」。将来必要になるかもしれないから今抽象化する、というのは多くの場合間違いだ。本当に必要になったときにリファクタリングすれば良い。予測した未来の変更のためのコードは、予測が外れると負債になる。
よくある過剰設計の症状:
- すべてのクラスにInterface/Strategy/Factoryを追加する
- 将来使うかもしれない拡張性のために、今必要のない抽象を入れる
- 「このパターンを使ったことがある」という満足感のためにパターンを使う
- コードを読む人が混乱するのに、設計者だけが「エレガントだ」と思っている
パターンを選ぶ判断軸
「パターンは問題があって初めて使うんだ」とケンタはユウキに言った。
「どういうときに使うべきか、わかりやすい基準はありますか?」
「問題を先に言語化してみよう。『〇〇という問題がある』という文章が書けないとき、そのパターンは不要かもしれない。」
問題→パターンの対応表
| 感じる問題 | 使うべきパターン | 典型的なRailsの場面 |
|---|---|---|
| 条件分岐が多く、新しい種別追加のたびに全体を修正する | Strategy | 配送料計算・割引ロジック |
| 何かが起きたとき、複数の場所に通知したい | Observer | 注文完了後の各種処理 |
| オブジェクトの生成方法が複雑、種類によって違う | Factory | 決済プロバイダの切り替え |
| アプリ全体で設定・接続を共有したい | Singleton(慎重に) | DB接続・設定オブジェクト |
| 外部ライブラリのインターフェースが使いにくい | Adapter | ActiveStorage・Stripe |
| オブジェクトに表示用の機能を追加したい | Decorator | Draper gem |
| 複数サービスをまとめてコントローラを薄くしたい | Facade | サービスオブジェクト |
| 同じ骨格の処理で部分だけ違う | Template Method | ApplicationController |
| 操作を遅延実行・取り消し・記録したい | Command | ActiveJob |
| ステータスによって振る舞いが変わる | State | AASM gem |
| 引数が多く、オブジェクト構築が複雑 | Builder | ActiveRecordクエリ |
| 別オブジェクトへのアクセスを制御したい | 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ヶ月かけて作り上げた注文システムの最終形を見ると、パターンが問題解決のために使われていた:
- 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行のクラス
endFacadeパターンと似ているが、決定的な違いがある。Facadeはサブシステムに処理を委ねる(薄いラッパー)のに対し、神クラスは自分で全部処理する(厚い実装)。
Facadeが正しい例:
class OrderCreationService # Facade
def call
validate # → OrderValidationChain に委ねる
charge # → PaymentGateway に委ねる
create # → Order.create! に委ねる
notify # → Notifications に委ねる
end
end2. 過剰な抽象化
# 必要のない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つを自問するようになった。
- 変更の理由は1つか?(単一責任原則)— このクラスを変更する理由が複数あれば、複数の責務を持っている
- テストを書けるか?(テスタビリティ)— テストが書きにくければ依存関係の問題がある
- 次に触れる人が理解できるか?(可読性)— 6ヶ月後の自分が読んでわかるか?
パターンはこの3つを達成するための手段だ。
この旅で学んだこと
ケンタが最初にコードレビューで「匂い」と言われてから半年。彼が学んだのはパターンの名前だけじゃなかった。
入社してすぐの頃、ケンタはコードを「動けばいい」と思っていた。テストを書かず、変数名は a や tmp、100行のメソッドに全部書いていた。
山田さんの最初のレビューコメントを今でも覚えている。
「動くコードを書くことは最低限。次に読む人(多くの場合、未来の自分)が理解できるコードを書くことが本当の仕事だ。」
最初は反発した。「動けばいいじゃないですか」と心の中で思っていた。でも半年で変わった。
自分が1ヶ月前に書いたコードを読んで、「これを書いたのは誰だ」と思うことがあった。コメントがない、変数名が不明瞭、条件分岐が5重になっている。それが自分が書いたコードだと気づいて、恥ずかしくなった。
「コードは一度書いて終わりじゃない。次の人(そして未来の自分)が読んで、変更できることが大事だ。」
デザインパターンは、それを助けるための先人の知恵だ。全部を覚える必要はない。問題に直面したとき、「こんな問題、先人たちはどう解決したのかな?」と考えるきっかけになれば十分だ。
ユウキのコードレビューを終えて、ケンタはGitHubにコメントを書いた。
「パターンの理解度が高い!各パターンの使い方は正しい。一つアドバイスすると、まずシンプルに書いて、複雑になったら段階的にパターンを適用してみよう。今回の注文完了処理は、まずFacadeだけで十分だと思う。良いコードは後から読む人のためにある。次のPRを楽しみにしています。」
山田さんが最初に言っていたことを、今度はケンタが伝える番だった。
各章で学んだパターンの一覧
| # | パターン | 解決する問題 | Railsでの代表例 |
|---|---|---|---|
| 2 | Strategy | 条件分岐の交換可能化 | 配送料・割引計算の切り替え |
| 3 | Observer | 変更の通知と疎結合 | ActiveSupport::Notifications |
| 4 | Factory | オブジェクト生成の抽象化 | FactoryBot |
| 5 | Singleton | 唯一のインスタンス保証 | Rails.application |
| 6 | Adapter | インターフェース変換 | ActiveStorage |
| 7 | Decorator | 機能の動的追加 | Draper gem |
| 8 | Facade | 複雑さの隠蔽 | サービスオブジェクト |
| 9 | Template Method | 処理の骨格の共通化 | ApplicationController |
| 10 | Command | 操作のオブジェクト化 | ActiveJob |
| 11 | State | 状態ごとの振る舞い管理 | AASM gem |
| 12 | Builder | 複雑なオブジェクトの段階的構築 | ActiveRecordクエリ |
| 13 | Proxy | アクセスの制御と代理 | キャッシュ・CloudFront |
| 14 | Chain 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か——でもそれはまた別の話だ。