Stage 4: デザインパターン — GoFパターンの活用
パターンは「設計の語彙」
「デザインパターンって、23個全部覚えないといけないですか?」ヒロシは心配そうに聞いた。
マイは笑った。「全部覚える必要はない。パターンは共通の設計問題への名前のついた解法。名前を知ることで、チーム間のコミュニケーションが楽になる。」
「『ここはStrategyパターンで』と言えば、実装の詳細を説明しなくていい。設計の語彙を持つことで、複雑な問題をシンプルに表現できる。」
今日はGoFパターンの中から、Railsで特によく使われる3つを深く学ぶ。
Strategyパターン: アルゴリズムを交換可能にする
「まずStrategyから。これは一番使う頻度が高い。」
問題: 割引計算のロジックが増えるたびにif文が増えていく。
# パターンを知らないと書いてしまうコード
class PriceCalculator
def calculate(order, discount_type)
base_price = order.items.sum(&:price)
case discount_type
when :none then base_price
when :member then base_price * 0.9
when :vip then base_price * 0.8
when :bulk then base_price > 50_000 ? base_price * 0.7 : base_price
when :seasonal then base_price - 1_000
# 新しい割引のたびにここを修正しなければならない
end
end
endStrategyパターンで解決:
# 各割引戦略をクラスに切り出す
module DiscountStrategy
class None
def apply(price)
price
end
end
class Member
def apply(price)
price * 0.9
end
end
class Vip
def apply(price)
price * 0.8
end
end
class Bulk
THRESHOLD = 50_000
DISCOUNT_RATE = 0.7
def apply(price)
price > THRESHOLD ? price * DISCOUNT_RATE : price
end
end
class Seasonal
def initialize(discount_amount)
@discount_amount = discount_amount
end
def apply(price)
[price - @discount_amount, 0].max
end
end
end
# PriceCalculatorはどの戦略かを知らなくていい
class PriceCalculator
def initialize(discount_strategy = DiscountStrategy::None.new)
@discount_strategy = discount_strategy
end
def calculate(order)
base_price = order.items.sum(&:price)
@discount_strategy.apply(base_price)
end
end
# 使い方
calculator = PriceCalculator.new(DiscountStrategy::Vip.new)
calculator.calculate(order)
# テストも簡単
RSpec.describe PriceCalculator do
it "VIP割引を適用する" do
strategy = DiscountStrategy::Vip.new
calc = PriceCalculator.new(strategy)
order = build(:order, items: [build(:item, price: 10_000)])
expect(calc.calculate(order)).to eq(8_000)
end
endINFO
RailsでのStrategyパターンの活用場所
- 支払い方法(Stripe/PayPal/銀行振込)
- 通知チャンネル(Email/SMS/Push)
- エクスポート形式(PDF/CSV/Excel)
- 検索アルゴリズム(全文検索/タグ検索/地理検索)
Observerパターン: イベントと反応を分離する
「次はObserver。Railsのcallbackと深く関係している。」
問題: ユーザー登録時に「メール送信・Slack通知・ポイント付与」など複数の処理が必要。全部Userモデルに書くとカオスになる。
# 悪い例: モデルに副作用を詰め込む
class User < ApplicationRecord
after_create :send_welcome_email, :notify_slack, :grant_signup_points
private
def send_welcome_email
UserMailer.welcome(self).deliver_later
end
def notify_slack
SlackNotifier.notify("#signups", "新規登録: #{email}")
end
def grant_signup_points
points.create!(amount: 100, reason: "signup")
end
end「テストするたびにSlackに通知が飛ぶ。ユーザー登録とは無関係な処理がモデルに縛り付けられている。」
Observerパターン(ActiveSupport::Notificationsで実装):
# app/models/user.rb — モデルはシンプルに
class User < ApplicationRecord
after_create :publish_registered_event
private
def publish_registered_event
ActiveSupport::Notifications.instrument("user.registered", user: self)
end
end
# app/observers/user_welcome_observer.rb
class UserWelcomeObserver
def self.subscribe!
ActiveSupport::Notifications.subscribe("user.registered") do |_name, _start, _finish, _id, payload|
user = payload[:user]
UserMailer.welcome(user).deliver_later
end
end
end
# app/observers/user_slack_observer.rb
class UserSlackObserver
def self.subscribe!
ActiveSupport::Notifications.subscribe("user.registered") do |_name, _start, _finish, _id, payload|
user = payload[:user]
SlackNotifier.notify("#signups", "新規登録: #{user.email}")
end
end
end
# app/observers/user_points_observer.rb
class UserPointsObserver
def self.subscribe!
ActiveSupport::Notifications.subscribe("user.registered") do |_name, _start, _finish, _id, payload|
user = payload[:user]
user.points.create!(amount: 100, reason: "signup")
end
end
end
# config/initializers/observers.rb
UserWelcomeObserver.subscribe!
UserSlackObserver.subscribe!
UserPointsObserver.subscribe!「Userモデルはuser.registeredイベントを発火するだけ。何が起きるかは知らない。関心の分離ができた。」
WARNING
ActiveRecord Callbackの罠
Railsのafter_createは便利ですが、ビジネスロジックを詰め込むと「テストしにくいモデル」ができます。Observerパターンを使い、副作用をモデルから切り離しましょう。
Factoryパターン: オブジェクト生成を抽象化する
「最後はFactory。オブジェクトの生成ロジックが複雑になったときの解法。」
問題: 決済プロバイダーを設定によって切り替えたい。
# 悪い例: 使う側が生成ロジックを知っている
class PaymentController < ApplicationController
def create
gateway = if Rails.env.production?
if ENV["PAYMENT_PROVIDER"] == "stripe"
StripeGateway.new(api_key: ENV["STRIPE_API_KEY"])
elsif ENV["PAYMENT_PROVIDER"] == "paypal"
PaypalGateway.new(client_id: ENV["PAYPAL_CLIENT_ID"], secret: ENV["PAYPAL_SECRET"])
end
else
FakeGateway.new
end
gateway.charge(params[:amount], params[:token])
end
endFactoryパターンで解決:
# app/factories/payment_gateway_factory.rb
class PaymentGatewayFactory
GATEWAYS = {
"stripe" => -> { StripeGateway.new(api_key: ENV.fetch("STRIPE_API_KEY")) },
"paypal" => -> {
PaypalGateway.new(
client_id: ENV.fetch("PAYPAL_CLIENT_ID"),
secret: ENV.fetch("PAYPAL_SECRET")
)
}
}.freeze
def self.create(provider = ENV.fetch("PAYMENT_PROVIDER", "stripe"))
return FakeGateway.new unless Rails.env.production?
factory = GATEWAYS[provider]
raise ArgumentError, "未知のプロバイダー: #{provider}" unless factory
factory.call
end
end
# app/controllers/payment_controller.rb
class PaymentController < ApplicationController
def create
gateway = PaymentGatewayFactory.create
result = gateway.charge(params[:amount], params[:token])
render json: result
end
end
# テストでも簡単にモック可能
RSpec.describe PaymentController do
before do
allow(PaymentGatewayFactory).to receive(:create).and_return(FakeGateway.new)
end
endAbstract Factory: 関連するオブジェクト群を生成
# 通知システム全体を環境に応じて切り替える
module NotificationFactory
def self.create_suite(env = Rails.env)
case env.to_s
when "production"
{
email: SesEmailNotifier.new,
sms: TwilioSmsNotifier.new,
push: FcmPushNotifier.new
}
when "test"
{
email: FakeEmailNotifier.new,
sms: FakeSmsNotifier.new,
push: FakePushNotifier.new
}
else
{
email: LogEmailNotifier.new,
sms: LogSmsNotifier.new,
push: LogPushNotifier.new
}
end
end
end
# AWS SES を使ったEmailNotifier
class SesEmailNotifier
def initialize
@client = Aws::SES::Client.new(region: "ap-northeast-1")
end
def notify(user, message)
@client.send_email(
destination: { to_addresses: [user.email] },
message: {
subject: { data: "お知らせ" },
body: { text: { data: message } }
},
source: "noreply@example.com"
)
end
endパターンの使いどころ
「パターンを使いすぎるのも問題。」マイが注意した。
「YAGNIという原則がある。You Aren't Gonna Need It。今必要じゃないものは作らない。」
| パターン | 使うとき |
|---|---|
| Strategy | アルゴリズムを実行時に切り替えたい |
| Observer | 一つのイベントに複数の反応が必要 |
| Factory | オブジェクト生成ロジックが複雑または環境依存 |
| Decorator | 既存クラスに機能を動的に追加したい |
| Template Method | 処理の骨格を固定し、詳細をサブクラスに委ねたい |
AWSでのパターン活用
# CloudFormationでのFactory的な設計
# 環境(dev/staging/prod)に応じて構成を切り替える
Parameters:
Environment:
Type: String
AllowedValues: [dev, staging, prod]
Conditions:
IsProduction: !Equals [!Ref Environment, prod]
Resources:
Database:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceClass: !If [IsProduction, db.r6g.xlarge, db.t3.micro]
MultiAZ: !If [IsProduction, true, false]Stage 4 のまとめ
ヒロシは今日学んだパターンを振り返った。
「パターンって、ゼロから考えなくていいんですね。先人が解決した問題への答えが、名前付きで提供されている。」
「そう。そして名前があることで、チームで話せる。『ここはObserverにしよう』と言えば、一発で意図が伝わる。」
| パターン | 問題 | 解決 |
|---|---|---|
| Strategy | アルゴリズムのif地獄 | 戦略を交換可能なオブジェクトに |
| Observer | モデルへの副作用詰め込み | イベントと反応を分離 |
| Factory | 複雑な生成ロジック | 生成の責務をFactoryに集約 |
「次はアーキテクチャ原則。クラス設計からコンポーネント設計へ、スケールを上げる。」
デザインパターンという語彙を手に入れたヒロシは、コードリーティングが楽しくなってきた気がした。