Rails Decorators: A Refactoring Adventure

Okay, I have a confession to make. We're not going on a real adventure. If you were expecting space ships or pirates, you're out of luck. However (bear with me), we are venturing into what is, for many Rails newbies, unchartered territory: Refactoring and design patterns. Exciting!

Ruby is a language that lends itself particularly well to design patterns. The basic pattern of your Rails app should be that your models are heavy and your controllers and views are skinny.

Let me explain it with cake:

I found this image hanging out in the comments section of a stack overflow question. If it's yours, well done and let me know!

As Rails developers, we like to adhere to the Single Responsibility Principle––the idea that, in Object Oriented Programming, each class should have one job and it's methods should be narrowly aligned with that responsibility.

In the MVC context of Rails, this principle often extends to our views and controllers. If we don't keep them thin, our cake is liable to topple over. Our controllers should only be responsible for delivering data to the client (in the views). They shouldn't know too much about the content of that data. To keep our controllers thin, we can use service objects.

Our views are responsible only for displaying information. We don't need to weigh them down with logic. Views are not in charge of deciding what to display. This principle can be tough to follow, especially when we are implementing Javascript to handle event-trigged changes to the DOM. That's where the Decorator Pattern comes in.

The Decorator Pattern

The decorator pattern is a design pattern that allows you to add behavior to an object without affecting or weighing down every object of that class. If you're familiar with object orientation in Javascript, you're probably familiar with the use of decorators to add capability to Javascript objects.

Object decorators are sometimes referred to as wrappers because they wrap objects with new capabilities. When we implement decorators in our Rails views, we can design them to extend an object's behavior based on runtime characteristics. In this way, we can use decorators hand-in-hand with Javascript functions to make event-trigged changes to the DOM.

Heavy Views == Bad

Let's take a look at some code that is desperately in need of some refactoring. For the purposes of this example, we'll be working with code from a past project of mine--a simple chatting app. You can check out the repo here or play a game of Title-ize! via my chatting app here.

In this app, we have a Conversation model that handles conversations, or chats, between users. Our conversation view, views/conversations/index.js.erb works in conjunction with Javascript long polling to alert a user that someone is messaging them to chat. You can learn more about long polling in a previous post but for now, we just need to understand the following:

A user's home page displays a list of all of the other users who can message them to play a game of Title-ize!

We've built a Javascript function that runs, starting on document ready, every 10 seconds. It polls the server--by sending a GET request to the index action of the Conversations controller--for any new messages or conversations.

The Conversations controller is responsible for responding to the request sent by our polling function every 10 seconds. The index action renders the conversations/index.js.erb template in the views directory. It contains Javascript functions that will add some alert-like styling to a user's name if that user has sent a new message to the current user.

In other words, if Charlier is messaging you, the you should see something like this:

So far, we described some styling that needs to appear under certain conditions, i.e., if a current user has received a new message. So, during an early stage of this project, my views/conversations/index.js.erb template contained that logic and looked like this:

<%if @update %>  
  <%@convos.each do |convo|%>
      var id = <%=convo.sender_id%>
      $('#new-messages-'+ id).html("<div class='blink'></div>");
      $("#new-messages-name-"+id).addClass("bold-name")

  <%end%>
<%end%>  

And the index action of my Conversations controller looked like this:

def index  
    @update = false
    @convos = []
    @users = User.all 
    @users.each do |user|
      @convos << Conversation.between(current_user, user).first  
    end
    @convos.compact!
    @convos.each do |convo|
      @update = true if convo.messages.last.created_at > Time.now - 10.seconds
    end

Our view is looking vague--we have some functions that are invoked if @updated. If what is updated? What @convos are we using to determine where to apply the alert styling?

On the controller side, the index method is very heavy--it knows way to much about the conversations it is serving up to the view. It knows about users, is checking all of the conversations and collecting them and preforming some rather complex logic to check for and collect only conversations with new messages.

If this was a cake, it would fall over and be ruined. To extract some of this logic out of the Conversations controller and view, we will use a decorator.

Service Objects vs. Decorators

You might be considering a service object here. However, we want to stick with service objects for jobs specific to controllers and employ decorators when dealing with our views. Although the problem here starts in our controller, we're only employing all of this logic so that we can display the correct data in our view. Accordingly, a decorator object is appropriate.

Building a Decorator

Before we can start building, let's think about what behavior we need to add to a conversation. In other words, what do we need to decorate it with?

In order to add alert styling to senders of new messages, as in the screenshot above, we need to be able to determine:

  1. Which conversations have new messages (i.e. messages that were born less than 10 seconds ago).

  2. Who is the sender of those messages.

So, our conversation decorator needs to be capable of those two behaviors.

The Conversation Decorator Class

To accomplish the goals outline above, we'll create a brand new ConversationDecorator class that has public-facing methods to find conversations with new messages and ID the senders of those messages.

