We’ve discussed using Concerns and Services to keep your ActiveRecords as healthy as they can be. These have dealt with repeated logic in models, and logic that could questionably be placed in either a controller or model. In the third and final part of this series we will be looking at presenters.
In this short series I will be looking at three ways of reducing the responsibilities of your models:
Presenters provide a way of patching additional functionality onto your model before passing it into a view. Usually these presenters will be page specific, but some may be more general and also modular or component based.
In realms other than Rails, the Presenter pattern may be more commonly known as the View Model pattern. The term “View Model” helps to explain what the pattern is for, i.e. a view specific domain model, a place to put logic that adapts a domain model for a particular view. “Presenter” equally captures the idea, a proxy object that presents a model to a view.
Typically presenters are a delegation object to the domain model. In other words, a presenter will encapsulate a model and for any method that does not exist on the presenter it will delegate back to the original object and call the method on it.
Presenters are also a form of decorator, an object that adds functionality onto the object it decorates. In this case the presenter object is decorating view specific functionality onto a model. Let’s dive into an example.
A page specific presenter maybe UserProfilePresenter. UserProfilePresenter wraps the User model for the UsersController#view.
class UsersController < ApplicationController def view @user = UserProfilePresenter.new(User.find(params[:id])) end end
As you can see, we pass the model into the presenter in the controller action. The view will then use ‘@user’ just like you would use a model directly. We could of course pass ‘User’ in as ‘@user’ and you should until you need to write some view specific logic. Say we need to use the username if a full name is not available for the page title of our profile page:
require 'delegate' class UserProfilePresenter < SimpleDelegator def page_title I18n.t('user.profile.page_title', name: full_name || username) end end
Here we’ve used a translation and passed ‘full_name || username’ as a variable. Of course we could have placed this line in our view, however ‘@user.page_title’ is much tidier.
If we want to display whether the user is online or the time they were last online, we can place this logic in an ‘#online_status’ method:
require 'delegate' class UserProfilePresenter < SimpleDelegator def online_status if online? I18n.t('user.profile.online_status.online') else I18n.t('user.profile.online_status.last_online', last_online: last_online) end end end
Again, we call on our translations, but use some additional logic to decide whether to display an online message or last online.
At this point you may be wondering where the methods ‘#full_name’, ‘#username’, ‘#online?’ and ‘#last_online’ come from. The trick is in ‘SimpleDelegator’. This class included in Ruby’s standard library provides an ‘#initialize’ method that takes an object. As you can see in our ‘UsersController’ above, we passed in a ‘User’ object. Any method that does not exist in ‘UserProfilePresenter’ will be called on the ‘User’ object we passed in. This means our presenter will have all the methods our model has, any overrides we define and also new methods we define. This is useful since we will often want to use model methods directly in our views without modification. There is no need to reinvent the wheel with presenters.
Sometimes you will have methods used in multiple ‘User’ related views. Take our ‘#online_status’ from the previous example. If this was needed in multiple views then we have a few options.
The first option would be to move it into the ‘User’ object. However if we want to keep our view related logic, especially translation specific calls to ‘I18n.t’, then we might want to put it somewhere else.
Another option could be to use a helper, ‘UserHelper’ could well have a method like so:
module UserHelper def user_online_status if @user.online? I18n.t('user.profile.online_status.online') else I18n.t('user.profile.online_status.last_online', last_online: @user.last_online) end end end
However, my preference would be to create a ‘UserPresenter’ mixin.
class UserPresenter module Mixin def online_status if online? I18n.t('user.profile.online_status.online') else I18n.t('user.profile.online_status.last_online', last_online: last_online) end end end include Mixin end
Here we have ‘UserPresenter’ which we can use standalone, and also ‘UserPresenter::Mixin’ that we can include in more specific presenters like ‘UserProfilePresenter’. Great!
Presenter objects make testing more complex edge cases of views easier. They don’t replace acceptance testing using capybara but they do help reduce the need to test views at the unit level.
Hopefully you will find use in presenters as I do for when your ActiveRecord models begin filling up with view related logic!