3
\$\begingroup\$

I've been trying to simplify a search method in a project that I've taken over. Is it possible to make the code any cleaner than I already have done?

purchase_controller#search (original)

def search
 parms = search_params
 @purchases = []
 if parms[:active].nil?
 active = false
 else
 active = parms[:active] == 'true'
 end
 uid = parms[:customer_id]
 pvid = parms[:provider_id]
 pat = parms[:patient_id]
 num_records = 10 if parms[:limit].nil?
 num_records = parms[:limit].to_i unless parms[:limit].nil?
 offset = 0 if parms[:offset].nil?
 offset = parms[:offset].to_i unless parms[:offset].nil?
 if parms[:active].nil?
 # all three
 @purchases = Purchase.where(customer_id: uid, provider_id: pvid, patient_id: pid).all.order(created_at: :desc).limit(num_records).offset(offset) if !uid.nil? && !pvid.nil? && !pat.nil?
 # two of three
 @purchases = Purchase.where(provider_id: pvid, patient_id: pid).all.order(created_at: :desc).limit(num_records).offset(offset) if !pvid.nil? && !pat.nil? && uid.nil?
 @purchases = Purchase.where(customer_id: uid, patient_id: pid).all.order(created_at: :desc).limit(num_records).offset(offset) if !uid.nil? && !pat.nil? && pvid.nil?
 @purchases = Purchase.where(customer_id: uid, provider_id: pvid).all.order(created_at: :desc).limit(num_records).offset(offset) if !uid.nil? && !pvid.nil? && pat.nil?
 # one of three
 @purchases = Purchase.where(customer_id: uid).all.order(created_at: :desc).limit(num_records).offset(offset) if !uid.nil? && pvid.nil? && pat.nil?
 @purchases = Purchase.where(provider_id: pvid).all.order(created_at: :desc).limit(num_records).offset(offset) if uid.nil? && !pvid.nil? && pat.nil?
 @purchases = Purchase.where(patient_id: pid).all.order(created_at: :desc).limit(num_records).offset(offset) if uid.nil? && pvid.nil? && !pat.nil?
 # none of three
 @purchases = Purchase.all.order(created_at: :desc).limit(num_records).offset(offset) if uid.nil? && pvid.nil? && pat.nil?
 else
 # all three
 @purchases = Purchase.where(customer_id: uid, provider_id: pvid, active: active, patient_id: pid).offset(offset).all.order(created_at: :desc).limit(num_records) if !uid.nil? && !pvid.nil? && !pat.nil?
 # two of three
 @purchases = Purchase.where(provider_id: pvid, active: active, patient_id: pid).all.order(created_at: :desc).limit(num_records).offset(offset) if !pvid.nil? && !pat.nil? && uid.nil?
 @purchases = Purchase.where(customer_id: uid, active: active, patient_id: pid).all.order(created_at: :desc).limit(num_records).offset(offset) if !uid.nil? && !pat.nil? && pvid.nil?
 @purchases = Purchase.where(customer_id: uid, provider_id: pvid, active: active).all.order(created_at: :desc).limit(num_records).offset(offset) if !uid.nil? && !pvid.nil? && pat.nil?
 # one of three
 @purchases = Purchase.where(customer_id: uid, active: active).all.order(created_at: :desc).limit(num_records).offset(offset) if !uid.nil? && pvid.nil? && pat.nil?
 @purchases = Purchase.where(provider_id: pvid, active: active).all.order(created_at: :desc).limit(num_records).offset(offset) if uid.nil? && !pvid.nil? && pat.nil?
 @purchases = Purchase.where(active: active, patient_id: pid).all.order(created_at: :desc).limit(num_records).offset(offset) if uid.nil? && pvid.nil? && !pat.nil?
 # none of three
 @purchases = Purchase.where(active: active).all.order(created_at: :desc).limit(num_records).offset(offset) if uid.nil? && pvid.nil? && pat.nil?
 end
 @purchases.each do |hc|
 hc.stripe_charge_id = nil
 hc.stripe_transaction_id = nil
 end
 render :index, status: :ok, json: @purchases
 end

purchases_controller#search (improved ?)

def search
 parms = search_params
 @purchases = []
 if parms[:active].nil?
 active = false
 else
 active = parms[:active] == 'true'
 end
 uid = parms[:customer_id]
 pvid = parms[:provider_id]
 pat = parms[:patient_id]
 num_records = 10 if parms[:limit].nil?
 num_records = parms[:limit].to_i unless parms[:limit].nil?
 offset = 0 if parms[:offset].nil?
 offset = parms[:offset].to_i unless parms[:offset].nil?
 base_scope = Purchase.order(created_at: :desc).offset(offset).limit(num_records)
 if parms[:active].nil?
 # all three
 @purchases = base_scope.where(customer_id: uid, provider_id: pvid, patient_id: pid) if !uid.nil? && !pvid.nil? && !pat.nil?
 # two of three
 @purchases = base_scope.where(provider_id: pvid, patient_id: pid) if !pvid.nil? && !pat.nil? && uid.nil?
 @purchases = base_scope.where(customer_id: uid, patient_id: pid) if !uid.nil? && !pat.nil? && pvid.nil?
 @purchases = base_scope.where(customer_id: uid, provider_id: pvid) if !uid.nil? && !pvid.nil? && pat.nil?
 # one of three
 @purchases = base_scope.where(customer_id: uid) if !uid.nil? && pvid.nil? && pat.nil?
 @purchases = base_scope.where(provider_id: pvid) if uid.nil? && !pvid.nil? && pat.nil?
 @purchases = base_scope.where(patient_id: pid) if uid.nil? && pvid.nil? && !pat.nil?
 # none of three
 @purchases = base_scope if uid.nil? && pvid.nil? && pat.nil?
 else
 active_scope = base_scope.where(active: active)
 # all three
 @purchases = active_scope.where(customer_id: uid, provider_id: pvid, patient_id: pid) if !uid.nil? && !pvid.nil? && !pat.nil?
 # two of three
 @purchases = active_scope.where(provider_id: pvid, patient_id: pid) if !pvid.nil? && !pat.nil? && uid.nil?
 @purchases = active_scope.where(customer_id: uid, patient_id: pid) if !uid.nil? && !pat.nil? && pvid.nil?
 @purchases = active_scope.where(customer_id: uid, provider_id: pvid) if !uid.nil? && !pvid.nil? && pat.nil?
 # one of three
 @purchases = active_scope.where(customer_id: uid) if !uid.nil? && pvid.nil? && pat.nil?
 @purchases = active_scope.where(provider_id: pvid) if uid.nil? && !pvid.nil? && pat.nil?
 @purchases = active_scope.where(patient_id: pid) if uid.nil? && pvid.nil? && !pat.nil?
 # none of three
 @purchases = active_scope if uid.nil? && pvid.nil? && pat.nil?
 end
 @purchases.each do |hc|
 hc.stripe_charge_id = nil
 hc.stripe_transaction_id = nil
 end
 render :index, status: :ok, json: @purchases
 end
