When to Use Decorators
When to use decorators in our web site? Decorators are also known as view-models or presenters.
Let’s start from the basics. Decorators fit into MVC (model-view-controller) architecture. Controller “sends” data into a view and the view is rendered and is usually send back to a browser on the other side of a connection. But I have also worked with MVC architecture in non-web applications…
When not to Use Decorators?
When a model is simple and therefore a respective view must be also simple (from the complexity perspective, not length).
<!-- app/views/articles/show.html.erb -->
<article class="article">
<header>
<h1 class="article__title"><%= @article.name %></h1>
</header>
<div class="article__body"><%= @article.content %></div>
</article>
Easy, the model is simple enough, a decorator is not needed.
When to Use Decorators?
When there is a complex model - very complex data along with complex logic in the view. Let’s look at my generator of the “all” Atom feed which is used here, in myrtana.sk. This shitty code took me a couple of hours to finish. Typical cowboy coding with no tests. Just messing around. Or, from different point of view: a “spike”. Because there will be another iteration where tests will be written and everything polished.
The data model is insanely complex and much complex than in Wordpress or other typical CMS. I sort of tried to implement full Dublin Core with qualifiers, but it’s not finished yet and the behavior is only provisional xD.
<!-- app/views/outer_comm/atom/index.xml.erb -->
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title type="text">myrtana.sk all content</title>
<subtitle type="html">hahahaha</subtitle>
<updated><%= Time.parse(@last_updated).iso8601 %></updated>
<id>myrtana.sk:all</id>
<link rel="alternate" type="text/html" hreflang="en" href="https://myrtana.sk/" />
<link rel="self" type="application/atom+xml" href="https://myrtana.sk/outer_comm/allfeed.atom" />
<rights>See respective licenses in the content</rights>
<generator uri="https://myrtana.sk/" version="1.0">Myrtana Experimental Prototype</generator>
<% @entries.each do |entry| %>
<entry>
<title><%= entry.metadata.title.join(_(' or ')) %></title>
<link rel="alternate" type="text/html" href="<%= article_url(id: entry.metadata.uri.first) %>" />
<id><%= entry.metadata.uri.first %></id>
<% updated = entry.metadata.updated.sort.reverse.first || entry.metadata.posted.sort.reverse.first || entry.metadata.created.sort.reverse.first || entry.metadata.published.sort.reverse.first %>
<updated><%= (Time.parse(updated) rescue Time.parse('1970-01-01')).iso8601 %></updated>
<% published = entry.metadata.published.sort.reverse.first || entry.metadata.posted.sort.reverse.first || entry.metadata.created.sort.reverse.first %>
<published><%= (Time.parse(published) rescue Time.parse('1970-01-01')).iso8601 %></published>
<author>
<name><%= entry.metadata.author.join(', ') %></name>
</author>
</entry>
<% end %>
</feed>
Let’s focus only on this view and assume that the code in the controller is nice, polished and elegant.
From my experience, only ifs, elses and eachs should be in a view. Other logic, i.e. formatting and handling of errors and edge cases, should be in a decorator and the decorator would be sent from controller to view. Time.parse
, .iso8601
, assigning of variables and joins have to go there.
Why? Complexity. It is a hell to maintain it, tweak it, test it, make big changes and everything is really fragile. Tests need to parse the resulting XML (if you actually can write a test for this and would not go insane), but if the XML changes in the future, so need tests, which is bad, because we are lazy, right? Also there’s no point to test XML when data correctness is tested. Ruby data structures should be tested and XML testing should be done by one simple happy integration test.
To refactor the code, here, metadata
structure with all those crazy arrays have to be processed by a decorator. So there is AtomFeedMetadataDecorator
class at least. Decorators are usually created by feature. Atom feed needs dates in ISO 8601 format, but HTML view use something more human readable.
With decorators, I’d like my feed generator to look like:
<!-- app/views/outer_comm/atom/index.xml.erb -->
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title type="text"><%= @feed.title %></title>
<subtitle type="html"><%= @feed.subtitle %></subtitle>
<updated><%= @feed.last_updated %></updated>
<id><%= @feed.id %></id>
<link rel="alternate" type="text/html" hreflang="en" href="<%= @feed.html_url %>" />
<link rel="self" type="application/atom+xml" href="<%= @feed.url %>" />
<rights>See respective licenses in the content</rights>
<generator uri="<%= @feed.generator_uri %>" version="1.0">
<%= @feed.generator_name %>
</generator>
<% @feed.articles.each do |entry| %>
<entry>
<title><%= entry.title %></title>
<link rel="alternate" type="text/html" href="<%= article_url(id: entry.uri) %>" />
<id><%= entry.uri %></id>
<updated><%= entry.updated %></updated>
<published><%= entry.published %></published>
<% @entry.authors do |author| %>
<author>
<name><%= author.name %></name>
</author>
<% end %>
</entry>
<% end %>
</feed>
In the case of the feed, there would be three decorators - AtomFeedDecorator
, AtomFeedMetadataDecorator
and AtomFeedArticleDecorator
. Feed decorator would contain all hardcoded values which were extracted outside from the view and also would contain decorated articles.
Article decorator would decorate content itself and also reference to decorated metadata. And metadata decorator would most like use a “helper” class to implement all this crazy updated || posted || created
behavior, so it’s the same across the site.
Here’s a gem I use for decorators https://github.com/drapergem/draper. But I also use plain Ruby classes sometimes.
That’s all guys, I’ll create another post when (and if) I implement decorators to Atom feed. But hey, there are many articles dealing with decorators - search for “rails decorators” or “nodejs view models”.
Add Comment