Using inheritance in Ruby is extremely powerful and can greatly reduce complexity in your code. In this blog post, I show you how to dry up your code with Plain Ole’ Ruby Objects (PORO’s) that leverage inheritance.

Example Use Case: Filtering Apartment Listings

Let’s say we have an apartment listings website. Users can search for apartments in their area and refine the search with filters like # of bedrooms, max price, and lease start date.

User Chooses Number of Bedrooms (images courtesy of apartmentlist.com)

User Sets Max Rent

User Selects Lease Start Date

When a user submits the form, the controller params will come in like:

{
  search: { bedrooms: "3", max_price: "1000.00", lease_start_date: "11/01/2015" }
}

As simple as this form is, we still have some work to do to display the right results.

The Quick Solution

The quickest and most obvious solution is to plop our code right into the controller:

# controllers/search_controller.rb
class SearchController < ApplicationController

  def apartments
    bedrooms = params[:search][:bedrooms].to_i
    max_price = params[:search][:max_price].to_f
    lease_start_date = Date.parse(params[:search][:lease_start_date])

    @apartments = Apartment.where(bedrooms: bedrooms).where("price <= ?", max_price).where(lease_start_date: lease_start_date)
  end

end

While simple now, this code will get unweildy quickly. Imagine we want to implement the following filters next:

User Selects Ammenities

I’ll save you the pain of showing the code required to implement these new filters, but you can imagine how long this controller action would become. It’s time to move the form data parsing and apartment filtering into a service object.

Service Object

Let’s create an object to handle the filtering of our results.

# controllers/search_controller.rb
class SearchController < ApplicationController

  # mmm, a controller action I can read
  def apartments
    @apartments = Forms::FilterApartments.new(params[:search]).apartments
  end

end
# classes/forms/filter_apartments.rb

class Forms::FilterApartments

  attr_reader :bedrooms, :max_price, :lease_start_date

  def initialize(args = {})
    @bedroooms = args[:bedrooms].to_i
    @max_price = args[:max_price].to_f
    @lease_start_date = string_to_date(args[:lease_start_date])
  end

  def apartments
    @apartments = Apartment.where(bedrooms: bedrooms)
                           .where("price <= ?", max_price)
                           .where(lease_start_date: lease_start_date)
  end

  private

    def string_to_date(date_string)
      Date.parse(date_string)
    end

end

This service object is certainly an enhancement. It provides a place where we can perform parsing and filtering logic and allows the controller to do what it does best: set data for our views to access.

Another side benefit is that testing this class would be very straight forward.

People Rent Houses Too!

Our app has taken off and users want another page to see house listings. Let’s start to write the controller action and corresponding service object to filter results for houses. Unlike apartments, houses come with some additional filtering options: acreage, and has_front_porch

# controllers/search_controller.rb
class SearchController < ApplicationController

  def apartments
    @apartments = Forms::FilterApartments.new(params[:search]).apartments
  end

  def houses
    @houses = Forms::FilterHouses.new(params[:search]).houses
  end

end
# classes/forms/filter_houses.rb

class Forms::FilterHouses

  attr_reader :bedrooms, :max_price, :lease_start_date, :acreage, :has_front_porch

  def initialize(args = {})
    @bedroooms = args[:bedrooms].to_i
    @max_price = args[:max_price].to_f
    @lease_start_date = string_to_date(args[:lease_start_date])
    @acreage = args[:acreage].to_f
    @has_front_porch = string_to_boolean(args[:has_front_porch])
  end

  def houses
    @houses = House.where(bedrooms: bedrooms)
                   .where("price <= ?", max_price)
                   .where(lease_start_date: lease_start_date)
                   .where("acreage >= ?", acreage)
                   .where(has_front_porch: has_front_porch)
  end

  private

    def string_to_date(date_string)
      Date.parse(date_string)
    end

    def string_to_boolean(boolean_string)
      boolean_string == "true"
    end

end