Then, our Conversations controller and view can rely on instances of the ConversationDecorator class to implement the appropriate styling.

Let's get started. In app/models, create conversation_decorator.rb:

class ConversationDecorator

end  

Collecting Conversations

The first task we can extract from our Conversations controller and into our decorator is the job of collecting all of the conversations between a current user and the other users. After all, we don't need to check every conversation for new updates, just the conversations our current user is involved in.

Let's build a method .convos that collects all such conversations:

class ConversationDecorator  
    def convos
      @convos ||= User.all.map { |user|       
        Conversation.between(current_user, user) }
        .flatten
    end
end  

Identifying New Messages

Let's move on to our second objective: determining which conversations have new messages. Remember, our Javascript polling function is sending a request for new messages to the server every 10 seconds. So, a new message is a message that was created earlier than 10 seconds ago.

Let's extract that logic from the Conversations controller and build an .updated? method

class ConversationDecorator  
    def convos
      @convos ||= User.all.map { |user|       
        Conversation.between(current_user, user) }
        .flatten
    end

    def updated?
      updated = false
      new_msgs = convos.collect do |convo|
           convo.messages.last.created_at.to_time >  
             Time.now.to_date - 10
      end

      updated = true if new_msgs
      updated

    end
end  

This .updated? method is a little messy. Let's break it down and think about opportunities for further refactoring.

We iterate over all of the conversations we earlier collected. We collect any conversations whose last message was created earlier than 10 seconds ago. We return 'true' if there are conversations with new messages. Otherwise, we return false.

Since we're thinking about design patterns today, another ideal to keep in mind is that methods, like classes, should have one responsibility. The above method looks like it has a few jobs: iterating over each conversation, grabbing any conversations with new messages, returning true or false based on the presence of such conversations.

Wouldn't it be helpful if a conversation could simply tell us whether or not it had any new messages? That sounds like a job for the Conversation model.

Model Methods

Let's build a method, .last_message, to grab the last message of each conversation and another method, .new_messages?, to determine if that message is "new", i.e. created in the last 10 seconds.

In app/models/conversation.rb:

class Conversation < ActiveRecord::Base

   ...

   def last_message
    messages.last
   end

   def new_messages?
    last_message.created_at.to_time > 
      Time.now.to_date - 10
   end

Now that a conversation can check it's own last message and tell us if it is new, we can refactor our .updated? method in the ConversationDecorator class:

class ConversationDecorator  
    def convos
      @convos ||= User.all.map { |user|       
        Conversation.between(current_user, user) }
        .flatten
    end

    def updated?
      convos.select { |convo| convo.new_messages? }.any?
    end
end  

Now our .updated? method is just one, beautiful, elegant line.

Almost Ready...

One last thing before we are ready to implement our ConversationDecorator in our view. You may have noticed that the .convos method that collets all the conversations between the current_user and the other users is relying on a current_user local variable.

In order for the ConversationDecorator class to be able to access the current_user, we need a way to pass it into instances of our ConversationDecorator when we initialize in the controller.

Let's add an initialize method to our decorator that allows for this:

class ConversationDecorator  
  attr_accessor :current_user

  def initialize(user)
    @current_user = user
  end

  def convos
    @convos ||= User.all.map { |user|  
      Conversation.between(current_user, user) }
      .flatten
  end

  def updated?
    convos.select { |convo| convo.new_messages? }.any?
  end
end  

Now we're ready to put our conversation decorator to work.

Implementing the ConversationDecorator

Our Conversation controller will be responsible for instantiating the ConversationDecorator and the conversations/index.js.erb view template will use the decorator's methods to execute the appropriate Javascript functions and display the correct styling to the viewer.

In app/controllers/conversations_controller:

class ConversationsController < ApplicationController  
  before_filter :authenticate_user!

  def index
    @decorated_conversation = 
     ConversationDecorator.new(current_user)
  end

In app/views/conversations/index.js.erb:

<% if @decorated_conversation.updated? %>  
  <% @decorated_conversation.convos.each do |convo| %>
      var id = <%= convo.sender_id %>
      $('#new-messages-'+ id).html("< div                
         class='blink'></ div>");
      $("#new-messages-name-"+id).addClass("bold-name")
  <% end %>
<% end %>  

Now, we've been able to remove all of that heavy logic from the index method of the Conversations controller and our view is more explicit.

Phew! That was a lot of work just to have something that already worked just fine look a little prettier. Was it worth it? I think so, and most developers would agree on the importance of clean, well-designed code. Our code is now:

  • Easy to understand––a developer reading it for the first time and easily see what behavior is being carried out.
  • Abstract––we are hiding messier logic in helper methods inside our decorator class and conversation model.
  • Built for the future––by abstracting the logic and separating out the responsibilities of different classes, our code is accommodating of future change.

I hope you're convinced to try implementing the decorator pattern on your own. Happy coding!

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus