See all articles

Design Patterns in Large Rails Applications

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

1 2 3 4 5 6 7 8 9 10 11 12 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

1 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

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

auth_context = OpenStruct.new(user: current_user)

CompanyPolicy.scope(auth_context)

1 2 3 4 5 6 7 8 9 Patterns like these are used by the [pundit](https://github.com/elabs/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 `P``olicy` 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

1 2 3 4 ## 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

1 2 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

1

class Finance::CompanyPolicy < Policy

def initialize(company)

@company = company

end

def pay_with_bitcoin?

false

end

end

1 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

Similar articles