View objects in Ruby with SimpleDelegator

View objects are given little love in Ruby because of Rails' lack of a true view layer in it's architecture. Let's look at some view object patterns that can help you scale your server rendered views.

Rails is a fantastic framework but I often find myself wondering where certain logic for the view should belong in a Rails project. I didn’t like how easy it was to write code in a global scope with Rails helpers and once I realized that Rails controllers and templates share the same context, I decided to introduce some intermediate view layer.

Enter, SimpleDelegator from Ruby’s standard library. I don’t want to start a naming war here so I’m going to call these objects ‘view objects’, and you’re free to interpret them as decorator/presenter/helper objects as you wish.

Basic example of how to use SimpleDelegator:

class User < PersistentDataStore::Base
  attr_accessor :first_name, :last_name
end

class UserView < SimpleDelegator
  def fullname
    "#{first_name} #{last_name}"
  end
end

user = User.new
user.first_name = "Luke"
user.last_name = "Skywalker"

user_view = UserView.new(user)
user_view.full_name  #=> "Luke Skywalker"
user_view.first_name #=> "Luke"
user_view.last_name  #=> "Skywalker"

SimpleDelegator works by implementing the method_missing method that ruby calls when it can’t find a variable or method name. It then asks the delegate object if it responds to the apparently missing method implementation and if it does it calls it.

This means when we call first_name in the UserView, SimpleDelegator delegates the method invocation to it’s delegate object - the object you created the UserView with, which is the user object.

In the real world this is almost never sufficient, I need to pass in additional arguments/state to use in the view.

Example of simple delegator with additional arguments:

class UserView < SimpleDelegator
  def initialize(delegate, salutation: 'Padawan')
    super(delegate)
    @salutation = salutation
  end

  def fullname
    "#{salutation} #{first_name} #{last_name}"
  end

  private
  attr_reader :salutation
end

user_view = UserView.new(user, salutation: "Jedi Master")
user_view.full_name  #=> "Jedi Master Luke Skywalker"
# Horay for keyword arguments!
user_view = UserView.new(user)
user_view.full_name  #=> "Padawan Luke Skywalker"

These are quite purely decoratoring the objects that are passed in, but are not yet doing much view specific work. Your web framework undoubtedly provides handy url and view helpers. So, what if I need to use a Rails url helper or link_to, you ask?

Cherry picking view helpers in your view objects:

module ViewHelpers
  def urls
    Rails.application.routes.url_helpers
  end

  def helpers
    ActionController::Base.helpers
  end
end

class UserView < SimpleDelegator
  include ViewHelpers

  def initialize(delegate, salutation: 'Padawan')
    super(delegate)
    @salutation = salutation
  end

  def fullname
    "#{salutation} #{first_name} #{last_name}"
  end

  def master_users_path
    helpers.link_to "MAASTERS", urls.users_path(skill: 'master')
  end

  private
  attr_reader :salutation
end

Warning: If you need to use _url helpers you need to provide the context with a default_url_options method implementation. This can be done by using active support with the following change to the module, and then including Rails.application.routes.url_helpers into the UserView class instead of using helper methods.

module ViewHelpers
  extend ActiveSupport::Concern
  included do
    def default_url_options
      ActionMailer::Base.default_url_options
    end
  end
  ...
end

I don’t use _url routes very often so I opt for the simpler approach, and I like this approach opposed to passing in the view context as a parameter because you completely decouple these objects from the view and can instantiate and use them anywhere where there is no view context to pass in, like the Rails console, or a service object for example.

A pattern my friend @franks921 likes to use is encapsulating the entire interface for the view in a single object that then calls into view objects. I like this pattern because it reduces the mental overhead of keeping track of if the method being called or instance variable being referenced is defined in the controller or some random Rails helper. It also makes your view layer more isolated making it easier to compose, test and so helps me sleep at night.

Encapsulating the template layers interface to an object

class DashboardView
  def initialize(current_user, users, featured_posts)
    @current_user = current_user
    @users = users.map { |u| UserView.new(u) }
    @featured_posts = featured_posts
  end
  attr_reader :current_user, :users, :featured_posts
end

# Instantiate this in your controller layer and pass it to the view as the only "source of truth" for the data on the page

If you get tired of writing ModelDecorator.new(model_instance) you can use some metaprogramming to define a decorate method passing in a model. Personally I prefer the code to be simpler, even if it is a little more verbose, so I opt for a little trick @frank921 on twitter uses.

Getting fancy with the spices

def decorate(object, decorater = nil)
  if object.is_a? Array
    return [] if object.empty?
    decorater_class = decorater || (object[0].class.name.split("::").last + "View").constantize
    object.map { |o| decorater_class.new(o) }
  else
    decorater_class = decorater || (object.class.name + "View").constantize
    decorater_class.new(object)
  end
end

user  = decorate(current_user)
users = decorate(User.popular(10))

A simpler approach even if slightly more verbose

class BaseView < SimpleDelegator
  def self.collection(objects)
    objects.map { |obj| new obj }
  end
end

class UserView < BaseView
  ...
end

users = User.popular(10)
users = UserView.collection(users)


References

  1. Article by thoughtbot comparing view object strategies
  2. SimpleDelegator documentation
  3. Nick Sutterer on helpers - old but good rant

Questions? I’d be humbled and happy to help.