Ruby Science Sample
Ruby Science Sample
Science
The reference for writing fantastic
Rails applications.
Contents
Introduction iii
Contact us v
I Code Smells 1
Long Method 2
Case Statement 4
Type Codes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Shotgun Surgery 7
II Solutions 9
Replace conditional with Null Object 10
truthiness, try, and other tricks . . . . . . . . . . . . . . . . . . . . . . 14
Extract method 15
Replace temp with query . . . . . . . . . . . . . . . . . . . . . . . . . 17
Extract Partial 19
Closing 22
ii
Introduction
Ruby on Rails is almost a decade old, and its community has developed a
number of principles for building applications that are fast, fun, and easy to
change: dont repeat yourself, keep your views dumb, keep your controllers
skinny, and keep business logic in your models. These principles carry most
applications to their rst release or beyond.
However, these principles only get you so far. After a few releases, most appli-
cations begin to suer. Models become fat, classes become few and large, tests
become slow, and changes become painful. In many applications, there comes
a day when the developers realize that theres no going back; the application is
a twisted mess, and the only way out is a rewrite or a new job.
Fortunately, it doesnt have to be this way. Developers have been using object-
oriented programming for several decades, and theres a wealth of knowledge
out there which still applies to developing applications today. We can use the
lessons learned by these developers to write good Rails applications by applying
good object-oriented programming.
Ruby Science will outline a process for detecting emerging problems in code,
and will dive into the solutions, old and new.
The full book contains three catalogs: smells, solutions, and principles. This
sample contains a few hand-picked chapters from the rst two catalogs, pub-
lished directly from the book, allowing you to get a sense for the content, style,
and delivery of the product.
If you enjoy the sample, you can get access to the entire book and sample
application at:
http://www.rubyscience.com
As a purchaser of the book, you also get access to:
Multiple formats, including HTML, PDF, EPUB, and Kindle.
A complete example application containing code samples referenced from
the book.
iii
INTRODUCTION iv
The GitHub repository to receive updates as soon as theyre pushed.
GitHub issues, where you can provide feedback tell us what youd like to
see.
Ask us your toughest Rails questions!
Contact us
If you have any questions, or just want to get in touch, drop us a line at
[email protected].
v
Part I
Code Smells
1
Long Method
The most common smell in Rails applications is the Long Method.
Long methods are exactly what they sound like: methods which are too long.
Theyre easy to spot.
Symptoms
If you cant tell exactly what a method does at a glance, its too long.
Methods with more than one level of nesting are usually too long.
Methods with more than one level of abstraction may be too long.
Methods with a og score of 10 or higher may be too long.
You can watch out for long methods as you write them, but nding existing
methods is easiest with tools like og:
% flog app lib
72.9: flog total
5.6: flog/method average
15.7: QuestionsController#create app/controllers/questions_controller.rb:9
11.7: QuestionsController#new app/controllers/questions_controller.rb:2
11.0: Question#none
8.1: SurveysController#create app/controllers/surveys_controller.rb:6
Methods with higher scores are more complicated. Anything with a score higher
than 10 is worth looking at, but og will only help you nd potential trouble
spots; use your own judgement when refactoring.
2
CHAPTER 1. LONG METHOD 3
Example
For an example of a Long Method, lets take a look at the highest scored method
from og, QuestionsController#create:
def create
@survey = Survey.find(params[:survey_id])
@submittable_type = params[:submittable_type_id]
question_params = params.
require(:question).
permit(:submittable_type, :title, :options_attributes, :minimum, :maximum)
@question = @survey.questions.new(question_params)
@question.submittable_type = @submittable_type
if @question.save
redirect_to @survey
else
render :new
end
end
Solutions
Extract Method is the most common way to break apart long methods.
Replace Temp with Query if you have local variables in the method.
After extracting methods, check for Feature Envy in the new methods to see if
you should employ Move Method to provide the method with a better home.
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 nd. Just grep your codebase for
case. However, you should also be on the lookout for cases sinister cousin,
the repetitive if-elsif.
Type Codes
Some applications contain type codes: elds that store type information about
objects. These elds are easy to add and seem innocent, but they result in code
thats harder to maintain. A better solution is to take advantage of Rubys
ability to invoke dierent behavior based on an objects 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 objects class to the database, so that
the correct class can be instantiated later on. If youre just using the type
column to let ActiveRecord decide which class to instantiate, this isnt a smell.
However, make sure to avoid referencing the type column from case or if
statements.
4
CHAPTER 2. CASE STATEMENT 5
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_answers
when Open
summarize_open_answers
when Scale
summarize_scale_answers
end
end
Note that many applications replicate the same case statement, which is a more
serious oence. 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 -%>
CHAPTER 2. CASE STATEMENT 6
Solutions
Replace Type Code with Subclasses if the case statement is checking a
type code, such as question type.
Replace Conditional with Polymorphism when the case statement is
checking the class of an object.
Shotgun Surgery
Shotgun Surgery is usually a more obvious symptom that reveals another smell.
Symptoms
You have to make the same small change across several dierent les.
Changes become dicult to manage because they hard to keep track of.
Make sure you look for related smells in the aected code:
Duplicated Code
Case Statement
Feature Envy
Long Parameter List
Parallel Inheritance Hierarchies
7
CHAPTER 3. SHOTGUN SURGERY 8
Example
Users names are formatted and displayed as First Last throughout the applica-
tion. If we want to change the formating to include a middle initial (e.g. First
M. Last) wed need to make the same small change in several places.
# app/views/users/show.html.erb
<%= current_user.first_name %> <%= current_user.last_name %>
# app/views/users/index.html.erb
<%= current_user.first_name %> <%= current_user.last_name %>
# app/views/layouts/application.html.erb
<%= current_user.first_name %> <%= current_user.last_name %>
# app/views/mailers/completion_notification.html.erb
<%= current_user.first_name %> <%= current_user.last_name %>
Solutions
Replace Conditional with Polymorphism to replace duplicated case state-
ments and if-elsif blocks.
Replace Conditional with Null Object if changing a method to return nil
would require checks for nil in several places.
Extract Decorator to replace duplicated display code in views/templates.
Introduce Parameter Object to hang useful formatting methods alongside
a data clump of related attributes.
Part II
Solutions
9
Replace conditional with
Null Object
Every Ruby developer is familiar with nil, and Ruby on Rails comes with a full
compliment of tools to handle it: nil?, present?, try, and more. However, its
easy to let these tools hide duplication and leak concerns. If you nd yourself
checking for nil all over your codebase, try replacing some of the nil values
with null objects.
Uses
Removes Shotgun Surgery when an existing method begins returning nil.
Removes Duplicated Code related to checking for nil.
Removes clutter, improving readability of code that consumes nil.
Example
# app/models/question.rb
def most_recent_answer_text
answers.most_recent.try(:text) || Answer::MISSING_TEXT
end
The most recent answer text method asks its answers association for
most recent answer. It only wants the text from that answer, but it must
rst check to make sure that an answer actually exists to get text from. It
needs to perform this check because most recent might return nil:
# app/models/answer.rb
def self.most_recent
order(:created_at).last
end
10
CHAPTER 4. REPLACE CONDITIONAL WITH NULL OBJECT 11
This call clutters up the method, and returning nil is contagious: any method
that calls most recent must also check for nil. The concept of a missing answer
is likely to come up more than once, as in this example:
# app/models/user.rb
def answer_text_for(question)
question.answers.for_user(self).try(:text) || Answer::MISSING_TEXT
end
Again, most recent answer text might return nil:
# app/models/answer.rb
def self.for_user(user)
joins(:completion).where(completions: { user_id: user.id }).last
end
The User#answer text for method duplicates the check for a missing answer,
and worse, its repeating the logic of what happens when you need text without
an answer.
We can remove these checks entirely from Question and User by introducing a
Null Object:
# app/models/question.rb
def most_recent_answer_text
answers.most_recent.text
end
# app/models/user.rb
def answer_text_for(question)
question.answers.for_user(self).text
end
CHAPTER 4. REPLACE CONDITIONAL WITH NULL OBJECT 12
Were now just assuming that Answer class methods will return something
answer-like; specically, we expect an object that returns useful text. We can
refactor Answer to handle the nil check:
# app/models/answer.rb
class Answer < ActiveRecord::Base
include ActiveModel::ForbiddenAttributesProtection
belongs_to :completion
belongs_to :question
validates :text, presence: true
def self.for_user(user)
joins(:completion).where(completions: { user_id: user.id }).last ||
NullAnswer.new
end
def self.most_recent
order(:created_at).last || NullAnswer.new
end
end
Note that for user and most recent return a NullAnswer if no answer can
be found, so these methods will never return nil. The implementation for
NullAnswer is simple:
# app/models/null_answer.rb
class NullAnswer
def text
No response
end
end
CHAPTER 4. REPLACE CONDITIONAL WITH NULL OBJECT 13
We can take things just a little further and remove a bit of duplication with a
quick Extract Method:
# app/models/answer.rb
class Answer < ActiveRecord::Base
include ActiveModel::ForbiddenAttributesProtection
belongs_to :completion
belongs_to :question
validates :text, presence: true
def self.for_user(user)
joins(:completion).where(completions: { user_id: user.id }).last_or_null
end
def self.most_recent
order(:created_at).last_or_null
end
private
def self.last_or_null
last || NullAnswer.new
end
end
Now we can easily create Answer class methods that return a usable answer, no
matter what.
Drawbacks
Introducing a null object can remove duplication and clutter, but it can also
cause pain and confusion:
As a developer reading a method like Question#most recent answer text,
you may be confused to nd that most recent answer returned an in-
stance of NullAnswer and not Answer.
Whenever a method needs to worry about whether or not an actual answer
exists, youll need to add explicit present? checks and dene present?
to return false on your null object. This is common in views, when the
view needs to add special markup to denote missing values.
CHAPTER 4. REPLACE CONDITIONAL WITH NULL OBJECT 14
NullAnswer may eventually need to reimplement large part of the Answer
API, leading to potential Duplicated Code and Shotgun Surgery, which is
largely what we hoped to solve in the rst place.
Dont introduce a null object until you nd yourself swatting enough nil values
to be annoying, and make sure youre actually cutting down on conditional logic
when you introduce it.
Next Steps
Look for other nil checks from the return values of refactored methods.
Make sure your Null Object class implements the required methods from
the original class.
Make sure no Duplicated Code exists between the Null Object class and
the original.
truthiness, try, and other tricks
All checks for nil are a condition, but Ruby provides many ways to check for
nil without using an explicit if. Watch out for nil conditional checks disguised
behind other syntax. The following are all roughly equivalent:
# Explicit if with nil?
if user.nil?
nil
else
user.name
end
# Implicit nil check through truthy conditional
if user
user.name
end
# Relies on nil being falsey
user && user.name
# Call to try
user.try(:name)
Extract method
The simplest refactoring to perform is Extract Method. To extract a method:
Pick a name for the new method.
Move extracted code into the new method.
Call the new method from the point of extraction.
Uses
Removes Long Methods.
Sets the stage for moving behavior via Move Method.
Resolves obscurity by introducing intention-revealing names.
Allows removal of Duplicated Code by moving the common code into the
extracted method.
Reveals complexity.
15
CHAPTER 5. EXTRACT METHOD 16
Lets take a look at an example Long Method and improve it by extracting
smaller methods:
def create
@survey = Survey.find(params[:survey_id])
@submittable_type = params[:submittable_type_id]
question_params = params.
require(:question).
permit(:submittable_type, :title, :options_attributes, :minimum, :maximum)
@question = @survey.questions.new(question_params)
@question.submittable_type = @submittable_type
if @question.save
redirect_to @survey
else
render :new
end
end
This method performs a number of tasks:
It nds the survey that the question should belong to.
It gures out what type of question were creating (the submittable type).
It builds parameters for the new question by applying a white list to the
HTTP parameters.
It builds a question from the given survey, parameters, and submittable
type.
It attempts to save the question.
It redirects back to the survey for a valid question.
It re-renders the form for an invalid question.
CHAPTER 5. EXTRACT METHOD 17
Any of these tasks can be extracted to a method. Lets start by extracting the
task of building the question.
def create
@survey = Survey.find(params[:survey_id])
@submittable_type = params[:submittable_type_id]
build_question
if @question.save
redirect_to @survey
else
render :new
end
end
private
def build_question
question_params = params.
require(:question).
permit(:submittable_type, :title, :options_attributes, :minimum, :maximum)
@question = @survey.questions.new(question_params)
@question.submittable_type = @submittable_type
end
The create method is already much more readable. The new build question
method is noisy, though, with the wrong details at the beginning. The task of
pulling out question parameters is clouding up the task of building the question.
Lets extract another method.
Replace temp with query
One simple way to extract methods is by replacing local variables. Lets pull
question params into its own method:
def build_question
@question = @survey.questions.new(question_params)
@question.submittable_type = @submittable_type
end
def question_params
params.
require(:question).
permit(:submittable_type, :title, :options_attributes, :minimum, :maximum)
end
CHAPTER 5. EXTRACT METHOD 18
Next Steps
Check the original method and the extracted method to make sure neither
is a Long Method.
Check the original method and the extracted method to make sure that
they both relate to the same core concern. If the methods arent highly
related, the class will suer from Divergent Change.
Check newly extracted methods for Feature Envy in the new methods
to see if you should employ Move Method to provide the method with a
better home.
Check the aected class to make sure its not a Large Class. Extracting
methods reveals complexity, making it clearer when a class is doing too
much.
Extract Partial
Extracting a partial is a technique used for removing complex or duplicated
view code from your application. This is the equivalent of using Long Method
and Extract Method in your views and templates.
Uses
Remove Duplicated Code from views.
Remove Shotgun Surgery by forcing changes to happen in one place.
Remove Divergent Change by removing a reason for the view to change.
Group common code.
Reduce view size and complexity.
Steps
Create a new le for partial prexed with an underscore ( lename.html.erb).
Move common code into newly created le.
Render the partial from the source le.
19
CHAPTER 6. EXTRACT PARTIAL 20
Example
Lets revisit the view code for adding and editing questions.
Note: There are a few small dierences in the les (the url endpoint, and the
label on the submit button).
# app/views/questions/new.html.erb
<h1>Add Question</h1>
<%= simple_form_for @question, as: :question, url: survey_questions_path(@survey) do |form| -%>
<%= form.hidden_field :type %>
<%= form.input :title %>
<%= render "#{@question.to_partial_path}_form", question: @question, form: form %>
<%= form.submit Create Question %>
<% end -%>
# app/views/questions/edit.html.erb
<h1>Edit Question</h1>
<%= simple_form_for @question, as: :question, url: question_path do |form| -%>
<%= form.hidden_field :type %>
<%= form.input :title %>
<%= render "#{@question.to_partial_path}_form", question: @question, form: form %>
<%= form.submit Update Question %>
<% end -%>
First extract the common code into a partial, remove any instance variables,
and use question and url as a local variables.
# app/views/questions/_form.html.erb
<%= simple_form_for question, as: :question, url: url do |form| -%>
<%= form.hidden_field :type %>
<%= form.input :title %>
<%= render "#{question.to_partial_path}_form", question: question, form: form %>
<%= form.submit %>
<% end -%>
Move the submit button text into the locales le.
# config/locales/en.yml
en:
helpers:
submit:
question:
create: Create Question
update: Update Question
CHAPTER 6. EXTRACT PARTIAL 21
Then render the partial from each of the views, passing in the values for
question and url.
# app/views/questions/new.html.erb
<h1>Add Question</h1>
<%= render form, question: @question, url: survey_questions_path(@survey) %>
# app/views/questions/edit.html.erb
<h1>Edit Question</h1>
<%= render form, question: @question, url: question_path %>
Next Steps
Check for other occurances of the duplicated view code in your application
and replace them with the newly extracted partial.
Closing
Thanks for checking out the sample of Ruby Science. If youd like to get access
to the full content, the example application, ongoing updates, and the ability
to get your questions about Ruby on Rails answered by us, you can pick it up
on our website:
http://www.rubyscience.com
22