loading...

Rails: Policy Objects Implementation

kputra profile image K Putra ・3 min read

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

Posted on by:

kputra profile

K Putra

@kputra

Backend developer. Code from 2018. Rubyist. Sorry for bad english.

Discussion

pic
Editor guide