Zenn
🔖

判定ロジックはPolicyパターンに集約

2025/03/27に公開

Daily Blogging96日目

判定ロジックがcontrollerにあるのが邪魔くさい
似通ったロジックも点在してる

そうだPolicyパターンを使おう

改訂新版 良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方

この本でも紹介されているPolicyパターン
これを使えば判定ロジックをスッキリさせることができそう

Policyパターンとは

本によると

判定条件(ルール)を部品化し、部品化した条件の組み合わせを可能にするしくみ

ルール :部品
ポリシー:組み合わせ

みたいな感じ

サンプルコードを作ってみる

異なるコンテンツタイプに対して、それぞれの購入可能判定ロジックが必要な場合

例えばこうできる?

ベースとなるPolicy

# コンテンツタイプに関わらず共通する処理
class BasePurchasePolicy
  def initialize(user, content)
    @user = user
    @content = content
    @rules = []

    add_common_rules
    setup_rules # サブクラスで固有ルールを追加
  end

  def add(rule)
    @rules << rule
  end

  def comply_with_all
    @rules.all?(&:ok)
  end

  private

  def add_common_rules
    add(UserActiveRule.new(@user))
    add(EnoughBalanceRule.new(@user, @content))
  end

  def setup_rules
    # サブクラスでオーバーライド
  end
end

ルールごとのクラス

class UserActiveRule
  def self.ok(user)
    user.active?
  end
end

class EnoughBalanceRule
  def self.ok(user, content)
    user.balance >= content.price
  end
end

class EbookLicenseRule
  def self.ok(user, ebook)
    !user.ebooks.include?(ebook) # すでに所有していないかを確認
  end
end

コンテンツごとのPolicy

# EBook用
class EbookPurchasePolicy < BasePurchasePolicy
  private

  def setup_rules
    add(EbookLicenseRule.new(@user, @content)) # 電子書籍特有のルール
  end
end

# Video用
class VideoPurchasePolicy < BasePurchasePolicy
  private

  def setup_rules
    add(RegionRestrictionRule.new(@user, @content)) # 動画特有のルール
  end
end

EBookを買う時はこういうコントローラにする

class EbooksController < ApplicationController
  # 必要なロジックは1つのbefore_actionに集約される!
  before_action :authorize_purchase, only: [:purchase]

  def purchase
    user = User.find(params[:user_id])
    ebook = Ebook.find(params[:id])

    # 購入処理
    user.update(balance: user.balance - ebook.price)
    render json: { message: '電子書籍の購入成功' }, status: :ok
  end

  private

  def authorize_purchase
    user = User.find(params[:user_id])
    ebook = Ebook.find(params[:id])

    policy = EbookPurchasePolicy.new(user, ebook)

    unless policy.comply_with_all
      render json: { message: '購入不可' }, status: :forbidden
    end
  end
end

メリット

それっぽいコードを作ってみた感想

  • controllerのメインロジックに集中できる
  • before_actionがスッキリする
  • controllerのテスト項目を減らせそう
    • 責務が分離されてる
  • ルールが変更になった時は、それぞれのPolicyクラスを修正するだけですむ

Discussion

ログインするとコメントできます