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
- Article by thoughtbot comparing view object strategies
- SimpleDelegator documentation
- Nick Sutterer on helpers - old but good rant
Questions? I’d be humbled and happy to help.