Rails: the quest to dumb views and skinny controllers

(como cualquier chapiadora)

Viktor Justo Vasquez

 

You can stalk me out on 

 twitter/github/instagram 

@vjustov

blog.vjustov.me

It's a mee.

Acknowledgments

Disclaimer: This is a very opinionated talk from someone who knows nothing at all.

The problem

  • The default architecture for Ruby on Rails, Model View Controller, can begin to break down as Controllers become bloated and logic begins to creep into the views. 

Shudder.

class ShoppersController < ApplicationController

  def create
    @shopper = Shopper.new(shopper_params)
    authorize @shopper
    @shopper = @shopper.decorate
    respond_to do |format|
      if @shopper.save
        unless params[:timetable_shopper_id].blank? 
        # Only link with TimetableShopper if needed.
          timetable_shopper = TimetableShopper.find(params[:timetable_shopper_id])
          timetable_shopper.shopper_id = @shopper.id
          timetable_shopper.save
        end
        flash.now[:notice] = "Shopper #{@shopper.name} was successfully created."
        format.html { 
          redirect_to( 
            edit_shopper_path(@shopper), 
            notice: "Shopper #{@shopper.name} was successfully created."
        )}
      else
        timetable_shoppers = TimetableShopper.all
        @timetable_shoppers = timetable_shoppers.decorate
        format.html { render action: 'new' }
      end
    end
  end
end

And now Shiver.

/ app/views/users/_form.html.haml

= form_for @user do |f| 
  - if @user.errors.any?
    = render "shared/form_errors", object: @user
  .field-box
    .col-xs-4.col-lg-4.col-md-3.col-sm-3
      = f.hidden_field :external_avatar_url
      - if @user.new_record?
        #avatar-box.upload-instructions.dashed_border
      - else
        #avatar-box
          = image_tag(@user.avatar.form_thumbnail.url)
    .col-xs-6
      = f.text_field :email
  .field-box
    = f.label :role, {class: "required-field"}
    .col-md-5
      .ui-select
        = collection_select @user, :role_ids, Role.all, :id, :name, 
            { checked: @user.role_ids.first, 
                include_blank:  "Please select" }, 
            { disabled: !policy(@user).can_change_role? }

Pfft, Noob, Wouldn't helper methods solve that?

