Ruby Science

Replace Conditional with Polymorphism

Conditional code clutters methods, makes extraction and reuse harder and can lead to leaky concerns. Object-oriented languages like Ruby allow developers to avoid conditionals using polymorphism. Rather than using if/else or case/when to create a conditional path for each possible situation, you can implement a method differently in different classes, adding (or reusing) a class for each situation.

Replacing conditional code allows you to move decisions to the best point in the application. Depending on polymorphic interfaces will create classes that don’t need to change when the application changes.

Uses

  • Removes divergent change from classes that need to alter their behavior based on the outcome of the condition.
  • Prevents shotgun surgery from adding new types.
  • Removes feature envy by allowing dependent classes to make their own decisions.
  • Makes it easier to remove duplicated code by taking behavior out of conditional clauses and private methods.
  • Makes conditional logic easier to reuse, which makes it easier to avoid duplication.
  • Replaces conditional logic with simple commands, following tell, don’t ask.

Example

This Question class summarizes its answers differently depending on its question_type:

# app/models/question.rb
class Question < ActiveRecord::Base
  include ActiveModel::ForbiddenAttributesProtection

  SUBMITTABLE_TYPES = %w(Open MultipleChoice Scale).freeze

  validates , true, if: 
  validates , true, if: 
  validates , true, SUBMITTABLE_TYPES
  validates , true

  belongs_to 
  has_many 
  has_many 

  accepts_nested_attributes_for , 

  def summary
    case question_type
    when 'MultipleChoice'
      summarize_multiple_choice_answers
    when 'Open'
      summarize_open_answers
    when 'Scale'
      summarize_scale_answers
    end
  end

  def steps
    (minimum..maximum).to_a
  end

  private

  def scale?
    question_type == 'Scale'
  end

  def summarize_multiple_choice_answers
    total = answers.count
    counts = answers.group().order('COUNT(*) DESC').count
    percents = counts.map do |text, count|
      percent = (100.0 * count / total).round
      "#{percent}% #{text}"
    end
    percents.join(', ')
  end

  def summarize_open_answers
    answers.order().pluck().join(', ')
  end

  def summarize_scale_answers
    sprintf('Average: %.02f', answers.average('text'))
  end
end

There are a number of issues with the summary method:

  • Adding a new question type will require modifying the method, leading to divergent change.
  • The logic and data for summarizing every type of question and answer is jammed into the Question class, resulting in a large class with obscure code.
  • This method isn’t the only place in the application where question types are checked, meaning that new types will cause shotgun surgery.

There are several ways to refactor to use polymorphism. In this chapter, we’ll demonstrate a solution that uses subclasses to replace type codes, which is one of the simplest solutions to implement. However, make sure to see the Drawbacks section in this chapter for alternative implementations.

Replace Type Code with Subclasses

Let’s replace this case statement with polymorphism by introducing a subclass for each type of question.

Our Question class is a subclass of ActiveRecord::Base. If we want to create subclasses of Question, we have to tell ActiveRecord which subclass to instantiate when it fetches records from the questions table. The mechanism Rails uses for storing instances of different classes in the same table is called single table inheritance. Rails will take care of most of the details, but there are a few extra steps we need to take when refactoring to single table inheritance.

Single Table Inheritance (STI)

The first step to convert to STI is generally to create a new subclass for each type. However, the existing type codes are named “Open,” “Scale” and “MultipleChoice,” which won’t make good class names. Names like “OpenQuestion” would be better, so let’s start by changing the existing type codes:

# app/models/question.rb
def summary
  case question_type
  when 'MultipleChoiceQuestion'
    summarize_multiple_choice_answers
  when 'OpenQuestion'
    summarize_open_answers
  when 'ScaleQuestion'
    summarize_scale_answers
  end
end
# db/migrate/20121128221331_add_question_suffix_to_question_type.rb
class AddQuestionSuffixToQuestionType < ActiveRecord::Migration
  def up
    connection.update(<<-SQL)
      UPDATE questions SET question_type = question_type || 'Question'
    SQL
  end

  def down
    connection.update(<<-SQL)
      UPDATE questions SET question_type = REPLACE(question_type, 'Question', '')
    SQL
  end
end

See commit b535171 for the full change.

The Question class stores its type code as question_type. The Rails convention is to use a column named type, but Rails will automatically start using STI if that column is present. That means that renaming question_type to type at this point would result in debugging two things at once: possible breaks from renaming and possible breaks from using STI. Therefore, let’s start by just marking question_type as the inheritance column, allowing us to debug STI failures by themselves:

# app/models/question.rb
set_inheritance_column  'question_type'

Running the tests after this will reveal that Rails wants the subclasses to be defined, so let’s add some placeholder classes:

# app/models/open_question.rb
class OpenQuestion < Question
end
# app/models/scale_question.rb
class ScaleQuestion < Question
end
# app/models/multiple_choice_question.rb
class MultipleChoiceQuestion < Question
end

Rails generates URLs and local variable names for partials based on class names. Our views will now be getting instances of subclasses like OpenQuestion rather than Question, so we’ll need to update a few more references. For example, we’ll have to change lines like:

<%= form_for @question do |form| %>

To:

<%= form_for @question, as: :question do |form| %>

Otherwise, it will generate /open_questions as a URL instead of /questions. See commit c18ebeb for the full change.

At this point, the tests are passing with STI in place, so we can rename question_type to type, following the Rails convention:

# db/migrate/20121128225425_rename_question_type_to_type.rb
class RenameQuestionTypeToType < ActiveRecord::Migration
  def up
    rename_column , , 
  end

  def down
    rename_column , , 
  end
end

