Policy object pattern
published on NOV 1, 2018
Policy objects are primitive Ruby objects, used for checking operations isolation. I personally love this pattern, however, there are some rules that we should stick to in order to name given object as a policy object.
Policy object rules
- Method name always ends with a quote mark
- Method returns
- We don’t modify passed attributes
- Code cover only simple read logic, no database calls etc.
Let’s build now a sample class which will be used later to implement policy object:
class UserService def initialize(user) @user = user end def name if user.full_name.blank? && user.email.present? user.email else user.full_name end end def account_name if user.sign_in_count > 0 && user.role == "admin" "Administrator" else "User" end end private attr_reader :user end
We can easily divide our class into two types of actions: reading and checking. Checking part is perfect code for policy objects. Since we are operating on a
User object we can create one policy object for both methods:
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
after implementing UserPolicy in our
UserService service things look much cleaner right now:
class UserService def initialize(user) @user = user end def name user_policy.use_email_as_name? ? user.email : user.full_name end def account_name user_policy.administrator_account_name? ? "Administrator" : "User" end private attr_reader :user def user_policy @_user_policy ||= UserPolicy.new(user) end end
Policy logic is now separated from reading part and it’s easier to test
UserService class because we can simply stub
UserPolicy. However, it’s not the end of our refactoring process. We can apply modified explaining variable pattern to our policy object. Instead of moving logic to variables we would move logic directly to smaller methods with meaningful names:
class UserPolicy def initialize(user) @user = user end def administrator_account_name? user_signed_in? && user_is_administrator? end def use_email_as_name? user_does_not_have_full_name? && user_has_email? end private attr_reader :user def user_does_not_have_full_name? user.full_name.blank? end def user_has_email? user.email.present? end def user_signed_in? user.sign_in_count > 0 end def user_is_administrator? user.role == "admin" end end
The class is now longer but it’s self-explainable.