ruby

Keep your Ruby Code Maintainable with Draper

Thomas Riboulet

Thomas Riboulet on

Keep your Ruby Code Maintainable with Draper

Design patterns can help to simplify your codebase so you don't need to reinvent the wheel.

In this post, we'll go into how to use Draper. But first, we will start with an overview of the decorator pattern and how to use it with Ruby's standard library.

Let's get started!

The Decorator Pattern for Ruby

According to Refactoring.Guru:

Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.

Decorator can also be called a wrapper. The idea is that you initialize an instance of a decorator class by passing the object you want to decorate to that decorator class initializer.

The new returned object can expose the same methods the original object did, plus a few more, or alter the original ones. In most cases (and in Draper's case), we use this pattern to delegate calls to the embedded object.

Let's dig into the Ruby standard library to look at the SimpleDelegator class, itself a child of the Delegator class. It's a great way to see the decorator's side of the pattern.

ruby
class Company def founded_on Date.new(1981, 9, 10) end end class CompanyDecorator < SimpleDelegator def founding_year founded_on.year end end decorated_company = CompanyDecorator.new(Company.new) decorated_company.founding_year #=> 1981 decorated_company.__getobj__ #=> #<Company: ...>

One thing to note: the decorated_company object has direct access to the public methods of the object it decorates.

And here lies the principle behind the decorator: the founded_on method, which can be an attribute pulled from the database, provides a key value for the company.

To display the year the company was founded, we might be tempted to use company.founded_on.year. This can work, yet we will slowly riddle the codebase with such calls. That is not practical; if we were to change the math or presentation of that value, we would need to change every single instance of that piece of code.

This kind of presentation code doesn't belong in the Company class; in Ruby on Rails, it doesn't belong in models either. If you have seen a few Rails projects, you probably have seen views littered with such presentation code. Decorators, delegators, or presenters are a better place for such presentation logic.

Using Decorators in Ruby on Rails Views, Models, and Controllers

Most, if not all, web applications need to display attributes of objects in different ways: names, dates, times, prices, numerical values, and coordinates. These uses are all over the place. Sometimes, we copy this kind of code in every view, use helper methods, or fill the model with methods that are just presenting data. Delegators are handy in moving all this logic into focused places.

Helper methods are usually spread around in different places. They might be grouped, but only carry a little meaning beyond the simple method's name. While models sound more appropriate, adding presentation methods tends to fatten them for little purpose. Any business logic in models or ActiveRecord-related code (associations, scopes, etc.) will be muddied by the presence of those methods.

In short, in Ruby on Rails, decorators allow us to keep responsibilities limited in the model, the view, the decorator, and the controller. Here is how it could work (note that we have simplified the code a bit to stay concise):

ruby
# app/models/company.rb class Company < ApplicationRecord def founded_on Date.new(1981, 9, 10) end end
ruby
# app/decorators/company_decorator.rb class CompanyDecorator < SimpleDelegator def founding_year founded_on.year end end
ruby
# app/controllers/companies_controller.rb class CompaniesController < ApplicationController def show company = Company.find(params[:id]) decorated_company = CompanyDecorator.new(company) render :show, locals: { company: decorated_company } end end
erb
# app/views/companies/show.html.erb <h1><%= company.name %></h1> <h2>Founded in <%= company.founding_year %></h2>

Using Draper in Your Ruby App

Draper is a bit more advanced than SimpleDelegator. To install Draper, simply add the gem to the application's Gemfile:

ruby
gem 'draper'

And run bundle install.

Decorators are usually written in the app/decorators folder; you can also use namespaces within that folder. Namespaces and the decorator's name should mirror the related models for the best results.

Draper includes a couple of Ruby on Rails generators too. So when generating resources (rails generate resource company), the related decorator will be created automatically.

If you prefer to do it by hand, you can also call the decorator generator with the model's name: rails generate decorator Company. By following this, you can rely on a bit of magic within your code to call decorate on the Company class instance and get the CompanyDecorator instance for that company.

ruby
decorated_company = Company.find(params[:id]).decorate # equivalent to company = Company.find(params[:id]) decorated_company = CompanyDecorator.new(company)

Draper also works with collections through the decorator's decorate_collection class method or the decorate method on a collection of objects.

ruby
decorated_companies = CompanyDecorator.decorate_collection(filtered_companies) # or decorated_companies = Company.recent.decorate

The decorator is similar to the one we saw with SimpleDelegator; it relies on Draper::Decorator instead. Methods of the decorated object are not directly accessible. Instead, we must call them through the object or model alias.

ruby
class CompanyDecorator < Draper::Decorator def founding_year object.founded_on.year end end

Then, if we want to directly call public methods of the decorated object with the decorator instance, we can define a list of such methods by using the delegate method.

ruby
class CompanyDecorator < Draper::Decorator delegate :name def founding_year object.founded_on.year end end

That way, we can use decorated_company.name. This approach is also handy to limit the exposure of the object that's being decorated from the view.

Example Draper Use Case: Rails View, Controller, and Model

A classic Draper use case relates to a User, Account, or Post model: they are often part of the core domain. The sheer quantity of business logic and presentation logic in those models can cause senior developers to frown and grab a new cup of coffee.

Let's take a User model use case: always a good magnet for plenty of excess fat.

ruby
class User < ApplicationRecord # table would have the following columns # # first_name, String # last_name, String # date_of_birth, DateTime # company_id, Uuid # street_name, String # street_number, String # city, String # zipcode, String # country, String def age DateTime.current.years_ago(date_of_birth.year).year # Note: this is Ruby on Rails specific end def company_name company.name end end

None of those methods add any value to the model. They are misplaced or purely related to the presentation of data.

The view might look something like the following.

erb
# app/models/users/show.html.erb <h1><%= "#{user.first_name} #{user.last_name}" %></h1> <ul> <li>Born <%= user.age %> years ago</li> <li>Address: <%= "#{user.street_number} #{user.street_name}" %></li> <li>City: <%= "#{user.zipcode} #{user.city} - #{user.country}" %></li> <li>Company: <%= user.company_name %></li> </ul>

The controller is something like this:

ruby
class UsersController < ApplicationController def show user = User.find(params[:id]) render :show, locals: { user: user } end end

Of course, another view, within the cart display (for example), would make use of an address partial:

erb
# app/views/carts/_address.html.erb <ul> <li><%= "#{user.street_number} #{user.street_name}" %> <li><%= "#{user.zipcode} #{user.city} - #{user.country}" %></li> </ul>

A decorator can make things cleaner.

ruby
class UserDecorator < Draper::Decorator delegate :company_name def age ((Time.now - object.date_of_birth.to_i) / 60 / 60 / 24 / 365).to_i end def address_line "#{object.street_number} #{object.street_name}" end def city_line "#{object.zipcode} #{object.city} - #{object.country}" end def name "#{object.first_name} #{object.last_name}" end end

The controller then looks like the following:

ruby
def show user = User.find(params[:id]) render :show, locals: { user: user.decorate } end

And the view is simpler.

erb
# app/models/users/show.html.erb <h1><%= user.name %></h1> <ul> <li>Born <%= user.age %> years ago</li> <li>Address: <%= user.street_line %></li> <li>City: <%= user.city_line %></li> <li>Company: <%= user.company_name %></li> </ul>

As you can see, it's not so different either.

Decorators: When Not to Use Them in Your Ruby on Rails App

Decorators can be overused and end up poisoning your code base. You might stuff too much in them, causing complex queries and an overhead. Decorators could also end up mixed in with helper methods and undecorated classes. All in all, you may overuse Draper.

Decorators, like any class, need to be focused and stick to one responsibility: presentation logic. If a method doesn't fit, maybe it belongs somewhere else. Complex queries might appear when the company_name method or something similar pulls data from a model and its associates.

A similar issue can emerge when you decorate too many objects at once. After all, you basically double the number of objects if you decorate a whole collection. So, be aware of this and decorate when needed.

Ruby on Rails developers tend to use a lot of helper methods. Some helper methods make sense but lack the object-oriented angle, in my opinion, and end up causing confusion. Relying on decorators will help limit the number of helper methods, but if you do use some, keep it light.

Testing Ruby code is almost a standard, it's definitely a good practice, and it also applies to Decorators. This will help, just like for any other class.

Preparing a Whole View with Decorator

There is a form of Decorator that is a bit more advanced: the view model, which can be done with Draper, SimpleDelegator, or even a plain old Ruby object (PORO). The concept is as follows: you create classes to prepare the data for a view from one or more objects. If we're talking just one object, we have something similar to what we have seen. If it's more than one, then it gets more crunchy.

The idea is to gather the whole responsibility of "data presentation" into one class, even for different objects at once. This is a case where we might cause complex queries, so don't hesitate to use includes and preload methods with queries to eager load associations as needed.

That way, instead of passing several objects to the view from the controller, you can prepare just one, and then access the data — ready to be used — from properly named methods.

Wrapping Up

In this post, we ran through the basics of Decorator and how to use it, before diving into how to use Decorators in Draper for a Ruby on Rails application. We finally touched on when to avoid using decorators and how to prepare a whole view with Decorator.

Draper is one of those gems you'll wish you had discovered before. It gives wings to Decorators. It's also the kind of nicely integrated library that will not only help you keep your code clean by setting clear limits between responsibilities, but also provide you with custom grammar and magic that fits the Ruby on Rails ecosystem like a glove.

Happy coding!

P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

Thomas Riboulet

Thomas Riboulet

Our guest author Thomas is a Consultant Backend and Cloud Infrastructure Engineer based in France. For over 13 years, he has worked with startups and companies to scale their teams, products, and infrastructure. He has also been published several times in France's GNU/Linux magazine and on his blog.

All articles by Thomas Riboulet

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps