Note: I've updated my thoughts on Draper in this post
What Is The Decorator Pattern?
In object-oriented programming, the decorator pattern (also known as Wrapper, an alternative naming shared with the Adapter pattern) is a design pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class.[1] The decorator pattern is often useful for adhering to the Single Responsibility Principle, as it allows functionality to be divided between classes with unique areas of concern.
- Taken from Wikipedia
A decorator allows us to modify an object without affecting the other objects with the same class. This is particularly useful in cleaning up view logic and removing helpers.
Examples
Perhaps we've created a turn by turn zombie game. We need to display a dynamic message based on the health status of the zombie to the user so we create a helper method
# app/helpers/zombie_helper.rb
def health_status(zombie)
if zombie.is_decapitated
"Killed at #{zombie.decapitated_at.strftime('%A, %B %e')}"
esle
"Undead"
end
Issues
This helper will be included in all controllers and views by default.
It's possible to configure the app to only serve the helpers correlating to the current namespace but we lose the ability to use methods for other classes.
The methods is ambiguous. Consider we create a new model, class Human < ActiveRecord::Base
. This will be our avatar in the game. So we need a way to show the health_status for our Humans, but we already have a helper method named health_status. We could refactor the method to return the correct message based on the methods argument class def health_status(object)
this would make for a long, messy method. Or we could rename create a new method def human_health_status(human)
and rename the zombie helper def zombie_health_status(zombie)
, this is better but still doesn't fix the fact that all controllers/views can access all of these methods, the larger the application the bigger this issue becomes.
Using Draper to Decorate Our Views
There's a better way to do this, let's use Draper to refactor the zombies helper.
# app/decorators/zombie_decorator.rb
class ZombieDecorator < Draper::Decorator
def health_status
if object.is_decapitated?
"Killed at #{decapitated_at}"
esle
"Undead"
end
end
def decapitated_at
object.decapitated_at.strftime("%A, %B %e")
end
end
Now we can control the initialization of these extra methods, simply call .decorate on the record
# app/controllers/zombies_controller.rb
def show
@zombie = Zombie.find(params[:id]).decorate
end
Now in the views we call the method on the record object itself, giving more context to the method
<%= @zombie.health_status %>
Ever seen this in an app?
# app/views/articles/show.html.erb
<% if user_signed_in? %>
<%= link_to 'Read more', article_path(@article) %>
<% else %>
<%= link_to 'Read more', sign_in_path %>
<% end %>
We can use the same pattern to refactor this logic out of the view.
# app/decorators/article_decorator.rb
def read_more_link
if h.user_signed_in?
h.link_to 'Read more', article_path(object)
else
h.link_to 'Read more', sign_in_path
end
end
# app/views/articles/show.html.erb
<%= @article.read_more_link %>
Further Reading
Evaluating Alternative Decorator Implementations In Ruby - by Dan Croak at Thoughtbot
Decorators Compared To Strategies, Composites, and Presenters - by Dan Croak at Thoughtbot
Design Patterns - by the gang of four
Draper Railscast - by Ryan Bates at Railscasts