DEV Community

Cover image for Keep your Ruby Code Maintainable with Draper
Thomas Riboulet for AppSignal

Posted on • Originally published at blog.appsignal.com

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.

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: ...>
Enter fullscreen mode Exit fullscreen mode

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):

# app/models/company.rb
class Company < ApplicationRecord
  def founded_on
    Date.new(1981, 9, 10)
  end
end
Enter fullscreen mode Exit fullscreen mode
# app/decorators/company_decorator.rb
class CompanyDecorator < SimpleDelegator
  def founding_year
    founded_on.year
  end
end
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode
# app/views/companies/show.html.erb

<h1><%= company.name %></h1>
<h2>Founded in <%= company.founding_year %></h2>
Enter fullscreen mode Exit fullscreen mode

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:

gem 'draper'
Enter fullscreen mode Exit fullscreen mode

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.

decorated_company = Company.find(params[:id]).decorate
# equivalent to
company = Company.find(params[:id])
decorated_company = CompanyDecorator.new(company)
Enter fullscreen mode Exit fullscreen mode

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

decorated_companies = CompanyDecorator.decorate_collection(filtered_companies)
# or
decorated_companies = Company.recent.decorate
Enter fullscreen mode Exit fullscreen mode

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.

class CompanyDecorator < Draper::Decorator
   def founding_year
    object.founded_on.year
  end
end
Enter fullscreen mode Exit fullscreen mode

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.

class CompanyDecorator < Draper::Decorator
  delegate :name

  def founding_year
    object.founded_on.year
  end
end
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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.

# 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>
Enter fullscreen mode Exit fullscreen mode

The controller is something like this:

class UsersController < ApplicationController
  def show
    user = User.find(params[:id])

    render :show, locals: { user: user }
  end
end
Enter fullscreen mode Exit fullscreen mode

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

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

A decorator can make things cleaner.

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
Enter fullscreen mode Exit fullscreen mode

The controller then looks like the following:

def show
  user = User.find(params[:id])

  render :show, locals: { user: user.decorate }
end
Enter fullscreen mode Exit fullscreen mode

And the view is simpler.

# 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>
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)