Design Patterns in Large Rails Applications: Policy Objects

iRonin IT Team - Experts in software development
rails, web, back-end development, design patterns

Using Policy Objects helps to keep your code clean, tight, and most of all, correct. Today’s blog post walks through Policy Object solutions used in a large Ruby on Rails application. Check it out!

In today’s post, we kick off where we left off last time, after explaining the use of the Query Object design pattern in a large Ruby on Rails application. Similar to the Query Objects outlined last time, Policy Objects are simple objects designed to provide a very specific functionality. Whereas Query Objects are responsible for database queries, Policy Objects are responsible for authorizing a given operation.

In refactoring a large Ruby on Rails application, we decided to implement Policy Objects to help keep our code tight and correct.

Example

CompanyPolicy = Struct.new(:auth_context, :company) do
  def update?
    active_company?
  end

  private

  def active_company?
    company.enabled && company.employees_count > 0
  end
end

This might look like a validator at first sight but the biggest difference between validators and policy objects is that validators will validate user input from the user while Policy Objects validate business rules.

You also don’t have to keep all authorization methods for specific records in a single Policy Class. You are free to create multiple Policy Objects (depending on the context they operate on):

CompanyBilingPolicy = Struct.new(:auth_context, :company) do
  def purchase_credits?(company)
    ...
  end
end

CompanyMailingPolicy = Struct.new(:auth_context, :company) do
  def send_newsletter?(company)
    ...
  end
end

It’s also good to have a default scope for fetching records that are available in the current context:

CompanyPolicy = Struct.new(:auth_context, :company) do
  ...

  def self.scope(auth_context)
    user = auth_context.user
    Company.owned_by(user)
  end
end

This way you can easily list all companies available to a current user:

auth_context = OpenStruct.new(user: current_user)
CompanyPolicy.scope(auth_context)

Patterns like these are used by the pundit gem, which also contains additional methods to work with policies easier (i.e. authorizing actions in the controller, scoping records in the controller, etc.).

Policy object conventions

In one of our projects we decided to use the Policy suffix for class names instead of the Policy namespace.

We also follow a few additional rules when we create Policy Object classes:

  • Class name must be meaningful so you can tell what the given class is doing only by looking at its name
  • Class name should contain Policy suffix
  • Each public method in the class should return a boolean value without any side-effect
  • Each public method should follow conventions for predicate methods (ended by the question mark)

Here is an example of a class that follows such rules:

class CompanyPolicy
  def initialize(company)
    @company = company
  end

  def active_company?
    company.enabled && company.employees_count > 0
  end

  private
  attr_reader :company
end

Dynamic Policy Objects

Sometimes in more complex projects, it might be good to have dedicated policy classes for separate subsets of records (imagine you have a lot of companies and there are different policies depending on the industry the company operates in).

In such cases you can create a generic policy class which works like a factory for creating concrete classes, based on some configuration.

If you would like to automate it to the max you can override Ruby’s new method to return a concrete class based on some mapping (i.e. a look-up table):

class CompanyPolicy
  class << self
    alias :_new :new

    def inherited(subclass)
      class << subclass
        alias :new :_new
      end
    end

    def new(company)
      klass = find_class_for(company)
      klass.new(company)
    end

    private

    def find_class_for(company)
      # Find class for a company based on the industry
    end
  end
end

We simply override the new method in the base class and restore the original one in the subclasses.

A dedicated policy class needs to extend the CompanyPolicy and provide an initialize method:

class Internet::CompanyPolicy < Policy
  def initialize(company)
    @company = company
  end

  def pay_with_bitcoin?
    true
  end
end
class Finance::CompanyPolicy < Policy
  def initialize(company)
    @company = company
  end

  def pay_with_bitcoin?
    false
  end
end

With the following code you can initialize the proper policy by passing a company to the CompanyPolicy class:

> policy = CompanyPolicy.new(company_from_internet_industry)
# Internet::CompanyPolicy object

> policy = CompanyPolicy.new(company_from_finance_industry)
# Finance::CompanyPolicy object

Summary

Thanks to Policy Objects we can separate logic better and test our code more easily. Of course, like in the case of Query Objects, we can use Policy Objects with the Builder Pattern or Decorator Pattern but also with any other pattern.

Need help with refactoring your code to make it more efficient, more testable, and more elegant? We’ve got you. Let iRonin take a look at your codebase and help you to run things more smoothly and simply. Get in contact with us now to find out how we can help in your dev projects!

Author's Bio
iRonin IT Team

Experts in software development

We are a 100% remote team of software development experts, providing web & mobile application development and DevOps services for international clients.

Similar articles
Comments

Bulletproof your development with remote team augmentation

Read how
This page is best viewed in portrait mode
Our websites and web services use cookies. We use cookies and collected data to enhance your experience, provide additional communication channels, improve marketing materials and enhance our offer. IRONIN SP. Z O.O. SP. K. is committed to protecting all the data that we collect or process in any way, especially data of personal nature. By accepting these terms you agree to our usage of cookies and processing your data, according to our Privacy Policy, and you declare that your browser settings reflect your preferences. Read more You have the right to revoke this agreement at any time, based on the terms of our Privacy Policy. You can change cookies settings in your browser. If you do not agree with us using cookies and processing your data, please change your cookies settings in your web browser and reject these terms. You can find more information about cookies, your data privacy This site uses cookies. By continuing to browse the site, you are agreeing to our use of cookies. data processing, and your rights in our Privacy Policy.