Zenn
🎃

Policyパターン適用しようと思ったらちょっと苦戦した話

2025/03/29に公開

Daily Blogging98日目

before_action整理用にPolicyパターン使ってみたけど、エラー処理で苦戦した

なんとなく考えてた改善案↓
判定ロジックはPolicyパターンに集約

問題点

元々のcontrollerには、before_actionが多くてアクションの処理に集中できない問題があった
こんな感じ

before_action :set_xxx
before_action :authentication
before_action :check_xxx_1
before_action :check_xxx_2
before_action :check_xxx_3
before_action :check_xxx_4
.
.
.

def create
end

private

def set_xxx
# インスタンス変数をセット
end

def check_xxx_1
# 条件を満たさない場合は、bad_request
end

def check_xxx_2
# 条件を満たさない場合は、forbidden
end

def check_xxx_3
# 条件を満たさない場合は、not_found
end

def check_xxx_4
# 条件を満たさない場合は、bad_request
end

エラー時のstatusが共通じゃない...

最初の改善案だと、policy.comply_with_allで一括判定しようとしてる。
でも全ての条件を満たしているのか、そうでないかしかわからないのでエラー時のstatusだし分けができない

最終的にこうした

Policyクラスにerrorを持たせて、条件をPASSできなかったらそれに紐づくエラー内容を格納する。

class BasePolicy
  attr_reader :error

  def initialize(**_args)
    @error = nil
  end

  def add(rule)
    @rules << rule
  end

  def comply_with_all
    failed_rule = @rules.find{ |rule| !rule.ok? }
    return true if failed_rule.nil?

    @error = failed_rule.error
    false
  end

  private

  # サブクラスでオーバーライド
  def setup_rules; end
end
class XXXPolicy < BasePolicy

  def initialize(arg)
    @arg = arg
    setup_rules
  end

  private

  def setup_rules
    add(XXXRule.new(arg))
    add(YYYRule.new(arg))
  end
end

各ルールに紐づくエラー内容は個別に定義しておく

class BaseRule
  # MEMO: サブクラスでオーバーライドする
  def ok?
    false
  end

  # MEMO: サブクラスでオーバーライドする
  def error
    {
      code: :internal_server_error,
      status: :internal_server_error,
    }
  end
end

ルールの例

class XXXRule < BaseRule
  def initialize(arg)
    @arg = arg
  end

  def ok?
    # 判定ロジック
  end

  def error
    {
      code: :bad_request,
      status: :bad_request,
    }
  end
end

class YYYRule < BaseRule
  def initialize(arg)
    @arg = arg
  end

  def ok?
    # 判定ロジック
  end

  def error
    {
      code: :not_found,
      status: :not_found,
    }
  end
end

コントローラではこうなる

.
.
.
def check_xxx
    policy = XXXPolicy.new(必要な引数渡す)

    return if policy.comply_with_all

    render_error_response(codes: [policy.error[:code]], status: policy.error[:status])
end

これでPolicyパターンで判定ロジックを集約しつつ、controllerをスリム化できた

Discussion

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