Experimenting with Draper
Let’s play around with the concept of decorators and check out some of the features offered by the Draper gem.
This tutorial is open source. Please contribute fixes or additions to the markdown source on Github.
Setup
Get the Blogger project from Github and run setup procedures:
1 2 3 4 5 | |
All existing tests should pass. Optionally, run the tests continuously while developing by running guard
Install Draper
Next, open the Gemfile and add a dependency on 'draper' like this:
1
| |
Run bundle, then start up your server.
Generate a Decorator
We’ll create a decorator to wrap the Article model. Draper gives you a handy generator:
Terminal
$
| |
It will create the folders app/decorators/, spec/decorators/and the files app/decorators/article_decorator.rb, spec/decorators/article_decorator_spec.rb. Open the file and you’ll find the frame of a ArticleDecorator class.
Restart your server so the new folder is added to the load path.
First Usage
Without adding anything to the decorator, let’s see the simplest usage. Open the articles_controller and look at the show action. It currently has:
1 2 3 | |
To make use of the decorator, call the .new method and pass in the Article from the database:
1 2 3 4 | |
But we can simplify that common pattern. The decorator will delegate the find method to the wrapped class, allowing us to write this:
1 2 3 | |
Then go and view the show page for a single Article by clicking on its name on the index.
Adding Methods
Now let’s add some actual functionality to our decorator.
Article Published On
Currently the show page just displays the raw created_at attribute. Often we want to standardize date formatting across our application, and the decorator makes this easy.
Let’s override the created_at method in our decorator:
1 2 3 | |
Since the decorator knows that it is decorating an instance of Article, it dynamically generates a method named article which returns the wrapped object. Here, calling article.created_at gets us the value from the original database model.
Now in the show.html.erb for the show view of Article you need to change the following line:
1
| |
to:
1
| |
Refresh the show in your browser and the date will be reformatted.
Comment Counter
Currently the show page uses the pluralize helper:
1
| |
That’s a Law of Demeter violation right away, and it isn’t setup for future internationalization. We can pull the functionality into the decorator by adding a method:
1 2 3 | |
Then in the view template:
1
| |
Dealing with a Collection
If you look in the index.html.erb, you’ll see a similar pluralize line. Can you just reuse the decorator method? Try calling the .comments_count method.
We need the article objects to be decorated in the controller. In your index action you have:
1 2 3 | |
Let’s tweak it a bit to decorate the collection:
1 2 3 4 | |
Now all elements of the collection are decorated and our index should work properly.
The original Demeter violation is still there, but now it can be cleaned in just one spot – by adding a counter cache column to the articles table and accessing the cache in the decorator.
Using Allows
When we define an interface, we want to be able to exclude or include specific accessors. With Draper decorators, we can do this with denies and allows. The allows is more common, so let’s try it.
In your browser, load the show page for an article. In the decorator, add this:
1
| |
Then refresh the page. It should blow up.
allows is modeled after attr_accessible in Rails. If your decorator calls allows, then all methods not listed are denied. This is a whitelist approach.
Allowing More Methods
So far you’re only allowing :title, so you’ll get exceptions as the other accessors try to pull out data. Add the needed methods to allows, separated by commas. Make sure you include to_param so your links will work properly.
Note that if you don’t use allows at all, everything is permitted.
Links
I hate writing delete links. In the show.html.erb, I’m using a helper to generate the link with icon:
1
| |
Which calls this helper in IconsHelper:
1 2 3 4 5 6 7 | |
It works fine and wraps up some of the ugliness, but using the helper is a procedural approach. The decorator allows us to be object-oriented.
Writing the Decorator Method
To rework this helper, let’s start by just dropping the helper method into our decorator class
1 2 3 4 5 6 7 8 9 10 11 | |
We don’t need to pass in the object parameter because the decorator will already know it’s article. We can write this:
1 2 3 4 5 6 7 8 9 10 11 | |
Note how object became article.
In the Show Template
Originally, we used a procedural-style helper method:
1
| |
Now we can use the decorator method:
1
| |
Keeping it object-oriented is the Ruby way.
Abstracting Decorators
In the conversion from Helper to Decorator in the last section, something was lost. The helper method worked on any object passed in, the decorator method belonged to the ArticleDecorator. We definitely don’t want to re-implement this code in multiple decorators, so how can we make it shareable?
ApplicationDecorator.rb
Approach one is to make an app/decorators/application_decorator.rb and move the method in there:
1 2 3 4 5 6 7 8 9 | |
You’ll also need to make your ArticleDecorator inherit from ApplicationDecorator.
It’ll work for what we have so far, but if we try and use this from a CommentDecorator, it’s going to blow up because of the call to article.
Draper provides a generic way to access the wrapped object – the wrapped_object method. Just change article to wrapped_object and we’re good to go:
1 2 3 4 5 6 7 8 9 | |
The main downside to this approach is that every decorator in the system is going to have this method. What if some decorators need a different style of delete icon?
The Module Approach
One of the limitations of helpers is that they all live in the same name space. You can’t have two different implementations of a delete_icon helper.
But since decorators are objects, that’s not an issue. We can use modules and mix them into the decorator classes.
For instance, we can create app/decorators/icon_link_decorations.rb and define this module:
1 2 3 4 5 6 7 8 9 | |
Remove the similar code from the ApplicationDecorator, and include the module from the ArticleDecorator:
1 2 3 4 | |
Any other decorators that want to use that method can similarly include the module.
Now, Play!
Those are the fundamental ideas, now you should try things on your own. Here are a few ideas:
More Links
Steve Klabnik wrote an example of implementing HATEOAS in his article here: http://blog.steveklabnik.com/posts/2012-01-06-implementing-hateoas-with-presenters
The resulting HTML looks like this:
1 2 3 4 5 | |
Going a step further than Steve’s approach:
- Implement an
.index_linkpresenter method that outputs the HTML link with theRELattribute set to"index" - Implement a
.linkpresenter method that outputs a link to the article, but sets theRELto"self"if the app is currently on that article’s show page. If it’s called from the index page, make theREL"article_1"with the correct ID - Can you abstract this into a
modulesuch that it could be included in aCommentPresenterand work for both? Try it.
Controlling Marshalling
We usually need to_json and to_xml operations to present an API. They’re often implemented in the model, but they really belong in the view layer. The decorator pattern can make it clean.
- Implement a
to_jsonmethod in the decorator that just calls theActiveRecordmethod
Beyond that, it would be great to scope the JSON based on the current user. Since we don’t have an authentication/authorization setup, we’ll fake it using a request parameter.
- Define two constants:
PUBLIC_ATTRIBUTESas an array containing symbols for thetitle,body, andcreated_atADMIN_ATTRIBUTESas an array containing everything fromPUBLIC_ATTRIBUTES, plusupdated_at
- Manually add a parameter to your request URL with
admin=true - Write a
current_user_is_admin?method in yourApplicationHelperwhich returns true if that parameter is set to"true" - Call that helper method (using
h.current_user_is_admin?) in your decorator.- When the user is an admin, show them the values specified by
ADMIN_ATTRIBUTES - When the user is not an admin, show them only the values specified by
PUBLIC_ATTRIBUTES
- When the user is an admin, show them the values specified by
If you want to play more with marshalling, what would it be like to create descendents of your ArticleDecorator like ArticleDecoratorXML and ArticleDecoratorJSON? What functionality could you add which would allow the user to stay in the "duck typing" mindset, calling the same method on an instance of any of the three decorators but getting back HTML, XML, or JSON?
Moving Forward with Decorators
The concept of Draper is still young. Please try it out on your projects and give us feedback at https://github.com/jcasimir/draper. Thanks!