Simple View Presenter Pattern in Rails without using Gems

 
Rails presenter/decorator pattern is represented by disquised guys Photo by Pixabay from Pexels

What’s the size of user.rb file in your current project? The default Rails way model-view-controller architecture often leads to a bloated model layer. Presenter pattern is one of the more straightforward ways to slim down your Rails models. It also helps to reduce the logic in the view layer and makes testing easier. In this blog post, I’ll describe how to implement a presenter pattern without including additional gem dependencies.

How not to views

Before we jump into coding our presenters, let’s first quickly discuss the shortcoming of the default approach of model + view. For the sake of this tutorial, let’s assume we want to code a feature displaying a number of unread notifications.

Without implementing additional layers of abstraction, we can either put the related logic into the model or view files. Sample implementation in the view partial could look like that:

app/views/user/_notifications.html.erb

<div id="notifications">
  <% unread_count = @current_user.notifications.unread.count %>
  <% if unread_count > 0 %>
    You have <%= unread_count %> unread
    <%= pluralize(unread_count, "notification") %>
  <% else %>
    You don't have unread notifications.
  <% end %>
</div>

Despite the logic being relatively simple, there’s already a lot of the erb syntax overhead. Another downside of putting logic into views is that it’s challenging to test it in isolation. You must run slow integration tests or configure controller specs to render views. Unfortunately, it’s sometimes problematic to simulate different edge cases without resorting to hard-to-maintain stubbing techniques. If logic is complex, I usually prefer to implement a single “happy path” integration spec and test edge cases using low-level unit tests.

Testing would be simpler if we implemented the same logic directly in the model:

app/model/user.rb

include ActionView::Helpers::TextHelper

# ...

def unread_notifications_text
  unread_count = notifications.unread.count

  if unread_count == 0
    return "You don't have unread notifications."
  end

  "You have %{unread_count} unread %{pluralize(unread_count, 'notification')}".
end

The logic is easier to grasp without all the noise added by the erb syntaxt. But the inclusion of ActionView::Helpers::TextHelper in the model indicates that we’re mixing up unrelated layers. Writing code like that is a one-way ticket to bloating your models to an unmaintainable mess.

Why not helpers?

Another way to implement this logic in a classical Rails way would be to use a built-in Rails helper (files inheriting from ApplicationHelper class). But, I usually try to avoid them. The same as in the case of views, they are challenging to test in isolation, and have a global scope. It means that you have to be careful to keep methods naming unique across ALL your helpers. From my experience refactoring legacy projects, putting logic into helpers can also lead to the terrible practice of including them in controllers to keep things DRY

Presenters to the rescue

Presenters are a popular programming pattern, and there’s an abundance of gems that implement it. But, in this tutorial, I’ll describe a from-scratch approach.

Just one more gem?

Based on the experience from my Rails performance audits, memory issues are the most common obstacle for projects to achieve adequate performance and scalability. And overusing gem dependencies is a simple way to unnecessarily bloat a Rails app memory usage. I mention memory issues because a not-so-long-ago popular Draper gem is infamous for its elevated memory consumption. Apparently, it’s no longer maintained with the last commit older than a year ago. There are successors to Draper improving on its shorcommings. But, adding a gem is always a risky investment, so in this tutorial, we’ll focus on a custom solution.

Implementing Rails presenters from scratch

Let’s start by defining a module:

app/models/concerns/presentable.rb

module Presentable
  def decorate
    "#{self.class}Presenter".constantize.new(self)
  end
end

and now include it in your model:

app/models/user.rb

class User < ApplicationRecord
  include Presentable

  # ...
end

The Presentable module uses Ruby metaprogramming to dynamically define a presenter class. It enforces a naming convention for presenter files. Let’s now create one:

app/presenters/user_presenter.rb

class UserPresenter < SimpleDelegator
  include ActionView::Helpers::TextHelper

  def unread_notifications_text
    unread_count = notifications.unread.count

    if unread_count == 0
      return "You don't have unread notifications."
    end

    "You have %{unread_count} unread %{pluralize(unread_count, 'notification')}".
  end
end

It inherits a SimpleDelegator class and is instantiated with the model object itself. It means that all the method calls will well delegate to the original. Now inside the controller, your can use your presenter like that:

app/controllers/users_controller.rb

class UsersController < ApplicationController
  # ...

  def index
    @users = User.recent.limit(50).map(&:decorate)
  end
end

By calling decorate on User model instances, you’re wrapping them inside a UserPresenter class and extending their API. You can interact with the new objects like before but also use all the methods defined inside a presenter.

This approach allows encapsulating logic related to the view layer in a separate file and helps to keep your model files from bloating their size.

Compared to a dedicated gem, this method is lightweight on memory and complexity. Just a few lines of metaprogramming and Ruby stdlib in action.

Testing presenters

As previously mentioned, since our presenters are POROs (Plain Old Ruby Objects), it’s straightforward to test them. Here’s a sample spec for our logic:

spec/presenters/user_presenter_spec.rb

require "rails_helper"

describe UserPresenter do
  let(:subject) do
    user.decorate
  end

  let(:user) do
    create(:user, notifications: [
        create(:notification),
        create(:notification)
      ]
    )
  end

  describe "#unread_notifications_text" do
    expect(subject.unread_notifications_text).to include("2 unread notifications")
  end
end

As you can see, we can test view-related logic without the usual overhead of view specs. There’s no need to authenticate the user, stub session state, render views, or launch a test browser. While high-level integration tests also have their value and place, testing more complex edge cases is usually an order of magnitude simpler with low-level unit tests.

Summary

Implementing presenters can be a relatively simple introduction to programming patterns that are not part of an orthodox Rails way. Organising codebase in a way that focuses on the logical layers of application is an approach that works for MVPs and also scales for more complex Rails projects.



Back to index