DEV Community

K Putra
K Putra

Posted on • Updated on

Rails: Facade Design Pattern for Index

If you continue to read this article, I assume that you know Ruby, OOP in Ruby, RoR, Active Record, and a little bit metaprogramming.

What is Facade Design Pattern?

Facade Design Patter provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.

source: GoF in Design Patterns - Elements of Reusable Object-Oriented Software

Perhaps you should read these articles before we start:
Rails: Query Object Pattern Implementation
Rails: skinny controller, skinny model

Let's start our journey! (I use Rails API-only as example, but this article can be implemented in normal Rails as well)

Table of Contents:
1. Problems
2. First Solution (bad)
3. Second Solution (a bit better)
4. Third Solution (much better)
5. The Cost
6. Final Word

1. Problems

In Fintax, the FE team asked me (the only one BE developer) to give this kind of response for index api:

{
    "object": [
        {
            "column": "object1"
        },
        {
            "column": "object2"
        }
    ],
    "page": {
        "total_record": 20,
        "per_page": 2
    }
}
Enter fullscreen mode Exit fullscreen mode

Okay, it's not hard. Let's move to first solution.

2. First Solution (bad)

Let assume we use will_paginate gems (I actually use that). For beginner, these will be their controllers:

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    users = User.where(nil)
    users = users.where(role: params[:role]) if params[:role]
    users = users.where(status: params[:status]) if params[:status]
    users = users.where(public_id: params[:public]) if params[:public]
    users = users.paginate(page: params[:page], per_page: 2)
    users_count = users.count
    render json: { object: users, page: { total_record: users_count, per_page: 2 }}, status: 200
  end
end

# app/controllers/companies_controller.rb
class CompaniesController < ApplicationController
  def index
    companies = Company.where(nil)
    companies = companies.where("name like ?", "#{params[:name]}%") if params[:name]
    companies = companies.where(tax_id: params[:tax]) if params[:tax]
    companies = companies.paginate(page: params[:page], per_page: 2)
    companies_count = companies.count
    render json: { object: companies, page: { total_record: companies_count, per_page: 2 }}, status: 200
  end
end
Enter fullscreen mode Exit fullscreen mode

This kind of controllers is very bad, because of various reasons:

  1. I have a dozen of models that need to be indexed (and this number will grow in the future), so this kind of code not only very repetitive, but also very hard to maintained;
  2. The total objects per page are not dynamic, so if the FE team asked to change it, we have to change all controllers;
  3. We send every columns exist in the model to FE;
  4. etc.

Let's move to our next solution.

3. Second Solution (a bit better)

We will use filterable module which I explain in my previous article.

We will use pluck. To be precise, I use pluck_all gems. Why we use pluck? So we don't send every columns to FE. We just give them what they need. Why we don't use as_json or map? Because pluck is better in performance.

We will make our pagination to become dynamic. Tell FE team that they can throw per_page parameter if someday they want to change total objects per page.

So, our controllers and models will be like this:

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    users = User.call_index(params)
    users_count = users.count
    users = users.show_index
    render json: { object: users, page: { total_record: users_count, per_page: (params[:per_page] || 2) }}, status: 200
  end
end

# app/controllers/companies_controller.rb
class CompaniesController < ApplicationController
  def index
    companies = Company.call_index(params)
    companies_count = companies.count
    companies = companies.show_index
    render json: { object: companies, page: { total_record: companies_count, per_page: (params[:per_page] || 2) }}, status: 200
  end
end

# app/models/user.rb
class User < ApplicationRecord
  include Filterable

  scope :role,   -> (role) { where(role: role) }
  scope :status, -> (status) { where(status: status) }
  scope :public, -> (public_id) { where(public_id: public_id) }

  def self.call_index(params)
    filter(params.slice(:role, :status, :public))
    .paginate(page: params[:page], per_page: (params[:per_page] || 2)
  end

  def self.show_index
    pluck_all(:id, :role, :status, :public)
  end
end

# app/models/company.rb
class Company < ApplicationRecord
  include Filterable

  scope :name, -> (name) { where("name like ?", "#{name}%") }
  scope :tax,  -> (tax_id) { where(tax_id: tax_id) }

  def self.call_index(params)
    filter(params.slice(:name, :tax))
    .paginate(page: params[:page], per_page: (params[:per_page] || 2)
  end

  def self.show_index
    pluck_all(:id, :name, :tax)
  end
end
Enter fullscreen mode Exit fullscreen mode

I hope you realize 2 things:

First, I use pluck after counting total object.

If I use count after pluck, I call method count from Array class, not count from Active Record. So, I'll get length of array instead of total object from active record.

Second, if you have already read Rails: Refactor Your Where Method, notice that I delete filtering_(params) method from controllers, and filter the params in models.

If I filter the params in controller as before and add page and per_page parameters, then the filter method will search scope named page and per_page. Because these scopes do not exist, then rails will throw error.

Tips: There are many ways to move params.slice() to another method. Why we need to move it? Imagine you have 10 keys in the params hash! One of the ways are: we create class inside model, like the code below. That way, the code is more cleaner (method should do one thing), and params.slice() is reusable inside the model.

# app/models/user.rb
class User < ApplicationRecord
  include Filterable

  scope :role,   -> (role) { where(role: role) }
  scope :status, -> (status) { where(status: status) }
  scope :public, -> (public_id) { where(public_id: public_id) }

  def self.call_index(params)
    filter(Whitelisting.call(params))
    .paginate(page: params[:page], per_page: (params[:per_page] || 2)
  end

  def self.show_index
    pluck_all(:id, :role, :status, :public)
  end

  class Whitelisting
    def self.call(params)
       params.slice(:role, :status, :public)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Okay, this is better than initial solution. Now, let's implement Facade Pattern !

4. Third Solution (much better)

Let's implement Facade Pattern. We'll make app/lib directory, and create index_facade.rb.

In short, index_facade.rb is the unified interface to a set of interfaces in a subsystem.

# app/lib/index_facade.rb
class IndexFacade
  def initialize(model, params)
    @model   = model.constantize
    @params  = params
    @objects = objects
  end

  def call
    {
      object: @objects.show_index
      page: {
        total_record: @objects.counts
        per_page: per_page
      }
    }
  end

  private

  def objects
    @model.call_index(@params)
  end

  def per_page
    @params[:per_page] || 2
  end
end
Enter fullscreen mode Exit fullscreen mode

Now, we'll update our controllers:

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    users = IndexFacade.new('User', params).call
    render json: users, status: 200
  end
end

# app/controllers/companies_controller.rb
class CompaniesController < ApplicationController
  def index
    companies = IndexFacade.new('Company', params).call
    render json: companies, status: 200
  end
end
Enter fullscreen mode Exit fullscreen mode

Voila! Look at our new controllers!

5. The Cost

The only costs are:

In every model, we have to include Filterable.

In every model, we have to add call_index and show_index method, as in User and Company.

What if I want index of Article is giving every column and have no filter?

# app/models/article.rb
class Article < ApplicationController
  include Filterable

  def self.call_index(params)
    filter(params.slice())
    .paginate(page: params[:page], per_page: (params[:per_page] || 2)
  end

  def self.show_index
    pluck_all
  end
end

# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    articles = IndexFacade.new('Article', params).call
    render json: articles, status: 200
  end
end
Enter fullscreen mode Exit fullscreen mode

6. Final Word

If you have opinion or better implementation, or may be I was wrong about Facade Pattern, let discuss in comment.

source: myself

Oldest comments (0)