Man, the FilterHouses service object seems very similar to the FilterApartments service object. Both are filtering by number of bedrooms, max_price, and lease_start_date. Both are converting the incoming lease_start_date string to a date as well.

Intuitively, we know that searching for houses and searching for apartments are fundamentally similar experiences. So it is no surprise to me we are seeing these commonalities in the code.

How can we dry this up? Let’s give inheritance a shot.

Implementing Inheritance

FilterApartments and FilterHouses Inherit From FilterProperties

Inheritance is really useful when two things are very similar. For example, a house and an apartment are two very similar entities. Inheritance allows us to put the common code between these two entities in one shared class, and the code specific to each the house and the apartment in those respective classes.

# classes/forms/filter_properties.rb

class Forms::FilterProperties

  attr_reader :bedrooms, :max_price, :lease_start_date

  def initialize(args = {})
    @bedroooms = args[:bedrooms].to_i
    @max_price = string_to_float(args[:max_price])
    @lease_start_date = string_to_date(args[:lease_start_date])
    after_initialize(args) # implemented by houses and apartments filters
  end

  def results
    @results = model.where(bedrooms: bedrooms)
                    .where("price <= ?", max_price)
                    .where(lease_start_date: lease_start_date)
  end

  private

    def after_initialize(args = {})
      # implemented by subclasses
    end

    def model
      # required method for subclasses
      # need to know what table to find the data
      fail "#{self.class.name} must implement model method."
    end

    def string_to_date(date_string)
      Date.parse(date_string)
    end

end
# classes/forms/filter_properties/apartments.rb

class Forms::FilterProperties::Apartments < Forms::FilterProperties

  private

    def model
      Apartment
    end

end
# classes/forms/filter_properties/houses.rb

class Forms::FilterProperties::Houses < Forms::FilterProperties

  attr_reader :acreage, :has_front_porch

  def results
    super # runs results method in Forms::FilterProperties
    @results = @results.where("acreage >= ?", acreage)
                       .where(has_front_porch: has_front_porch)
  end

  private

    def after_initialize(args = {})
      @acreage = args[:acreage].to_f
      @has_front_porch = string_to_boolean(args[:acreage])
    end

    def model
      House
    end

    def string_to_boolean(boolean_string)
      boolean_string == "true"
    end

end

With these three classes in place, we have abstracted the commonalities between FilterApartments and FilterHouses into one class, FilterProperties. Further, since the Houses filter has every filter that Apartments has plus acreage and has_front_porch, we see that the Houses filter only needs to worry about acreage and has_front_porch.

In my opinion, implementing inheritance in this situation is absolutely critical. What happens, for example, if the bedrooms filter changes? Maybe users want to see apartments and houses with more than 3 bedrooms instead of filtering exactly on the bedroom number. This type of change would be cumbersome if the logic for filtering bedrooms was in both the Apartments filtering class and the Houses filtering class (let alone separate controller actions if we didn’t implement service objects).

You may be saying, “Why not implement Single Table Inheritance (STI)?” Actually, I would in fact use STI here so that we could store all properties in one table called “properties.” However, I often use POROs with inheritance to handle the form parsing and at least abstract those common parsing needs into a shared class.

Here is what STI would look like in our Rails modeling:

# models/property.rb
class Property < ActiveRecord::Base
end

# models/apartment.rb
class Apartment < Property
end

# models/house.rb
class House < Property
end

With this modeling, we would not need the “model” method in our FilterProperties service object. To me, a house and an apartment are too similar of entities to not inherit from a common model, which intuitively is a property. Further, I can envision a situation in which both apartments and houses are shown to users in the same list.

Ryan Francis

Partner & Developer

As ambitious as he is tall, Ryan has a passion for creation. In 2012, he created Francis Lofts & Bunks, a company in Western Ohio that manufactures aluminum loft beds and bunk beds. Equipped with a burning desire to build things that are useful to others, Ryan has come into his own in web development, combining creativity, logic, and an empathy for others to build outstanding, easy-to-use products.

Ready to Build Something Great?

Partner with us to develop technology to grow your business.