Rails: Policy Objects Implementation
Tags: ruby, rails, designpattern, cleancode
Published: 2020-01-02 07:17:42

*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](https://medium.com/@steverob/policy-object-d158495265fa)

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](#chapter-1)
[2. Moving Policy Logic to Model](#chapter-2)
[3. Create a separate class](#chapter-3)
[4. Conclusion](#chapter-4)
<a name="chapter-1"></a>
##1. Problem
Let say we have this kind of controller:
```ruby
# 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.
<a name="chapter-2"></a>
##2. Moving Policy Logic to Model
Well, we can move the logic to our model. So, it become like this:
```ruby
# 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!
<a name="chapter-3"></a>
##3. Create a separate class
Now, we create a class:
```ruby
# 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?
```ruby
# 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
```
<a name="chapter-4"></a>
##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