How do I Rails… ? Use a decorator design pattern in ruby to make development easier

Loi V Tran
4 min readDec 31, 2017

--

In part #1, we saw that nil values can collect in our view for many reasons.

Today, we’re going to use the ruby gem Draper in order to implement a Decorator design pattern in our Ruby 2.4.0 & Rails 5.1.4 application.

Using this pattern will make developing the front end easier; 5 lines in 3 files of refactoring.

A decorator inherits methods from the object it wraps, adding additional methods we implement without effecting the underlying object. The one we’ll add today concerns presentational logic.

What?

In other words, decorators wraps, or as I like to think of it, own a ruby object:

Rails console
# Two ways to call2.4.0 :010 > user = User.first.decorate
=> #<UserDecorator:0x007fd10659bb98 @object=#<User id: 1, name: "Loi V Tran", email: "loi@coderschool.vn", ...)
2.4.0 :012 > user = UserDecorator.new(User.first)
=> #<UserDecorator:0x007fd103366128 @object=#<User id: 1, name: "Loi V Tran", email: "loi@coderschool.vn", ...)

and are concerned with, among other things, the presentation of our app to end users. Let’s see how.

First, add the Draper gem to our Gemfile.

# Gemfilegem 'draper'

Then:

bundle install

When that's complete, run:

rails g decorator User

This will generate a new file for us at

# app/decorators/user.rbclass UserDecorator < Draper::Decorator
delegate_all
end

Now, we can refactor & fix our front end bug with 5 lines of code in 3 files.

The first two are our changes to the controller & view respectively:

  1. Controller:
# app/controllers/users_controller.rbdef index
@users = User.all
end

becomes:

def index
@users = User.all.decorate
end

2. View:

# app/views/index.html.erb<% @users.each do |other_user| %>
...
# Displaying location requires 2 method calls <ul>
<li>Position: <%= other_user.position %></li>
<li>School: <%= other_user.school %></li>
<li>Location: <%= other_user.city + ', ' + other_user.state %></li>
</ul>
...
<% end %>

becomes:

# app/views/index.html.erb
<% @users.each do |other_user| %>
...
# Displaying location requires 1 method call<ul>
<li>Position: <%= other_user.position %></li>
<li>School: <%= other_user.school %></li>
<li>Location: <%= other_user.location %></li>
</ul>
...
<% end %>

Lastly, we need to use implement the location method we just called in the view.

# app/decorators/user.rbclass UserDecorator < Draper::Decorator  delegate_all  def location
return object.city + ', ' + object.state if object.city && object.state
return object.city if object.city
'Unlisted'
end
end

Now, when we’re in our view, we’re no longer calling methods on a User object, but rather, a decorator object. Let’s prove it using pry to stop the server.

original:

# Remember@users = [User, User, User, ...]

other_user.class == User

new:

@users = [UserDecorator, UserDecorator, UserDecorator, ...]

other_user.class == UserDecorator.

What is object though…?

after this, when we run other_user.location

<ul>
<li>Position: <%= other_user.position %></li>
<li>School: <%= other_user.school %></li>
<li>Location: <%= other_user.location %></li>
</ul>

The method location we define in the UserDecorator is called:

# app/decorators/user_decorator.rbclass UserDecorator < Draper::Decorator
delegate_all

def location
return object.city + ', ' + object.state if object.city && object.state
return object.city if object.city
'Unlisted'
end
end

Resulting in us having fixed the bug we found in the front end.

Using a decorator design pattern in ruby can make our lives easier in several ways. Here are a few.

1. We don’t implement hard to maintain conditional logic in the front end to work around nil values.

2. We don’t add logic to our ActiveRecord User model, because this behavior doesn’t concern our model, but rather the presentation of our object to the end user.

3. We avoid helper methods, instead, recycling the logic to a decorator using a design pattern which can be used again later. This also helps us from polluting the global namespace the way a helper method would.

Thanks for reading!

Stay tuned in for more.

Like or comment to be notified for part 3 where we write tests using the RSpec 3.7 gem to have confidence our method behaves the way we expect it to.

--

--