Now we need to build the appropriate subclass instead of Question. We can use a little Ruby meta-programming to make that fairly painless:

# app/controllers/questions_controller.rb
def build_question
  @question = type.constantize.new(question_params)
  @question.survey = @survey
end

def type
  params[][]
end

At this point, we’re ready to proceed with a regular refactoring.

Extracting Type-Specific Code

The next step is to move type-specific code from Question into the subclass for each specific type.

Let’s look at the summary method again:

# app/models/question.rb
def summary
  case question_type
  when 'MultipleChoice'
    summarize_multiple_choice_answers
  when 'Open'
    summarize_open_answers
  when 'Scale'
    summarize_scale_answers
  end
end

For each path of the condition, there is a sequence of steps.

The first step is to use extract method to move each path to its own method. In this case, we already extracted methods called summarize_multiple_choice_answers, summarize_open_answers, and summarize_scale_answers, so we can proceed immediately.

The next step is to use move method to move the extracted method to the appropriate class. First, let’s move the method summarize_multiple_choice_answers to MultipleChoiceQuestion and rename it to summary:

class MultipleChoiceQuestion < Question
  def summary
    total = answers.count
    counts = answers.group().order(Arel.sql('COUNT(*) DESC')).count
    percents = counts.map do |text, count|
      percent = (100.0 * count / total).round
      "#{percent}% #{text}"
    end
    percents.join(', ')
  end
end

MultipleChoiceQuestion#summary now overrides Question#summary, so the correct implementation will now be chosen for multiple choice questions.

Now that the code for multiple choice types is in place, we repeat the steps for each other path. Once every path is moved, we can remove Question#summary entirely.

In this case, we’ve already created all our subclasses, but you can use extract class to create them if you’re extracting each conditional path into a new class.

You can see the full change for this step in commit a08f801.

The summary method is now much better. Adding new question types is easier. The new subclass will implement summary and the Question class doesn’t need to change. The summary code for each type now lives with its type, so no one class is cluttered up with the details.

Polymorphic Partials

Applications rarely check the type code in just one place. Running grep on our example application reveals several more places. Most interestingly, the views check the type before deciding how to render a question:

# app/views/questions/_question.html.erb
<% if question.type == 'MultipleChoiceQuestion' -%>
  <ol>
    <% question.options.each do |option| -%>
      <li>
        <%= submission_fields.radio_button , option.text, id: dom_id(option) %>
        <%= content_tag , option.text, for: dom_id(option) %>
      </li>
    <% end -%>
  </ol>
<% end -%>

<% if question.type == 'ScaleQuestion' -%>
  <ol>
    <% question.steps.each do |step| -%>
      <li>
        <%= submission_fields.radio_button , step %>
        <%= submission_fields.label "text_#{step}", label: step %>
      </li>
    <% end -%>
  </ol>
<% end -%>

<% if question.type == 'OpenQuestion' -%>
  <%= submission_fields.text_field  %>
<% end -%>

In the previous example, we moved type-specific code into Question subclasses. However, moving view code would violate MVC (introducing divergent change into the subclasses) and, more importantly, it would be ugly and hard to understand.

Rails has the ability to render views polymorphically. A line like this—

<%= render @question %>

—will ask @question which view should be rendered by calling to_partial_path. As subclasses of ActiveRecord::Base, our Question subclasses will return a path based on their class name. This means that the above line will attempt to render open_questions/_open_question.html.erb for an open question, and so on.

We can use this to move the type-specific view code into a view for each type:

# app/views/open_questions/_open_question.html.erb
<%= submission_fields.text_field  %>

You can see the full change in commit 8243493.

Multiple Polymorphic Views

Our application also has different fields on the question form depending on the question type. Currently, that also performs type-checking:

# app/views/questions/new.html.erb
<% if @question.type == 'MultipleChoiceQuestion' -%>
  <%= form.fields_for(, @question.options_for_form) do |option_fields| -%>
    <%= option_fields.input , label: 'Option' %>
  <% end -%>
<% end -%>

<% if @question.type == 'ScaleQuestion' -%>
  <%= form.input  %>
  <%= form.input  %>
<% end -%>

We already used views like open_questions/_open_question.html.erb for showing a question, so we can’t just put the edit code there. Rails doesn’t support prefixes or suffixes in render, but we can do it ourselves easily enough:

# app/views/questions/new.html.erb
<%= render "#{@question.to_partial_path}_form", question: @question, form: form %>

This will render app/views/open_questions/_open_question_form.html.erb for an open question, and so on.

Drawbacks

It’s worth noting that, although this refactoring improved this particular example, replacing conditionals with polymorphism is not without its drawbacks.

Using polymorphism like this makes it easier to add new types, because adding a new type means that you just need to add a new class and implement the required methods. Adding a new type won’t require changes to any existing classes, and it’s easy to understand what the types are because each type is encapsulated within a class.

However, this change makes it harder to add new behaviors. Adding a new behavior will mean finding every type and adding a new method. Understanding the behavior becomes more difficult because the implementations are spread out among the types. Object-oriented languages lean toward polymorphic implementations, but if you find yourself adding behaviors much more often than adding types, you should look into using observers or visitors instead.

Using subclasses forces you to use inheritance instead of composition for reuse and separation of concerns. See composition over inheritance for more on this subject.

Also, using STI has specific disadvantages. See the chapter on STI for details.

Next Steps

  • Check the new classes for duplicated code that can be pulled up into the superclass.
  • Pay attention to changes that affect the new types, watching out for shotgun surgery that can result from splitting up classes.

Ruby Science

The canonical reference for writing fantastic Rails applications from authors who have created hundreds.

Work with us to make a new Rails app, or to maintain, improve, or scale your existing app.