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.
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):
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:
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.
Draper also works with collections through the decorator's decorate_collection
class method or the decorate
method on a collection of objects.
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.
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.
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.
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.
The controller is something like this:
Of course, another view, within the cart display (for example), would make use of an address partial:
A decorator can make things cleaner.
The controller then looks like the following:
And the view is simpler.
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!