Draper is a handy new gem for extracting logic normally held in helpers and views into something Ruby developers love: objects! Helpers are a great means to an end, but most of the time, our helpers accept arguments, a great sign that the method is procedural instead of being a method on the object we’re passing.
In my Intro to Test-Driven Rails workshop (which I’m holding September 17-18 in San Francisco and September 24-25 in Boston), one of the apps we build is a simple todo tracker. I decided to try out Draper in the codebase and see how things turned out; let’s dig in to some code and see what it can do!
Here’s the helper in question:
# app/helpers/todos_helper.rb
module TodosHelper
def completion_link(todo)
if todo.completed_at?
link_to 'Incomplete', todo_completion_path(todo), method: :delete
else
link_to 'Complete', todo_completion_path(todo), method: :post
end
end
def todo_state(todo)
if todo.completed_at
'complete'
else
''
end
end
end
Fairly straightforward: we have completion_link, which either POSTs or
DELETEs to /todos/:id/completion (adding or removing the timestamp of
completed_at), and todo_state, which generates an HTML class for us.
The view shouldn’t be much of a surprise:
<%= link_to 'Create a Todo', new_todo_path %>
<ul id='my-todos'>
<% @todos.each do |todo| %>
<li class='<%= todo_state todo %>' id='<%= dom_id todo %>'>
<%= todo.description %>
<%= completion_link todo %>
</li>
<% end %>
</ul>
As I mentioned above, helper methods that accept some instance of a model are
begging to be moved to an object that can instead be instantiated with that
model; in this case, a TodoDecorator.
My ideal interface would look like:
<%= todo.list_item do %>
<%= todo.description %>
<%= todo.completion_link %>
<% end %>
This provides the flexibility of adding whatever markup within the <li>
while moving the logic for id/class generation (as well as the
complete/incomplete links) out of the helper. Now that I know the interface,
let’s write some tests.
First, require the correct file in the RSpec helper:
require 'draper/test/rspec_integration'
To generate the decorator and its test, run:
rails generate decorator Todo
With that added, let’s test-drive the implementation. First, the list_item
method:
# spec/decorators/todo_decorator_spec.rb
describe TodoDecorator do
context 'list_item' do
it 'renders list item for complete todo' do
todo = build_stubbed(:todo, :completed)
result = TodoDecorator.new(todo).list_item do
'string'
end
markup = Capybara.string(result)
markup.should have_css("li#todo_#{todo.id}.complete",
text: 'string')
end
it 'renders list item for incomplete todo' do
todo = build_stubbed(:todo)
result = TodoDecorator.new(todo).list_item do
'string'
end
markup = Capybara.string(result)
markup.should have_css("li#todo_#{todo.id}:not(.complete)",
text: 'string')
end
end
end
I test both complete and incomplete todos, ensuring that the text within the
block is present. Capybara.string makes it really easy to write assertions
against the generated markup.
Next, let’s test completion_link:
describe TodoDecorator do
include Rails.application.routes.url_helpers
context 'completion_link' do
it 'generates a link to complete the todo when incomplete' do
todo = build_stubbed(:todo)
result = TodoDecorator.new(todo).completion_link
markup = Capybara.string(result)
markup.should have_css("a[data-method='post'][href='#{todo_completion_path(todo)}']",
text: 'Complete')
end
it 'generates a link to mark the todo as incomplete when complete' do
todo = build_stubbed(:todo, :completed)
result = TodoDecorator.new(todo).completion_link
markup = Capybara.string(result)
markup.should have_css("a[data-method='delete'][href='#{todo_completion_path(todo)}']",
text: 'Incomplete')
end
end
# context 'list_item' ...
end
Finally, the decorator implementation:
class TodoDecorator < Draper::Base
decorates :todo
def list_item(&block)
h.content_tag(:li, list_item_options, &block)
end
def completion_link
if completed_at?
h.link_to 'Incomplete', completion_path, method: :delete
else
h.link_to 'Complete', completion_path, method: :post
end
end
private
def completion_path
h.todo_completion_path(self)
end
def dom_id
h.dom_id(self)
end
def list_item_options
{ id: dom_id, class: state }
end
def state
if completed_at?
'complete'
end
end
end
This should look familiar since the majority of it came from the existing
helper. The biggest thing to note is accessing helper methods versus methods
on the decorated component. Helper methods are accessed by calling them on
h, which represents Rails’ helpers; this includes routes, record identifiers
(like dom_id), and tag generators (link_to, content_tag). Methods on
the decorated component (todo) can be invoked directly. Finally, if you don’t
want to prefix helper methods with h., just include Draper::LazyHelpers in
your decorator.
The view can now be changed to my desired implementation, and the suite is still green! What are the benefits to a decorator versus leaving logic in the view and helpers?
I’ve only recently started playing with the draper gem but I really enjoy it. The benefits are pretty clear to me and I love the fact that it’s so easily testable. If you’ve got procedural code or logic in your views (or methods on your models that are purely view-specific) I’d recommend moving those methods to a better place: a decorator.
And now, a friendly message from your local Tell, don’t ask Department.
Not so good:
<% if current_user.admin? %>
<%= current_user.admin_welcome_message %>
<% else %>
<%= current_user.user_welcome_message %>
<% end %>
Better:
<%= current_user.welcome_message %>
Not so good:
def check_for_overheating(system_monitor)
if system_monitor.temperature > 100
system_monitor.sound_alarms
end
end
Better:
system_monitor.check_for_overheating
class SystemMonitor
def check_for_overheating
if temperature > 100
sound_alarms
end
end
end
Not so good:
class Post
def send_to_feed
if user.is_a?(TwitterUser)
user.send_to_feed(contents)
end
end
end
Better:
class Post
def send_to_feed
user.send_to_feed(contents)
end
end
class TwitterUser
def send_to_feed(contents)
twitter_client.post_to_feed(contents)
end
end
class EmailUser
def send_to_feed(contents)
# no-op.
end
end
Not so good:
def street_name(user)
if user.address
user.address.street_name
else
'No street name on file'
end
end
Better:
def street_name(user)
user.address.street_name
end
class User
def address
@address || NullAddress.new
end
end
class NullAddress
def street_name
'No street name on file'
end
end
Good OOP is about telling objects what you want done, not querying an object and acting on its behalf. Data and operations that depend on that data belong in the same object.
Tell, don’t ask!
Detect emerging problems in your codebase with Ruby Science. We’ll deliver solutions for fixing them, and demonstrate techniques for building a Ruby on Rails application that will be fun to work on for years to come.
The Rails community has been abuzz with object-oriented programming, SOLID principles, laws, design patterns, and other principles, practices, and patterns. We’ve (re)discovered new tools and techniques to separate and reuse logic, making code easier to test, understand, and maintain. Now that we’ve learned about all these new tools, when do we use them?
Applying design practices correctly takes a lot of practice, and when you first discover a hammer, everything looks like a nail. You can refactor every class ad-nauseum until each class obeys every letter of the SOLID principles, but that approach will hurt you: for one, not every class will ever see too much use or change, but for another, many of the abstractions advocated by those principles can hurt overall readability in the short term. Extracting a method means you need to jump around a file to figure out what’s going on. Extracting a class means you have to jump between files. Extracting a library means you need to jump between projects. Adding names can clarify what you’re doing, but adding too many names results in a vocabulary overload. Abstractions can grant you infinite flexibility but zero readability.
If you’ve seen these principles applied incorrectly or overapplied, you may be tempted to throw the baby out with the bath water. Many Ruby developers came from Java, and after a few years of working with AbstractUserDecoratorFactoryFactories, some of them decided to never use the words “design pattern” again. Many principles have been applied with good intention and terrible results, but that’s no reason to throw out everything we’ve learned since the inception of object-oriented programming.
Applying the Dependency Inversion Principle in one situation may create an unreadable, abstract puzzle, but using it when you need it can keep a single class from ruining an entire application. Applying any principle as a black and white law will result in a game of refactoring whack-a-mole. However, if we can’t apply these principles universally, how do we know when to be aggressive about refactoring?
Every project has them: one or two classes that seem to know everything. Any question you could ask in the domain is answered by one of these classes. Any class you look into seems to depend on them. You can’t change any class in the system without breaking the tests for a god class. My experience has taught me that most projects will have two god classes: User and whatever the focus happens to be for that application. In a blog application, it will be User and Post. In an Agile project management tool, it will be User and Story. God classes grow and become entangled with every other component of the system until there’s no room to breath.
God classes are a great place to start aggressive refactoring. Without even looking inside their files, you can probably tell which two classes in your project are the culprits. I think very carefully before adding any behavior to these classes. I apply SOLID principles rigourously to these classes. If I can extract behavior from one of these classes using a design pattern like observer or decorator, I’ll do it.
Extracting behavior from a class like this doesn’t reduce overall comprehensibility, because the class is already too large to comprehend. Introducing abstractions to these classes makes them easier to understand, because you can at least fit all the abstractions in your head at once, even if you can’t remember what the implementations are like.
You can use tools like Churn to figure out which files in your project change the most. The top two contenders are likely to be your project’s god classes, but you may be surprised which other files change the most. If something changes once, it’s likely to change again, and locating the pieces of these classes that change and extracting them will make the next change faster. Refactor the classes that change the most by slimming them down and making them as readable as possible to increase productivity and help to avoid defects.
If you can pinpoint a bug to a particular class, it’s likely that refactoring that class will help to prevent the next bug. Bugs love company, and the same parts of an application tend to break again and again. Extractions can help isolate the trickier components, and refactoring may reveal that the bugs are cropping up because you were thinking about the problem the wrong way to begin with. Keep track of which files you change when fixing bugs; these files are great targets for aggressive refactoring.
When do you stop refactoring? When is a change good enough to commit? When is a branch good enough to deploy? When is a library good enough to release?
A program is never finished, and no amount of refactoring will make it perfect. I refactor to fix the parts of an application or library that have caused me pain. Rather than attacking theoretical problems, identify components that have actually bitten you and apply theory to tame them. Refactor as you go, and fix one problem at a time.