Ruby Science
Case Statement
Case Statements are a sign that a method contains too much knowledge.
Symptoms
- Case statements that check the class of an object.
- Case statements that check a type code.
- Divergent
change caused by changing or adding
when
clauses. - Shotgun surgery caused by duplicating the case statement.
Actual case
statements are extremely easy to find. Just
grep your codebase for “case.” However, you should also be on the
lookout for case
’s sinister cousin, the repetitive
if-elsif
.
Type Codes
Some applications contain type codes—fields that store type information about objects. These fields are easy to add and seem innocent, but result in code that’s harder to maintain. A better solution is to take advantage of Ruby’s ability to invoke different behavior based on an object’s class, called “dynamic dispatch.” Using a case statement with a type code inelegantly reproduces dynamic dispatch.
The special type
column that ActiveRecord uses is not
necessarily a type code. The type
column is used to
serialize an object’s class to the database so that the correct class
can be instantiated later on. If you’re just using the type
column to let ActiveRecord decide which class to instantiate, this isn’t
a smell. However, make sure to avoid referencing the type
column from case
or if
statements.
Example
This method summarizes the answers to a question. The summary varies based on the type of question.
# app/models/question.rb
def summary
case question_type
when 'MultipleChoice'
summarize_multiple_choice_answerswhen 'Open'
summarize_open_answerswhen 'Scale'
summarize_scale_answersend
end
Note that many applications replicate the same case
statement, which is a more serious offense. This view duplicates the
case
logic from Question#summary
, this time in
the form of multiple if
statements:
# app/views/questions/_question.html.erb<% if question.question_type == 'MultipleChoice' -%>
<ol>
<% question.options.each do |option| -%>
<li>
<%= submission_fields.radio_button :text, option.text, id: dom_id(option) %>
<%= content_tag :label, option.text, for: dom_id(option) %>
</li>
<% end -%>
</ol>
<% end -%>
<% if question.question_type == 'Scale' -%>
<ol>
<% question.steps.each do |step| -%>
<li>
<%= submission_fields.radio_button :text, step %>
<%= submission_fields.label "text_#{step}", label: step %>
</li>
<% end -%>
</ol>
<% end -%>
Solutions
- Replace
type code with subclasses if the
case
statement is checking a type code, such asquestion_type
. - Replace
conditional with polymorphism when the
case
statement is checking the class of an object. - Use convention over configuration when selecting a strategy based on a string name.
Ruby Science
The canonical reference for writing fantastic Rails applications from authors who have created hundreds.