(In case you, the spectator, didn't know)

Helpers are usually generic functions that assist in providing some functionality to whichever objects it is "helping". Nothing too fancy here. Due to the fact that we are talking about views, these methods generally handle some sort of data representation.

module BlogsHelper
  def display_title_for(blog)
    "#{blog.name} - The Blog!"
  end
end

But helpers are shit.[1]

No seriously, helpers suck.[2]

  • Helpers aren’t objects.
  • They are included at the view with "rails magic", causing them to not have a explicit receiver, which doesn't tell you where that code might be located. 
  • Their responsibilities tend to be a little bit of everything (rendering html, perform certain validation on the the model, etc, etc...).
  • It also hurts maintainability. It adds to the temptation that it's OK to put code in a helper that shouldn’t otherwise be there.

The reasons are:

# someapp/app/views/something.html.erb
# ...
<% people_without_access_from(company).each do |person| %>
  <%= add_person_to_project_check_box(person, company) %>
<% end %>
# ...
class SomethingHelper
  def add_person_to_project_check_box(person, company)
    content_tag(:label, 
      check_box_tag("people_ids[]", person.id, false, 
        { :class => "company_#{company.id}_person" }) +
      " " + person.name
      ) + tag(:br)
  end
end

Yuck.[3]

class ProductDecorator < Draper::Decorator

# ...

  def validation_ranged_regular
    regular_ranged = h.content_tag(:label, "Regular R: ")
    if object[:ranged_regular_from]
      regular_ranged << h.number_to_currency(object[:ranged_regular_from])
      
      if object[:ranged_regular_to] do
        regular_ranged << " - " << h.number_to_currency(object[:ranged_regular_to]) 
      end
    end
    html = "" << h.content_tag(:span, regular_ranged.html_safe, class: "first-price-value")
    special_ranged = h.content_tag(:label, "Special R: ")
    if object[:ranged_special_from]
      special_ranged << h.number_to_currency(object[:ranged_special_from])
      if object[:ranged_special_to] do
        special_ranged << " - " +  h.number_to_currency(object[:ranged_special_to]) 
      end
    end
    html << h.content_tag(:span, special_ranged.html_safe, class: "first-price-value")
    html.html_safe
  end
  
# ...

end

...

Decorators

The solution

Decorator Pattern

  • The decorator pattern is a design pattern that allows behavior to be added to an individual object, without affecting the behavior of other objects from the same class.[4] 
  • Decorator is a design pattern that is used to extend the functionality of specific object by wrapping it, without effecting other instances of that object.[5]
class NiñitaPerfecta
  def ingredients
    ['Azucar', 'Flores', 'Muchos Colores']
  end
end

class ChicasSuperpoderosas < SimpleDelegator
  def ingredients
    super << 'Sustancia X'
  end
end
class Burger
  def price
    100
  end
end

class BaconCheeseBurger
  def initialize(burger)
    @burger = burger
  end

  def price
    @burger.price + 30
  end
end

class DobleCarneBurger
  def initialize(burger)
    @burger = burger
  end

  def price
    @burger.price + 50
  end
end

DobleCarneBurger.new(
  BaconCheeseBurger.new(
    Burger.new
  )
).price 
=> 180

How does a decorator solve our problem?

Come the Presenter

  • So, Presenters are just decorators. 
  • The main goal of presenters is to keep logic out of the view.
  • Its secondary goal is to keep related functionality, which would have previously existed in helpers, in close proximity to the relevant objects. Presenters maintain an object-oriented approach to logic in the view.
  • If you have conditionals in your views, you'll likely benefit greatly from moving that logic to a presenter. [6] 

They TURN your controllers from this:

class ProductsController < ApplicationController
  def new
    @page = Page.find(params[:page_id])
    @product = @page.products.new(crop_params).decorate
    @brands = Brand.active.pluck(:name, :id)
    @units = Unit.active.pluck(:name, :id)
  end
end

Into this:

class ProductsController < ApplicationController
  def new
    @presenter = NewProductPresenter.new(products_params)
  end
end
  • The Presenter pattern creates a class representation of the state of the view.
  • A Presenter can encapsulate aggregating data from various objects and leave the controller with more focused responsibilities.
  • This also allows for testing of aggregation or calculation behavior without a dependency on setting up a Controller to a testable state.
class CarsController < ApplicationController
  def show
    car = Car.find(params[:id])
    dealer = Dealer.find(params[:dealer_id])

    @car = ViewCarPresenter.new(car, dealer)
  end
end
class ViewCarPresenter
  def initialize(car, dealer)
    @car, @dealer = car, dealer
  end

  def availability
    count = dealer.cars_in_stock.select { |c| c == car }.count
    "There are #{count} of this car."
  end

  def price_description
    if price > 500_000
      "Expensive piece of metal this one is!!"
    else
      "Damn it's was cheap!!"
    end
  end
end
#car-view
  .availability
    = @car.availability
  .comments
    = @car.price_description
describe ViewCarPresenter do 
  let(:car) { double(car, price: 2000) }
  let(:dealer) { double('dealer', cars_in_stock: [car]) }

  let(:presenter) { ViewCarPresenter.new(car ,dealer) }

  it 'calculates the availability' do
    expect(presenter.availability).to eql('There are 1 of this car.')
  end
    
  it "renders the price description" do
    expect(presenter.price_description).to eql("Damn it's was cheap!!")
  end
end

Disclaimer:  The Rails community have come to a confusion regarding what's a presenter. I'm using Jay Fields[7] and Avdi Grimm's[8] terminology.

 

ALSO, I KNOW THIS SOUNDS A WHOLE LOT like form objects, view-models, carriers, and a bunch of other objects you might have read on the internet

Enter the Exhibits

  • Wraps a single model instance.

  • Encapsulates decisions about how to render an object. The tell-tale of an Exhibit is telling an object "render yourself", rather than explicitly rendering a template and passing the object in as an argument.

  • Brings together a model and a context. Exhibits need a reference to a "context" object—either a controller or a view context—in order to be able to render templates as well as construct URLs for the object or related resources.

class ReadingListExhibit < DisplayCase::Exhibit

  def self.applicable_to?(object)
    object_is_any_of?(object, 'ReadingList')
  end

  def to_json(options={})
    output = "["
    books[0..-2].each do |book|
      output << " { \"book\": #{book.to_json}"
      output << " },\n"
    end
    output << " book: { #{books[-1].to_json} } "
    output << "]"
  end

  def to_csv(options={})
    output = ""
    books.each do |book|
      output << "\"#{book.title}\", \"#{book.author}\", \"#{book.publisher}\" \n"
    end
    output
  end

  def render_navigation(template)
    template.render(
      :partial => "shared/reading_list_navigation", 
      :locals => { 
        :selected_reading_list => self.name, 
        :other_reading_lists => ReadingList.without_current(__getobj__) 
        }
    )
  end

  def render_books(template)
    template.render(:partial => "shared/book", :collection => self.books)
  end

  def render_overview(template)
    template.render(:partial => 'shared/reading_list', :object => __getobj__) 
  end
end
class BookExhibit < DisplayCase::Exhibit
  def self.applicable_to?(object)
    Rails.logger.debug("app/exhibiit #{object}")
    object_is_any_of?(object, 'Book')
  end

  def render_book(template)
    Rails.logger.info(template.class)
    template.render(
      partial: 'shared/book', 
      locals: { :book => __getobj__})
  end
end
<div class="row-fluid">
  <div class="span8">
    <% @books.each do |book| %>
      <%= book.render_book(self) %>
    <% end %>
   </div>
   <div class="span2 well">
      Suggested Reading Lists:
      <% @reading_lists.each do |e| %>
        <%= e.render_overview(self) %>
      <% end %>
   </div>
</div>
class ReadingListsController < ...
  respond_to :html

  def index
    @books = exhibit(Book.all)
    @reading_lists = exhibit(ReadingList.all)
    respond_with(@reading_lists, @books)
  end
end

View

Model

Presenters

Exhibits

 

View-model spectrum

Let's merge 'em together: Presenter + Exhibits

Presenters and exhibits are not mutually exclusive.

 We can combine presenters and exhibits to take advantage of both. We use presenters to deal with any view-related logic. We use exhibits to manage rendering the objects.

class ShowAllReadingListsPresenter
  attr_reader :books, :reading_list

  def initialize(books, reading_list)
    @books = BooksExhibit.new(books)
    @reading_list = ReadingListExhibit.new(reading_list) 
  end

end
<div class="row-fluid">
  <div class="span8">
    <% @presenter.books.each do |book| %>
      <%= book.render_book(self) %>
    <% end %>
   </div>
   <div class="span2 well">
      Suggested Reading Lists:
      <% @presenter.reading_lists.each do |e| %>
        <%= e.render_overview(self) %>
      <% end %>
   </div>
</div>
class ReadingListsController 
  respond_to :html

  def index
    @presenter = ViewAllReadingList.new(
        Books.all, 
        ReadingList.all, 
        self
    )
  end
end

So, what about what i read on the internet this morning?

"Classic" presenters are oriented towards a particular view; they are, in Jay Fields' words, "class versions of your views". They have names like ReportPresenter or OrderCompletionPresenter.

In contrast, this second generation of presenters are oriented primarily towards specific models. They have names like UserPresenter or PicturePostPresenter. They enable a particular model instance to render itself to a page.

  • Regarding decorators, I personally prefer to refer to them as such only when we are using them in a general way, with no particular interest on views or anything.

Word of warning

Sources

  1. http://nicksda.apotomo.de/2011/10/rails-misapprehensions-helpers-are-shit/
  2. http://blog.steveklabnik.com/posts/2011-09-09-better-ruby-presenters
  3. https://signalvnoise.com/posts/1108-what-belongs-in-a-helper-method
  4. http://en.wikipedia.org/wiki/Decorator_pattern
  5. http://stackoverflow.com/a/16237911/2525603
  6. http://mikepackdev.com/blog_posts/31-exhibit-vs-presenter
  7. http://blog.jayfields.com/2007/03/rails-presenter-pattern.html
  8. http://objectsonrails.com/#ID-2656c30c-080a-4a4e-a53e-4fbaad39c262

> file.EOF?
=> TRUE

Made with Slides.com