200_success
145k22 gold badges190 silver badges478 bronze badges
asked May 12, 2015 at 22:39
\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

Whenever my controllers become too large and the logic too complex, I push that logic down into one of a couple layers:

  1. The model
  2. A Command or Service object

Pushing Behavior Into The Model

The first thing I see that could be pushed into the model is this code:

@purchases.each do |hc|
 hc.stripe_charge_id = nil
 hc.stripe_transaction_id = nil
end

The two lines that set the stripe_charge_id and stripe_transaction_id to nil should be a method on the Purchase model:

class Purchase < ActiveRecord::Base
 def clear_stripe_info
 stripe_charge_id = nil
 stripe_transaction_id = nil
 end
end

The rest of it should be pushed into some sort of service or command object. I have a personal disdain for "service" objects because of the same problems with naming things "SomethingManager". I prefer the command pattern for these sorts of things.

Moving Search Behavior Into A Command Object

All of the search logic can be encapsulated in the command object. The idea behind this is to:

  1. Simplify your controller
  2. Make the search logic reusable outside the context of an MVC web application
  3. Make it easier to unit test your controller

On top of that, we can take advantage of the ActiveRecord query interface to further reduce the lines of code.

Simplifying the Controller

First, let's look at the simplified controller:

def search
 command = SearchPurchasesCommand.new search_params
 @purchases = command.execute
 render :index, status: :ok, json: @purchases
end

Ideally, we want just three lines of code.

  1. Create the SearchPurchasesCommand object and pass it parameters
  2. Tell the command object: "Just do whatever it is you do to give me search results"
  3. Render the view and be done with it

Easy-peasey, rice and cheesey. It makes testing this controller much easier because, you only need to mock the search_params and the return value of the command object, instead of needing the full weight of ActiveRecord and a relational database just for a unit test.

Creating the SearchPurchasesCommand Class

Next, the implementation of the SearchPurchasesCommand class that encapsulates all of the craziness of searching for purchases:

class SearchPurchasesCommand
 def initialize(params = {})
 @active = params[:active] == 'true' unless params[:active].nil?
 @num_records = params[:limit].nil? ? 10 : params[:limit].to_i
 @offset = params[:offset].nil? ? 0 : params[:offset].to_i
 @customer_id = params[:customer_id]
 @provider_id = params[:provider_id]
 @patient_id = params[:patient_id]
 end
 def execute
 query = Purchases.order(created_at: :desc)
 .limit(@num_records)
 .offset(@offset)
 query = query.where(customer_id: @customer_id) unless @customer_id.nil?
 query = query.where(provider_id: @provider_id) unless @provider_id.nil?
 query = query.where(patient_id: @patient_id) unless @patient_id.nil?
 query = query.where(active: @active) unless @active.nil?
 query.all.each { |purchase| purchase.clear_stripe_info }
 end
end

The initialize method extracts the params into usable variables. A simple Ternary operator is all that's needed to convert the offset and limit to integers rather than two lines of code for each case.

The execute method is where all the work is done. The first thing we do is get a query object from ActiveRecord. The order, limit and offset are common to all branches of your code, so we start there. Now that we have a query object, it's just a simple matter of testing the other conditions for nil before adding that condition to the query.

Lastly, before we return the array of results, we loop over each one and call the Purchase#clear_stripe_info method we defined earlier.

answered May 20, 2015 at 15:25
\$\endgroup\$
3
\$\begingroup\$

I think the purchases conditions can be simplified vastly if you just prepare the hash before. Something like this (off the top of my head):

where_hash = {}
{
 :uid => :customer_id,
 :pvid => :provider_id,
 :pat => :patient_id
}.each do |the_var, hash_key|
 the_value = binding.local_variable_get(the_var) # Ruby >= 2.1
 # binding.eval("the_value = '#{the_var}'") # Ruby < 2.1
 where_hash[hash_key] = the_value unless the_value.nil?
end
my_limit = my_offset = my_order = nil
unless active
 my_limit = num_records
 my_offset = offset
 my_order = { created_at: :desc }
end
@purchases = base_scope
 .where(where_hash)
 .order(my_order)
 .limit(my_limit)
 .offset(my_offset)

As you see the whole if/else block is useless and we shortened it into a smaller one as well. If I'm not wrong, nil values in where, limit, offset and order are simply ignored so if any param is nil it will just not apply that filtering to it instead of returning 0 results. So in practice, your scope will be full by default and minified the more values it is given.

answered May 13, 2015 at 7:34
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.