{ Josh Rendek }

<3 Go & Kubernetes

When working on a rails application you can sometimes find duplicated or very similar code between two different controllers (for instance a UI element and an API endpoint). Realizing that you have this duplication there are several things you can do. I’m going to go over how to extract this code out into the query object pattern 1 and clean up our constructor using the builder pattern 2 adapted to ruby.

I’m going to make a few assumptions here, but this should be applicable to any data access layer of your application. I’m also assuming you’re using something like Kaminari for pagination and have a model for People.

 2def index
 3  page = params[:page] || 1
 4  per_page = params[:per_page] || 50
 5  name = params[:name]
 6  sort = params[:sort_by] || 'last_name'
 7  direction = params[:sort_direction] || 'asc'
 9  query = People
10  query = query.where(name: name) if name.present?
11  @results = query.order("#{sort} #{direction}").page(page).per_page(per_page)

So we see this duplicated elsehwere in the code base and we want to clean it up. Lets first start by extracting this out into a new class called PeopleQuery.

I usually put these under app/queries in my rails application.

 2class PeopleQuery
 3  attr_accessor :page, :per_page, :name, :sort, :direction, :query
 4  def initialize(page, per_page, name, sort, direction)
 5    self.page = page || 1
 6    self.per_page = per_page || 50
 7    self.name = name
 8    self.sort = sort || 'last_name'
 9    self.direction = direction || 'asc'
10    self.query = People
11  end
13  def build
14    self.query = self.query.where(name: self.name) if self.name.present?
15    self.query.order("#{self.sort} #{self.direction}").page(self.page).per_page(self.per_page)
16  end

Now our controller looks like this:

2def index
3  query = PeopleQuery.new(params[:page], params[:per_page], params[:name], params[:sort], params[:direction])
4  @results = query.build

Much better! We’ve decoupled our control from our data access object (People/ActiveRecord), moved some of the query logic outside of the controller and into a specific class meant to deal with building it. But that constructor doesn’t look very nice. We can do better since we’re using ruby.

Our new PeopleQuery class will look like this and will use a block to initialize itself instead of a long list of constructor arguments.

 1class PeopleQuery
 2  attr_accessor :page, :per_page, :name, :sort, :direction, :query
 3  def initialize(&block)
 4    yield self
 5    self.page ||= 1
 6    self.per_page =|| 50
 7    self.sort ||= 'last_name'
 8    self.direction ||= 'asc'
 9    self.query = People
10  end
12  def build
13    self.query = self.query.where(name: self.name) if self.name.present?
14    self.query.order("#{self.sort} #{self.direction}").page(self.page).per_page(self.per_page)
15  end

We yield first to let the caller set the values and then after yielding we set our default values if they weren’t passed in. There is another method of doing this with instance_eval but you end up losing variable scope and the constructor looks worse since you have to start passing around the params variable to get access to it, so we’re going to stick with yield.

 2def index
 3  query = PeopleQuery.new do |query|
 4    query.page = params[:page]
 5    query.per_page = params[:per_page]
 6    query.name = params[:name]
 7    query.sort = params[:sort]
 8    query.direction = params[:direction]
 9  end
10  @results = query.build

And that’s it! We’ve de-duplicated some code (remember we assumed dummy controller’s index method was duplicated elsewhere in an API call in a seperate namespaced controller), extracted out a common query object, decoupled our controller from ActiveRecord, and built up a nice way to construct the query object using the builder pattern.

comments powered by Disqus