In today's post, we will be looking into a software design pattern called Facade. When I first adopted it, it felt a little bit awkward, but the more I used it in my Rails apps, the more I started to appreciate its usefulness. More importantly, it allowed me to test my code more thoroughly, to clean out my controllers, to reduce the logic within my views and to make me think more clearly about an application's code's overall structure.
Being a software development pattern, facade is framework agnostic but the examples I will provide here are for Ruby on Rails. However, I encourage you to read through this article and try them out regardless of the framework you are using. I'm sure that once you become familiar with this pattern, you will start seeing opportunities to use it in many parts of your codebase.
Without further ado, let's dive right in!
The Problem with the MVC Pattern
The MVC (Model-View-Controller) pattern is a software development pattern that dates back to the 1970s. It's a battle-tested solution for designing software interfaces, separating programming concerns into three main groups that communicate amongst each other in a unique way.
Many large web frameworks emerged in the early 2000s with the MVC pattern as their foundation. Spring (for Java), Django (for Python) and Ruby on Rails (for Ruby), were all forged with this trinity of interconnected elements at their core. Compared to the spaghetti-code resulting from software that did not make use of it, the MVC pattern was a huge achievement and turning point in the evolution of both software development and the internet.
In essence, the Model-View-Controller pattern allows for the following: a user performs an action on the View. The View triggers a request to a Controller which can potentially create/read/update or delete a Model. The Model transaction responds back to the Controller, which in turn renders some change that the user will see reflected in the View.
There are plenty of pros to this programming pattern. To list some:
- It improves code maintainability by separating concerns
- It allows for greater testability (the Models, Views and Controllers can be tested in isolation)
- It encourages good coding practices by enforcing the Single Responsibility Principle of SOLID: "A class should have only one reason to change."
A phenomenal achievement for its time, developers soon realized that the MVC pattern was also somewhat limiting. Variants started to emerge, such as HMVC (hierarchical model–view–controller), MVA (model–view–adapter), MVP (model–view–presenter), MVVM (model–view–viewmodel) and others, which all sought to address the limitations of the MVC pattern.
One of the problems that the MVC pattern introduces, and the topic of today's article, is the following: who is responsible for handling complex view logic? The view should simply be concerned with presenting the data, the controller is just relaying the message it received from the model, and the model should not be concerned with any view logic.
To help with this common conundrum, all Rails applications get initialized with a helpers
directory. The helper
directory can contain modules with methods that assist in complex View logic.
Here is an example of a helper within a Rails application:
app/helpers/application_helper.rb
1module ApplicationHelper
2 def display_ad_type(advertisement)
3 type = advertisement.ad_type
4 case type
5 when 'foo'
6 content_tag(:span, class: "foo ad-#{type}") { type }
7 when 'bar'
8 content_tag(:p, 'bar advertisement')
9 else
10 content_tag(:span, class: "badge ads-badge badge-pill ad-#{type}") { type }
11 end
12 end
13end
This example is simple but demonstrates the fact that you would want to extract this kind of decision making from the template itself in order to reduce its complexity.
Helpers are nice, but there is yet another pattern for handling complicated View logic that has become accepted through the years, and that is the Facade pattern.
Introduction to the Facade Pattern
In a Ruby on Rails application, facades are usually placed within the app/facades
directory.
While similar to helpers
, facades
are not a group of methods within a module. A Facade is a PORO (Plain Old Ruby Object) that is instantiated within the controller, but one that handles elaborate View business logic. As such, it allows the following benefits:
- Rather than having a single module for
UsersHelper
orArticlesHelper
orBooksHelper
, each controller action can have its own Facade:Users::IndexFacade
,Articles::ShowFacade
,Books::EditFacade
. - More so than modules, facades encourage good coding practices by allowing you to nest facades to ensure the Single Responsibility Principle is enforced. While you probably don't want facades that are nested hundreds of levels deep, having one or two layers of nesting for improved maintainability and test coverage can be a good thing.
Here is a contrived example:
1module Books
2 class IndexFacade
3 attr_reader :books, :params, :user
4
5 def initialize(user:, params:)
6 @params = params
7 @user = user
8 @books = user.books
9 end
10
11 def filtered_books
12 @filtered_books ||= begin
13 scope = if query.present?
14 books.where('name ILIKE ?', "%#{query}%")
15 elsif isbn.present?
16 books.where(isbn: isbn)
17 else
18 books
19 end
20
21 scope.order(created_at: :desc).page(params[:page])
22 end
23 end
24
25 def recommended
26 # We have a nested facade here.
27 # The `Recommended Books` part of the view has a
28 # single responsibility so best to extract it
29 # to improve its encapsulation and testability.
30 @recommended ||= Books::RecommendedFacade.new(
31 books: books,
32 user: user
33 )
34 end
35
36 private
37
38 def query
39 @query ||= params[:query]
40 end
41
42 def isbn
43 @isbn ||= params[:isbn]
44 end
45 end
46end
When Not to Use the Facade Pattern
Let's take a moment to also reflect on what facades are not.
Facades should not be placed in classes that live, for example, in the
lib
directory for code that needs to be displayed in the View. The facade's lifecycle should be generated in the Controller action and be used in its associated View.Facades are not meant to be used for business logic to perform CRUD actions (there are other patterns for that, such as Services or Interactors—but that is a subject for another day.) In other words, facades should not be concerned with creating, updating or deleting. Their aim is to extract intricate presentation logic from the View or Controller and offer a single interface to access all that information.
Last but not least, Facades are not a silver bullet. They do not allow you to bypass the MVC pattern, but rather, they play along with it. If a change occurs in a Model, it will not be immediately reflected in the View. As is always the case with MVC, the controller action would have to be re-rendered in order for the Facade to display changes on the View.
Controller Benefits
One of the main, obvious benefits of Facades is that they will allow you to dramatically reduce the controller logic.
Your controller code will be reduced from something like this:
1class BooksController < ApplicationController
2 def index
3 @books = if params[:query].present?
4 current_user.books.where('name ILIKE ?', "%#{params[:query]}%")
5 elsif params[:isbn].present?
6 current_user.books.where(isbn: params[:isbn])
7 else
8 current_user.books
9 end
10
11 @books.order(created_at: :desc).page(params[:page])
12 @recommended = @books.where(some_complex_query: true)
13 end
14end
To this:
1class BooksController < ApplicationController
2 def index
3 @index_facade = Books::IndexFacade.new(user: current_user, params: params)
4 end
5end
View Benefits
For the Views, there are two main benefits when using Facades:
- Conditional checks, inline queries and other logic can be neatly extracted from the template itself making the code far more readable. For instance, you could use it in a form:
1<%= f.label :location %>
2<%= f.select :location, options_for_select(User::LOCATION_TYPES.map { |type| [type.underscore.humanize, type] }.sort.prepend(['All', 'all'])), multiple: (current_user.active_ips.size > 1 && current_user.settings.use_multiple_locations?) %>
Could just become:
1<%= f.label :location %>
2<%= f.select :location, options_for_select(@form_facade.user_locations), multiple: @form_facade.multiple_locations? %>
- Variables that get called multiple times can be cached. This can offer significant performance improvements to your app and help remove pesky N+1 queries:
1// Somewhere in the view, a query is performed.
2<% current_user.books.where(isbn: params[:isbn]).each do |book| %>
3 // Do things
4<% end %>
5
6// Somewhere else in the view, the same query is performed again.
7<% current_user.books.where(isbn: params[:isbn]).each do |book| %>
8 // Do things
9<% end %>
would become:
1// Somewhere in the view, a query is performed.
2<% @index_facade.filtered_books.each do |book| %>
3 // Do things
4<% end %>
5
6// Somewhere else in the view.
7// Second query is not performed due to instance variable caching.
8<% @index_facade.filtered_books.each do |book| %>
9 // Do things
10<% end %>
Testing Benefits
A major benefit of Facades is that they allow you to test singular bits of business logic without having to write an entire controller test, or worse, without having to write an integration test that goes through a flow and reaches a page just to ensure that the data presentation is as expected.
As you will be testing single POROs, this will help maintain a fast test suite.
Here is a simple example of a test written in Minitest for demonstration purposes:
1require 'test_helper'
2
3module Books
4 class IndexFacadeTest < ActiveSupport::TestCase
5 attr_reader :user, :params
6
7 setup do
8 @user = User.create(first_name: 'Bob', last_name: 'Dylan')
9 @params = {}
10 end
11
12 test "#filtered_books returns all user's books when params are empty"
13 index_facade = Books::IndexFacade.new(user: user, params: params)
14
15 expectation = user.books.order(created_at: :desc).page(params[:page])
16
17 # Without writing an entire controller test or
18 # integration test, we can check whether using the facade with
19 # empty parameters will return the correct results
20 # to the user.
21 assert_equal expectation, index_facade.filtered_books
22 end
23
24 test "#filtered_books returns books matching a query"
25 @params = { query: 'Lord of the Rings' }
26 index_facade = Books::IndexFacade.new(user: user, params: params)
27
28 expectation = user
29 .books
30 .where('name ILIKE ?', "%#{params[:query]}%")
31 .order(created_at: :desc)
32 .page(params[:page])
33
34 assert_equal expectation, index_facade.filtered_books
35 end
36 end
37end
Unit testing facades considerably improves test suite performance, and every large company will eventually encounter slow test suites unless problems like these aren’t addressed with some level of seriousness.
One Facade, Two Facades, Three Facades, More?
You might encounter a scenario where a View renders a partial that outputs some data. In that case, you have the option of either using the parent facade or using a nested facade. That largely depends on how much logic is involved, whether you want to test it separately and whether it makes sense to extract the functionality.
There is no golden rule for how many facades to use or how many facades to nest within each other. That is to the developer's discretion. I generally prefer to have a single facade for the controller action and I limit nesting to a single level to make the code easier to follow.
Here are some general questions you can ask yourself during development:
- Does the facade encapsulate the logic I am trying to present on the view?
- Does the method within the facade make sense in this context?
- Is the code easier to follow now, or harder to follow?
When in doubt, always strive to make your code as easy to follow as possible.
Conclusion
In conclusion, facades are a fantastic pattern to keep your controllers and views lean, while improving code maintainability, performance and testability.
However, like any programming paradigm, there is no silver bullet. Even the multitude of patterns that have emerged in more recent years (HMVC, MVVM, etc.) are not be-all-end-all solutions to the complications of software development.
Similar to the second law of thermodynamics, which states that the state of entropy in a closed system will always increase, so too in any software project does the complexity increase and evolve over time. In the long run, the goal is to write code that is as easy to read, test, maintain and follow as possible; facades offer exactly this.
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!