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)