簡易ValidationのようなPolicy Object用RubyGemを作った話し
Policy Objectパターン用のDSLを提供するRubyGemを作りました。
簡易的なActiveModel::Validationのように使え、ポリシー違反をメッセージとして取得できる軽量なGemです。
- RubyGems.org policy_check
- GitHub hazi/policy_check
元々はRails初心者の時に「ActiveModel::Validation的な感じで、save時にブロックしなくて必要な時にだけ正当性を検証できて、エラーメッセージが取得できるものが欲しい。」という需要が始まりでした。
その後、「それってPolicy Objectっていうらしいぞ?」「でもValidationみたいに書きたい…」「なんとなく自作したけど使いづらい」と何年か格闘した結果今回のGemに行きつきました。
使い方
ここでいうPolicy Objectとは下記のようなクラスを指します。
class UserPolicy
def initialize(user)
@user = user
end
def administrator_account_name?
user.sign_in_count > 0 && user.role == "admin"
end
def use_email_as_name?
user.full_name.blank? && user.email.present?
end
private
attr_reader :user
end
参考、引用元: Rails tips: Policy Objectパターンでリファクタリング(翻訳)|TechRacho by BPS株式会社
単なるPolicy Objectであればこのままでいいのですが、
- 何がポリシー違反だったのかを知りたい
- ちょっとしたポリシーは元のクラスに直接書きたい
- 大量の条件があるとメソッドが異様に長くなる
- 大量の条件を分割するためにメソッド名を考えるのが辛い
という問題を解決します。
上記の UserPolicy
をPolicyCheckを使って書き直すと下記のようになります。
class UserPolicy
extend PolicyCheck
def initialize(user)
@user = user
end
policy :administrator_account_name do
error("このアカウントはまだ使用されてません") { user.sign_in_count = 0 }
error("管理者アカウントではありません") { user.role != "admin" }
end
policy :use_email_as_name do
error("ユーザー名が設定されています") { user.full_name.present? }
error("メールアドレスが設定されていません") { user.email.blank? }
end
private
attr_reader :user
end
user_policy = UserPolicy.new(user)
user_policy.administrator_account_name? #=> false
user_policy.use_email_as_name? #=> true
user_policy.administrator_account_name_errors #=> ["管理者アカウントではありません"]
基本的にはActive Recordなどに直接書くとFat Model化してしまうので推奨はされませんが、それも想定して作っています。
class User < ApplicationRecord
extend PolicyCheck
policy :administrator do
error("このアカウントはまだ使用されてません") { sign_in_count = 0 }
error("管理者アカウントではありません") { role != "admin" }
end
end
User.find(1).administrator? #=> true
また、無名policyを使うとシンプルなPolicy Objectを実装できます。
class AdminUserPolicy
extend PolicyCheck
def initialize(user)
@user = user
end
policy do
error("このアカウントはまだ使用されてません") { user.sign_in_count = 0 }
error("管理者アカウントではありません") { user.role != "admin" }
end
private
attr_reader :user
end
admin_policy = AdminUserPolicy.new(user)
admin_policy.valid? #=> false
admin_policy.invalid? #=> true
admin_policy.error_messages #=> ["管理者アカウントではありません"]
内部実装
PolicyCheckを作る前に似たようなgemを作ってみたのですが、多数のメソッド、変数を使っており気軽にinclude/extendしたくないなという作りになってしまいました。
なので、今回はそういったことが起きないように必要最低限のメソッドが作られるように、Module Builderパターンを使ってextend PolicyCheck
を行っても .policy
クラスメソッドが作られるだけで、クラス変数やインスタンス変数は作られないようにしました。
policy :xxx
を実行した時に初めて #xxx?
#xxx_errors
インスタンスメソッドが作られます。
#error
で定義した内容はどこに行くかというと、blockの状態で各メソッドに保存されています。
実装としてはこの辺りになります。
今後に関して
#error_messages
はデバッグの際にも役立ちますが、フロントに表示するエラーメッセージとして使用することもあると思うので、需要がありそうだったらRuby I18nを使ったローカライズにも簡単に対応できるようにしようかなとは思っています。
その他何かアイディアがあれば是非コメントしてください。
基本的にはあまり高機能にしない方が需要にフィットしそうだなとは思ってます。
類似Gem
類似gemとしてProcedureというものがあります。こちらは開発した後に存在を知ったのですが、procedure design patternとされており、Policy Objectよりだいぶ複雑なものを想定されてそうです。
参考: Rails procedure design pattern – Long Live Ruby
Link
- RubyGems.org policy_check
- GitHub hazi/policy_check
Discussion