49de4cd2f26705785cbef2b15a9df7aa

In my Rails app, the Property class method #filtered_properties accepts an arg containing params from an HTTP request, processes the params, and returns an array of properties matching the params.

I've created separate class methods (#filter_X , where "X" is "price", "bedrooms", etc) for processing each parameter, which #filtered_properties calls. However, #filtered_properties is still 120 lines.

Might you have any suggestions for how to make #filtered_properties and/or #filter_X more DRY and/or shorter?

def self.filter_price(direction, price)
    return nil if direction.blank? or price.blank?
    return {:error => 'Invalid argument: direction'} unless ['at most', 'at least'].include? direction
    return {:error => 'Invalid argument: price'}     unless price.to_s =~ /\A[0-9]+\Z/

    case direction
      when 'at most'  then  return  {:sql => 'price <= :price', :price  => price.to_s}
      when 'at least' then  return  {:sql => 'price >= :price', :price  => price.to_s}
      else                  raise   'Unknown error'
    end
  end

  def self.filter_bedrooms(bedrooms)
    return nil if bedrooms.blank?
    return {:error => 'Invalid argument: bedrooms'} unless bedrooms.to_s =~ /\A[0-9]+\Z/

    {:sql => 'bedrooms = :bedrooms', :bedrooms => bedrooms.to_s}
  end

  #
  # ...Snip. There are several more methods like the ones above...
  #

  def self.filtered_properties(params)
    conditions_string = ''
    conditions_hash   = {}      # Stores the conditions for filtering out properties.
    condition_errors  = []      # Stores errors related to a filter request.

    # Process the "price" and "price_direction" params.
    filtered = filter_price params[:price_direction], params[:price]
    if filtered.blank?      # Do nothing.
    elsif filtered[:error]
      condition_errors << 'Please provide a number for the price. Eg: 1500'
    else
      conditions_string << filtered[:sql]
      conditions_hash[:price] = filtered[:price]
    end

    # Process the "bedrooms" param.
    filtered = filter_bedrooms params[:bedrooms]
    if filtered.blank?      # Do nothing.
    elsif filtered[:error]
      condition_errors << 'Please provide a number for the bedrooms. Eg: 2'
    else
      conditions_string << ' AND ' << filtered[:sql]
      conditions_hash[:bedrooms] = filtered[:bedrooms]
    end

    # Process the "bathrooms" param.
    filtered = filter_bathrooms params[:bathrooms]
    if filtered.blank?      # Do nothing.
      puts '> bathrooms is blank'
    elsif filtered[:error]
      condition_errors << 'Please provide a number for the bathrooms. Eg: 2'
    else
      conditions_string << ' AND ' << filtered[:sql]
      conditions_hash[:bathrooms] = filtered[:bathrooms]
    end

    # ...snip...

    return condition_errors unless condition_errors.empty?

    self.find :all, :conditions => [conditions_string, conditions_hash]

  end # End #filtered_properties

Refactorings

No refactoring yet !

A8d3f35baafdaea851914b17dae9e1fc

Adam

October 27, 2008, October 27, 2008 04:06, permalink

1 rating. Login to rate!

I am going to leave the implementation of ItemFilter as an exercise for the reader as it's more involved than I want to take on at this point, and not necessary for the illustration. But I will point you towards ActiveRecord::Validations and ActiveRecord::NamedScope as two modules that you will probably want to utilize in your class.

class PriceFilter < ItemFilter
  DIRECTIONS = { 'at most' => '<=', 'at least' => '>=' }
  
  validates_presence_of :price, :direction
  validates_format_of :price, :with => /\A[0-9]+\Z/
  validates_inclusion_of :direction, :in => DIRECTIONS.keys
  
  def filter
    { :conditions => [ "price #{DIRECTIONS[direction]} ?", price ] }
  end
end

class Item < ActiveRecord::Base
  def self.filter_properties(params)
    params.inject(self) do |klass,(name,options)|
      ItemFilter.filter_for(name).new(klass, options).to_scope
    end
  end
end
49de4cd2f26705785cbef2b15a9df7aa

Nick

October 30, 2008, October 30, 2008 17:25, permalink

No rating. Login to rate!

Hey Adam, thanks for the refactoring suggestion. It's a bit over my head at the moment, as I've never played with AR named scopes, and I'm not entirely sure what you meant the ItemFilter class to do.

What I ended up doing was moving #filtered_properties and #filter_* from the Property model into a new model, named PropertyFilter.

PropertyFilter inherits from ActiveRecord::BaseWithoutTable so that I can use AR's convenient validations, and not save data to a database table.

class PropertyFilter < ActiveRecord::BaseWithoutTable
  attr_accessible :price_direction, :price # ...many more attributes...

  column :price_direction,  :string
  column :price,            :integer
  column :bedrooms,         :integer
  # ...many more column definitions...

  valid_price_directions  = ['at most', 'at least']
  # ...many more valid values defined...

  validates_inclusion_of :price_direction,
    :in           => valid_price_directions,
    :allow_blank  => true
  # ...many more validations...

  def bedrooms_constraint
    return nil if self.bedrooms.blank?
    {:sql => 'bedrooms = :bedrooms', :bedrooms => self.bedrooms.to_s}
  end

  # ...many more _constraint methods...

  def properties
    # ...Call each _constraint method, and build the conditions for #find...

    return {:properties => Property.find(:all, :bounds => bounds, :conditions => [conditions_sql, condition_values])}
  end
end

Your refactoring





Format Copy from initial code

or Cancel