If you continue to read this article, I assume that you know Ruby, OOP in Ruby, and RoR.
What is Policy Object Design Pattern?
Policy Objects encapsulate and express a single business rule.
In our applications we might have different business rules coded mostly as if-else or switch statements. These rules represent concepts in your domain like whether “a customer is eligible for a discount” or whether “an email is supposed to be sent or not” or even whether “a player should be awarded a point”.
source: this article
Let's start our journey! (I use Rails API-only as example, but this article can be implemented in normal Rails as well)
Table of Contents:
1. Problem
2. Moving Policy Logic to Model
3. Create a separate class
4. Conclusion
1. Problem
Let say we have this kind of controller:
# app/controllers/discounts_controller.rb class DiscountsController < ApplicationController def create if can_user_get_discount? code = GenerateDiscountVoucherCode.new(@current_user.id).call render json: { status: "OK", message: "Your Discount Voucher Code: #{code}" }, status: 201 else render json: { status: "Failed", message: "You are not allowed to get Discount Voucher" }, status: 422 end end private def can_user_get_discount? is_premium? && last_discount_more_than_10_days_ago? && high_buyer? end def is_premium? @current_user.premium? end def last_discount_more_than_10_days_ago? @current_user.last_discount_sent_at < ten_days_ago end def ten_days_ago Time.now - 10.days end def high_buyer? @current_user.total_purchase_this_month > 5_000 end end
You put all the policy logic (business rule) in your controller. This is not good. Especially if you have many/complex policy logic.
Btw, you can ignore GenerateDiscountVoucherCode
class. We just need to know that this class is the one that responsible for generating the discount voucher code.
2. Moving Policy Logic to Model
Well, we can move the logic to our model. So, it become like this:
# app/controllers/discounts_controller.rb class DiscountsController < ApplicationController def create if @current_user.can_get_discount? code = GenerateDiscountVoucherCode.new(@current_user.id).call render json: { status: "OK", message: "Your Discount Voucher Code: #{code}" }, status: 201 else render json: { status: "Failed", message: "You are not allowed to get Discount Voucher" }, status: 422 end end end # app/models/user.rb class User < ApplicationRecord enum membership: ['regular', 'premium'] MINIMUM_PURCHASE = 5_000 def can_get_discount? self.premium? && self.last_discount_more_than_10_days_ago? && self.high_buyer? end def last_discount_more_than_10_days_ago? self.last_discount_sent_at < ten_days_ago end def ten_days_ago Time.now - 10.days end def high_buyer? self.total_purchase_this_month > MINIMUM_PURCHASE end end
Still not good. Now we just bloat our model. Let's implement Policy Object Design Pattern!
3. Create a separate class
Now, we create a class:
# app/lib/discount_voucher_policy.rb class DiscountVoucherPolicy MINIMUM_PURCHASE = 5_000 def initialize(user) @user = user end def allowed? is_premium? && last_discount_more_than_10_days_ago? && high_buyer? end private def is_premium? @user.premium? end def last_discount_more_than_10_days_ago? @user.last_discount_sent_at < ten_days_ago end def ten_days_ago Time.now - 10.days end def high_buyer? @user.total_purchase_this_month > MINIMUM_PURCHASE end end
Now, what are our controller and model looks like?
# app/controllers/discounts_controller.rb class DiscountsController < ApplicationController def create if policy.allowed? code = GenerateDiscountVoucherCode.new(@current_user.id).call render json: { status: "OK", message: "Your Discount Voucher Code: #{code}" }, status: 201 else render json: { status: "Failed", message: "You are not allowed to get Discount Voucher" }, status: 422 end end private def policy DiscountVoucherPolicy.new(@current_user) end end # app/models/user.rb class User < ApplicationRecord enum membership: ['regular', 'premium'] end
4. Conclusion
Compared to our first controller, this is "just" putting our policy logic to a separate class. It is true, as our main intention is moving our policy logic out of controller and model.
Policy Objects encapsulate and express a single business rule.
You'll find this design pattern useful if you have complex policy logic. Example I gave just a simple policy logic.
source: myself
Top comments (0)