2 patterns for refactoring with your ruby application
Nov 15, 2013 - 3 minutesWhen 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
.
1
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'
8
9 query = People
10 query = query.where(name: name) if name.present?
11 @results = query.order("#{sort} #{direction}").page(page).per_page(per_page)
12end
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.
1
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
12
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
17end
Now our controller looks like this:
1
2def index
3 query = PeopleQuery.new(params[:page], params[:per_page], params[:name], params[:sort], params[:direction])
4 @results = query.build
5end
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
11
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
16end
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.
1
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
11end
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.