January 14, 2020

Using facades to streamline your controllers

Enter facades, the perfect diet pill for your fat Ruby on Rails controllers.

It's inevitable that sometimes controller actions can scale with more than one instance variable. As a rule of thumb, I like to keep the maximum amount of instance variables available to one or two; this is where facades come in extra handy.

Facades are Plain Old Ruby Objects (POROs) that can be defined within your Ruby on Rails application to help with the passing of data from models to views; they should not contain are create, update or destroy actions, simply just reading.

Before we begin with this walkthrough, let's assume the role of having a website that has pages, advertisements and testimonials defined as models.

Before you begin

Create an application

Let's create a sample Rails application to hold our models and our explain facade.

Create an application as follows (we're not using any RDBMS; simply sticking with SQLite is more than enough for testing):

rails new getting_funky_with_facades

Then, let's create some models. You will note that not all fields have the data-type passed to them, as it is implied as a string by default:

rails g model Page title content:text
rails g model Advertisement title summary:text button_text button_link
rails g model Testimonial author_name date:date content:text

Now, let's seed some data. Copy and paste the following to your db/seeds.rb file so we can add some dummy content.

Page.create!(
  title: 'About Us',
  content: '<p>Hello world.</p>'
)

Advertisement.create!(
  title: 'Check out Read and Write: Rails',
  summary: "It's the one-stop place for Ruby on Rails tutorials for developers of all levels.",
  button_text: 'Show me!',
  button_link: 'https://rails.readandwrite.io/'
)

Testimonial.create!(
  author_name: 'John Smith',
  date: Date.today,
  content: "I said something really good and now you can see it forever."
)

The last step is to feed your data to the hungry database monster, but first, we need to migrate.

rails db:migrate

Now, let's feed (or seed, as it's more transitionally known), the data to the database:

rails db:seed

Create some output

Now we have the data, but no way of viewing it, so let's change that.

Firstly, let's open config/routes.rb and enter:

resources :pages, only: :show

Then, let's create a controller:

rails g controller Pages show --skip-routes

Now we will have lots of files, but the two important ones here are:

  • app/controllers/pages_controller.rb
  • app/views/pages.html.erb

We will use them moving forward.

Outputting your data

If you've followed the steps above, we will be working with the app/controllers/pages_controller.rb, but if not, simply replace this with a controller of your choosing.

We're going to create instance variables for the three models we created earlier.

Our show action will contain these variables:

def show
  @page = Page.find(params[:id])
  @advertisement = Advertisement.first
  @testimonial = Testimonial.first
end

Now, let's create some output in our app/views/pages.html.erb. Here's some a basic template:

<main>
  <h1><%= @page.title %></h1>

  <%= raw @page.content %>
</main>

<aside>
  <%= @advertisement.title %>
  <%= @advertisement.summary %>
  <%= link_to @advertisement.button_text, @advertisement.button_link, target: '_blank', rel: 'noopener' %>
</aside>

<blockquote>
  <%= simple_format @testimonial.content %>
  <footer><%= @testimonial.author_name %>, <%= l @testimonial.date, format: :long %></footer>
</blockquote>

This is all we need to be able to view our example page, so let's run our server:

bundle exec rails s

This will allow you to go to (http://localhost:3000)[http://localhost:3000] in a new tab.

To get to our page, simply append /pages/1, so the link becomes: (http://localhost:3000/pages/1)[http://localhost:3000/pages/1].

Getting funky with facades

So far so good - we have some content and an output with some instance variables from our controller action.

This is where we can use a facade to replace the instance variables.

To do this, we need to create a new directory named facades in our app folder, so it reads app/facades. Then, we're going to create another folder for pages. I like the use the naming scheme of controller/action for facades, so ours will be namespaced in Pages then named ShowFacade.

Let's do that now, create: app/facades/pages/show_facade.rb, with the following content:

module Pages
  #
  # Access the data required for pages/show controller method
  #
  class ShowFacade
    def page
      @page ||= Page.find(1)
    end

    def advertisement
      @advertisement ||= Advertisement.first
    end

    def testimonial
      @testimonial ||= Testimonial.first
    end
  end
end

You will note that this uses the double pipe equals operator for caching the queries too - a bonus of using facades!

But you may also note that the ID has been hard coded in to the facade. That's a problem, but we're going to fix that soon.

However, before that, let's remove our instance variables from our controller and restart our server.

def show
  @facade = Pages::ShowFacade.new
end

When we've restarted our server, and re-loaded our page, we will be greeted with a NoMethodError error. This is expected as our view still references the old instance variables. Let's update that now.

<main>
  <h1><%= @facade.page.title %></h1>

  <%= raw @facade.page.content %>
</main>

<aside>
  <%= @facade.advertisement.title %>
  <%= @facade.advertisement.summary %>
  <%= link_to @facade.advertisement.button_text, @facade.advertisement.button_link, target: '_blank', rel: 'noopener' %>
</aside>

<blockquote>
  <%= simple_format @facade.testimonial.content %>
  <footer><%= @facade.testimonial.author_name %>, <%= l @facade.testimonial.date, format: :long %></footer>
</blockquote>

You will note that I have replaced all occurances of the old instance variables to one; @facade. This helps to clean things up.

This is a simple facade setup for now, but it doesn't scale; what happens if we want to visit page ID 2 in the future? It simply won't work as the ID is hard coded to the facade.

My way of combatting this is to use a private method to pass the Page object to the facade and then use attr_accessor to access the page almost as if it's its own method.

Let's attempt that now by using the following code in our pages controller:

class PagesController < ApplicationController
  def show
    @facade = Pages::ShowFacade.new(page)
  end

  private

  def page
    Page.find(params[:id])
  end
end

Then, let's re-work out facade to accept the page object. Add an initialize method to app/facades/pages/show_facade.rb and define our attr_accessor:

def initialize(page)
  @page = page
end

attr_accessor :page

With this, we can now remove the page method so the facade becomes:

module Pages
  #
  # Access the data required for pages/show controller method
  #
  class ShowFacade
    def initialize(page)
      @page = page
    end

    attr_accessor :page

    def advertisement
      @advertisement ||= Advertisement.first
    end

    def testimonial
      @testimonial ||= Testimonial.first
    end
  end
end

This now tidies up our controller and defines our first simple facade for re-usable objects.

When you compare the multiple instance methods of:

def show
  @page = Page.find(params[:id])
  @advertisement = Advertisement.first
  @testimonial = Testimonial.first
end

to our much cleaner:

def show
  @facade = Pages::ShowFacade.new(page)
end

It's clear that facades have their advantages; they can also potentially help with the Law of Demeter but that will be covered in a future post.

Feedback and comments

Do you like what you see? Has it helped you structure your Ruby on Rails controllers in a much neater way? Hop over to our contact page and let us know!