Some of you Rails developers probably have used a 'decorator' or 'presenter' library. These libraries aim to bridge between Rails model and view layers. If I am to define it, a presenter allows developers to group helper methods related to a model to be under a namespace related to that model, instead of the current global space.
But would you believe it? There are actually a dozen or more decorator/presenter gems out there. Why do we reinvent the wheels? The first reason is that there is really a demand, because keeping large amount of helper methods under the same namespace is just unrealistic. The second reason is that, these gem owners have different views on this philosophical question: should the interface be implicit (things are done for the user under the hood) or explicit (user has to type more).
As a fun exercise I will compare 6 of these gems and explain how the general concept works. I have not used some of the gems, but only read the readme and some of the code, so if there are mistakes please let me know.
*Disclaimer, I am the owner of LulalalaPresenter gem. And if you have never used a presenter/decorator before, read this post to know why it is useful.
The following table represents the spectrum of these gems. On the left end, we see gems favoring implicity more. On the right end, gems are more explicit in nature.
|Decorate (Quack like a model)||Y||optional||Y||Y|
|Directly call helper method within decorator||Y||optional|
|Globally accessible view context||Y||Y||Y|
|Lines of code||281||718||130||375||110||38|
|Lines of test||473||3037||354||1556||294||n/a|
(I find many more gems after writing this. A complete list can be found here. If you want to add your gem to it, let me know~)
"I hate magic" camp
The conservative rubyists prefer to avoid magic. They don't like to override too much things, and they write PORO instead of meta-programming.
The most simple example can be seen from Ryan Bate's RailsCast: "Presenters from Scratch". In the video he explains step by step how to make a simple presenter.
The only meta-programming used is how it infers the presenter class from the model. All other interfaces are simple object passing method calls. Only 38 lines are required to achieve this.
Since by default it does not act like a model, Ryan calls it a presenter instead of decorator. You can still delegate calls to model if you wish, but the readme indicates that presenter object should not be mixed up with model object.
I'll talk about LulalalaPresenter later after ActiveDecorator, because it is its fork.
According to Design Patterns in Ruby, the decorator pattern is a wrapper that "supports the same core interface, but adds its own twist on that interface." In this case we add view related functionality around the model. A decorator can act as if it is the model, which means less view changes are required.
ActiveDecorator, oprah and display-case all position themselves as decorators. Draper on the other hand gives the user freedom to choose if the wrapper should become a decorator or not.
One association further
Associations are part of the ActiveModel interface too, and some gems can decorate associations for you to save some key strokes (example). This is no small task, as there are multiple ways to trigger associations. It is therefore more possible to break across Rails versions.
Directly call helper methods within decorator
Normally for a model decorator to call helper methods within it, it needs to call via
view_context (often alias as
h.url_for(). Both ActiveDecorator and Draper offers a way to save key strokes so you can call
url_for directly. This is done by a simple trick: if the model does not support a method, we retry it on view context again (example).
The implications are: 1. this is a wee bit slower because
method_missing is utilized. 2. if same method name exists on model and view_context, model's method takes precedence. This is usually not a problem.
Globally accessible view_context
Draper, ActiveDecorator and LulalalaPresenter all keeps the view_context in a globally accessible place (example). This is done for two reasons:
Give an OO-esque feel to the decoration method:
Draper decorates by calling
LulalalaPresenter presents by calling
The design saves you from passing view_context, otherwise one will need to do
model.decorate(view_context)all the time.
To allow automatic decoration (see below).
The Holy Grail of Implicity
We have reached the end of implicity. To automatically decorate things, ActiveDecorator hooks into the render call, and decorates instance variables when applicable (example).
The gem walked the extra miles for you, so you don't have to do anything beside writing the decorator class. In some way, this feels like Rails philosophy, where we just write the controller/model/view, and things will just hook up perfectly without you knowing what's happening under the hood.
My Own Presenter and Conclusion
As you can see from the table, most are decorators. They behave like models, delegating calls to the inside. However if we treat presenter as a separate object, we can reduce a lot of the delegation complexities.
I was looking for a presenter which does not involve decorating ActiveModel, but I couldn't find one. Ryan's solution was the closest I could find. So I thought I can make my own.
I personally hate typing parenthesis, because my left little finger aches when holding
shift key. Instead of
present(model).foo, I prefer
model.present.foo. If we are to do this, how can presenter get hold of view_context object? We can pass it in everytime like this
model.present.foo(vc), but that's definitely too much too type. In the end, I found out ActiveDecorator's globally accessible view_context, so I used it to make my own. Hopefully this does not offend any one, I feel guilty but I want to please my little finger.
Do we need presenters/decorators pattern? Some may argue this is an offense to the MVC architecture, and some may think it introduces extra complexity. Again we will probably never get a consensus on this, but I use it because it made coding Rails more pleasant. So if you have large number of helper methods in your codebase, I recommend you to just pick a gem and try it out :)