You are on page 1of 227

Ruby

Science
The reference for writing fantastic
Rails applications.
Ruby Science
thoughtbot Joe Ferris Harlow Ward
June 7, 2013
Contents
Introduction vi
Code Reviews . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vi
Follow Your Nose . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vii
Removing Resistance . . . . . . . . . . . . . . . . . . . . . . . . . . . viii
Bugs and Churn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ix
Metrics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ix
How To Read This Book . . . . . . . . . . . . . . . . . . . . . . . . . x
I Code Smells 1
Long Method 2
Large Class 4
God Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Feature Envy 8
Case Statement 10
Type Codes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
i
CONTENTS ii
Shotgun Surgery 13
Divergent Change 15
Long Parameter List 18
Duplicated Code 20
Uncommunicative Name 23
Single Table Inheritance (STI) 25
Comments 28
Mixin 30
Callback 33
II Solutions 35
Replace Conditional with Polymorphism 36
Replace Type Code With Subclasses . . . . . . . . . . . . . . . . . . 39
Single Table Inheritance (STI) . . . . . . . . . . . . . . . . . . . . . . . 39
Polymorphic Partials . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
Replace conditional with Null Object 47
truthiness, try, and other tricks . . . . . . . . . . . . . . . . . . . . . . 52
Extract method 53
Replace temp with query . . . . . . . . . . . . . . . . . . . . . . . . . 56
Rename Method 58
CONTENTS iii
Extract Class 62
Extract Value Object 77
Extract Decorator 80
Extract Partial 91
Extract Validator 94
Introduce Explaining Variable 97
Introduce Form Object 100
Introduce Parameter Object 106
Introduce Facade 109
Use class as Factory 110
Move method 115
Inline class 118
Inject dependencies 122
Replace Subclasses with Strategies 130
Replace mixin with composition 152
Replace Callback with Method 158
Use convention over conguration 161
Scoping constantize . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
CONTENTS iv
III Principles 168
DRY 169
Duplicated Knowledge vs Duplicated Text . . . . . . . . . . . . . . . . 169
Single responsibility principle 172
Cohesion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
Responsibility Magnets . . . . . . . . . . . . . . . . . . . . . . . . . . 176
Tension with Tell, Dont Ask . . . . . . . . . . . . . . . . . . . . . . . . 176
Tell, Dont Ask 180
Tension with MVC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
Law of Demeter 185
Multiple Dots . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Multiple Assignments . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
The Spirit of the Law . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
Objects vs Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
Duplication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
Composition over inheritance 190
Dynamic vs Static . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
Dynamic Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
The trouble With Hierarchies . . . . . . . . . . . . . . . . . . . . . . . 195
Mixins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
Single Table Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . 196
CONTENTS v
Open/closed principle 199
Strategies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
Everything is Open . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206
Monkey Patching . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
Dependency inversion principle 209
Inversion of Control . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
Where To Decide Dependencies . . . . . . . . . . . . . . . . . . . . . 213
Introduction
Ruby on Rails is almost a decade old, and its community has developed a num-
ber 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 ap-
plications 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 ap-
plication 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 apply-
ing 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.
Code Reviews
Our rst step towards better code is to review it.
Have you ever sent an email with typos? Did you reviewwhat you wrote before
vi
INTRODUCTION vii
clicking Send? Reviewing your e-mails prevents mistakes and reviewing your
code does the same.
To make it easier to review code, always work in a feature branch. The branch
reduces the temptation to push unreviewed code or to wait too long to push
code.
The rst person who should review every line of your code is you. Before com-
mitting new code, read each changed line. Use gits diff and --patch features
to examine code before you commit. Read more about these features using
git help add and git help commit.
If youre working on a team, push your feature branch and invite your team-
mates to review the changes via git diff origin/master..HEAD.
Team review reveals how understandable code is to someone other than the
author. Your team members understanding now is a good indicator of your
understanding in the future.
However, what should you and your teammates look for during review?
Follow Your Nose
Code smells are indicators something may be wrong. They are useful be-
cause they are easy to see, sometimes easier than the root cause of a problem.
When you review code, watch for smells. Consider whether refactoring the
code to remove the smell would result in better code. If youre reviewing a
teammates feature branch, share your best refactoring ideas with them.
Smells are associated with one or more refactorings (ex: remove the Long
Method smell using the Extract Method refactoring). Learn these associations
in order to quickly consider them during review, and whether the result (ex:
several small methods) improves the code.
Dont treat code smells as bugs. It will be a waste of time to x every smell.
Not every smell is the symptom of a problem and despite your best intentions,
you can accidentally introduce another smell or problem.
INTRODUCTION viii
Removing Resistance
Another opportunity for refactoring is when youre having diculty making a a
change to existing code. This is called resistance. The refactoring you choose
depends on the type of resistance.
Is it hard to determine where new code belongs? The code is not readable
enough. Rename methods and variables until its obvious where your change
belongs. This and every subsequent change will be easier. Refactor for read-
ability rst.
Is it hard to change the code without breaking existing code? Add extension
points or extract code to be easier to reuse, and then try to introduce your
change. Repeat this process until the change you want is easy to introduce.
Each change should be easy to introduce. If its not, refactor.
When you are making your changes, you will be in a feature branch. Try to
make your change without refactoring. If your meet resistance, make a work
in progress commit, check out master, and create a new refactoring branch:
git commit -m 'wip: new feature'
git push
git checkout master
git checkout -b refactoring-for-new-feature
Refactor until you x the resistance you met on your feature branch. Then,
rebase your feature branch on top of your refactoring branch:
git rebase -i new-feature
git checkout new-feature
git merge refactoring-for-new-feature --ff-only
If the change is easier now, continue in your feature branch. If not, check out
your refactoring branch and try again.
INTRODUCTION ix
Bugs and Churn
If youre spending a lot of time swatting bugs, remove smells in the methods or
classes of the buggy code. Youll make it less likely that a bug will be reintro-
duced.
After you commit a bug x to a feature branch, nd out if the code you changed
to x the bug is in les which change often. If the buggy code changes often,
nd smells and eliminate them. Separate the parts that change often from the
parts that dont.
Conversely, avoid refactoring areas with low churn. Refactoring changes code,
and with each change, you risk introducing new bugs. If a le hasnt changed
in six months, leave it alone. It may not be pretty, but youll spend more time
looking at it when you break it trying to x something that wasnt broken.
Metrics
Various tools are available which can aid you in your search for code smells.
You can use og to detect complex parts of code. If you look at the classes and
methods with the highest og score, youll probably nd a few smells worth
investigating.
Duplication is one of the hardest problems to nd by hand. If youre using
dis during code reviews, it will be invisible when you copy and paste existing
methods. The original method will be unchanged and wont showup in the di,
so unless the reviewer knows and remembers that the original existed, they
wont notice that the copied method isnt just a new addition. Use ay to nd
duplication. Every duplicated piece of code is a bug waiting to happen.
When looking for smells, reek can nd certain smells reliably and quickly. At-
tempting to maintain a reek free code base is costly, but using reek once you
discover a problematic class or method may help you nd the solution.
To nd les with a high churn rate, try out the aptly-named churn gem. This
works best with Git, but will also work with Subversion.
INTRODUCTION x
You can also use Code Climate, a hosted tool which will scan your code for
issues every time you push to Git. Code Climate attempts to locate hot spots
for refactoring and assigns each class a simple A through F grade.
If youd prefer not to use a hosted service, you can use MetricFu to run a large
suite of tools to analyze your application.
Getting obsessed with the counts and scores from these tools will distract from
the actual issues in your code, but its worthwhile to run them continually and
watch out for potential warning signs.
How To Read This Book
This book contains three catalogs: smells, solutions, and principles.
Start by looking up a smell that sounds familiar. Each chapter on smells ex-
plains the potential problems each smell may reveal and references possible
solutions.
Once youve identied the problem revealed by a smell, read the relevant so-
lution chapter to learn how to x it. Each solution chapter will explain which
problems it addresses and potential problems which can be introduced.
Lastly, smell and solution chapters will reference related principles. The smell
chapters will reference principles that you can follow to avoid the root problem
in the future. The solution chapters will explain howeach solution changes your
code to follow related principles.
By following this process, youll learn how to detect and x actual problems in
your code using smells and reusable solutions, and youll learn about principles
that you can follow to improve the code you write from the beginning.
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
2
CHAPTER 1. LONG METHOD 3
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.
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.
Large Class
Most Rails applications suer from several Large Classes. Large classes are
dicult to understand and make it harder to change or reuse behavior. Tests
for large classes are slow and churn tends to be higher, leading to more bugs
and conicts. Large classes likely also suer from Divergent Change.
Symptoms
You cant easily describe what the class does in one sentence.
You cant tell what the class does without scrolling.
The class needs to change for more than one reason.
The class has more private methods than public methods.
The class has more than 7 methods.
The class has a total og score of 50.
Example
This class has a high og score, has a large number of methods, more private
than public methods, and has multiple responsibility:
# app/models/question.rb
class Question < ActiveRecord::Base
include ActiveModel::ForbiddenAttributesProtection
SUBMITTABLE_TYPES = %w(Open MultipleChoice Scale).freeze
4
CHAPTER 2. LARGE CLASS 5
validates :maximum, presence: true, if: :scale?
validates :minimum, presence: true, if: :scale?
validates :question_type, presence: true, inclusion: SUBMITTABLE_TYPES
validates :title, presence: true
belongs_to :survey
has_many :answers
has_many :options
accepts_nested_attributes_for :options, reject_if: :all_blank
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(:text).order('COUNT(*) DESC').count
percents = counts.map do |text, count|
percent = (100.0 * count / total).round
CHAPTER 2. LARGE CLASS 6
"#{percent}% #{text}"
end
percents.join(', ')
end
def summarize_open_answers
answers.order(:created_at).pluck(:text).join(', ')
end
def summarize_scale_answers
sprintf('Average: %.02f', answers.average('text'))
end
end
Solutions
Move Method to move methods to another class if an existing class could
better handle the responsibility.
Extract Class if the class has multiple responsibilities.
Replace Conditional with Polymorphism if the class contains private
methods related to conditional branches.
Extract Value Object if the class contains private query methods.
Extract Decorator if the class contains delegation methods.
Replace Subclasses with Strategies if the large class is a base class in an
inheritance hierarchy.
Prevention
Following the Single Responsibility Principle will prevent large classes from
cropping up. Its dicult for any class to become too large without taking on
more than one responsibility.
You can use og to analyze classes as you write and modify them:
% flog -a app/models/question.rb
48.3: flog total
CHAPTER 2. LARGE CLASS 7
6.9: flog/method average
15.6: Question#summarize_multiple_choice_answers app/models/question.rb:38
12.0: Question#none
6.3: Question#summary app/models/question.rb:17
5.2: Question#summarize_open_answers app/models/question.rb:48
3.6: Question#summarize_scale_answers app/models/question.rb:52
3.4: Question#steps app/models/question.rb:28
2.2: Question#scale? app/models/question.rb:34
God Class
A particular specimen of Large Class aects most Rails applications: the God
Class. A God Class is any class that seems to know everything about an appli-
cation. It has a reference to the majority of the other models, and its dicult
to answer any question or perform any action in the application without going
through this class.
Most applications have two God Classes: User, and the central focus of the
application. For a todo list application, it will be User and Todo; for photo sharing
application, it will be User and Photo.
You need to be particularly vigilant about refactoring these classes. If you dont
start splitting up your God Classes early on, then it will become impossible to
separate them without rewriting most of your application.
Treatment and prevention of God Classes is the same as for any Large Class.
Feature Envy
Feature envy reveals a method (or method-to-be) that would work better on a
dierent class.
Methods suering from feature envy contain logic that is dicult to reuse, be-
cause the logic is trapped within a method on the wrong class. These meth-
ods are also often private methods, which makes them unavailable to other
classes. Moving the method (or aected portion of a method) to a more appro-
priate class improves readability, makes the logic easier to reuse, and reduces
coupling.
Symptoms
Repeated references to the same object.
Parameters or local variables which are used more than methods and
instance variables of the class in question.
Methods that includes a class name in their own names (such as
invite_user).
Private methods on the same class that accept the same parameter.
Law of Demeter violations.
Tell, Dont Ask violations.
Example
# app/models/completion.rb
def score
8
CHAPTER 3. FEATURE ENVY 9
answers.inject(0) do |result, answer|
question = answer.question
result + question.score(answer.text)
end
end
The answer local variable is used twice in the block: once to get its question, and
once to get its text. This tells us that we can probably extract a new method
and move it to the Answer class.
Solutions
Extract Method if only part of the method suers from feature envy, and
then move the method.
Move Method if the entire method suers from feature envy.
Inline Classes if the envied class isnt pulling its weight.
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
10
CHAPTER 4. CASE STATEMENT 11
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.
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
CHAPTER 4. CASE STATEMENT 12
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 -%>
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.
Use Convention over Conguration when selecting a strategy based on
a string name.
Shotgun Surgery
Shotgun Surgery is usually a more obvious symptomthat reveals another smell.
Symptoms
You have to make the same small change across several dierent les.
Changes become dicult to manage because they are 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
Example
Users names are formatted and displayed as First Last throughout the appli-
cation. 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 %>
13
CHAPTER 5. SHOTGUN SURGERY 14
# 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 Polymorphismto 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 along-
side a data clump of related attributes.
Use Convention over Conguration to eliminate small steps that can be
inferred based on a convention such as a name.
Inline Classes that only serve to add extra steps when performing
changes.
Divergent Change
A class suers from Divergent Change when it changes for multiple reasons.
Symptoms
You cant easily describe what the class does in one sentence.
The class is changed more frequently than other classes in the applica-
tion.
Dierent changes to the class arent related to each other.
15
CHAPTER 6. DIVERGENT CHANGE 16
Example
# app/controllers/summaries_controller.rb
class SummariesController < ApplicationController
def show
@survey = Survey.find(params[:survey_id])
@summaries = @survey.summarize(summarizer)
end
private
def summarizer
case params[:id]
when 'breakdown'
Breakdown.new
when 'most_recent'
MostRecent.new
when 'your_answers'
UserAnswer.new(current_user)
else
raise "Unknown summary type: #{params[:id]}"
end
end
end
This controller has multiple reasons to change:
Control ow logic related to summaries, such as authentication.
Any time a summarizer strategy is added or changed.
Solutions
Extract Class to move one cause of change to a new class.
Move Method if the class is changing because of methods that relate to
another class.
Extract Validator to move validation logic out of models.
Introduce Form Object to move form logic out of controllers.
CHAPTER 6. DIVERGENT CHANGE 17
Use Convention over Conguration to eliminate changes that can be in-
ferred by a convention such as a name.
Prevention
You can prevent Divergent Change from occurring by following the Single Re-
sponsibility Principle. If a class has only one responsibility, it has only one rea-
son to change.
You can use churn to discover which les are changing most frequently. This
isnt a direct relationship, but frequently changed les often have more than
one responsibility, and thus more than one reason to change.
Long Parameter List
Ruby supports positional method arguments which can lead to Long Parameter
Lists.
Symptoms
You cant easily change the methods arguments.
The method has three or more arguments.
The method is complex due to number of collaborating parameters.
The method requires large amounts of setup during isolated testing.
18
CHAPTER 7. LONG PARAMETER LIST 19
Example
Look at this mailer for an example of Long Parameter List.
# app/mailers/mailer.rb
class Mailer < ActionMailer::Base
default from: "from@example.com"
def completion_notification(first_name, last_name, email)
@first_name = first_name
@last_name = last_name
mail(
to: email,
subject: 'Thank you for completing the survey'
)
end
end
Solutions
Introduce Parameter Object and pass it in as an object of naturally
grouped attributes.
A common technique used to mask a long parameter list is grouping parame-
ters using a hash of named parameters; this will replace connascence position
with connascence of name (a good rst step). However, it will not reduce the
number of collaborators in the method.
Extract Class if the method is complex due to the number of collabora-
tors.
Duplicated Code
One of the rst principles were taught as developers: Keep your code DRY.
Symptoms
You nd yourself copy and pasting code from one place to another.
Shotgun Surgery occurs when changes to your application require the
same small edits in multiple places.
20
CHAPTER 8. DUPLICATED CODE 21
Example
The QuestionsController suers from duplication in the create and update meth-
ods.
# app/controllers/questions_controller.rb
def create
@survey = Survey.find(params[:survey_id])
question_params = params.
require(:question).
permit(:title, :options_attributes, :minimum, :maximum)
@question = type.constantize.new(question_params)
@question.survey = @survey
if @question.save
redirect_to @survey
else
render :new
end
end
def update
@question = Question.find(params[:id])
question_params = params.
require(:question).
permit(:title, :options_attributes, :minimum, :maximum)
@question.update_attributes(question_params)
if @question.save
redirect_to @question.survey
else
render :edit
end
end
Solutions
Extract Method for duplicated code in the same le.
CHAPTER 8. DUPLICATED CODE 22
Extract Class for duplicated code across multiple les.
Extract Partial for duplicated view and template code.
Replace Conditional with Polymorphism for duplicated conditional logic.
Replace Conditional with Null Object to remove duplicated checks for
nil values.
Uncommunicative Name
Software is run by computers, but written and read by humans. Names provide
important information to developers who are trying to understand a piece of
code. Patterns and challenges when naming a method or class can also provide
clues for refactoring.
Symptoms
Diculty understanding a method or class.
Methods or classes with similar names but dissimilar functionality.
Redundant names, such as names which include the type of object to
which they refer.
Example
In our example application, the SummariesController generates summaries from
a Survey:
# app/controllers/summaries_controller.rb
@summaries = @survey.summaries_using(summarizer)
The summarize method on Survey asks each Question to summarize itself using a
summarizer:
23
CHAPTER 9. UNCOMMUNICATIVE NAME 24
# app/models/survey.rb
def summaries_using(summarizer)
questions.map do |question|
question.summarize(summarizer)
end
end
The summarize method on Question gets a value by calling summarize on a sum-
marizer, and then builds a Summary using that value.
# app/models/question.rb
def summarize(summarizer)
value = summarizer.summarize(self)
Summary.new(title, value)
end
There are several summarizer classes, each of which respond to summarize.
If youre lost, dont worry: youre not the only one. The confusing maze of similar
names make this example extremely hard to follow.
See Rename Method to see how we improve the situation.
Solutions
Rename Method if a well-factored method isnt well-named.
Extract Class if a class is doing too much to have a meaningful name.
Extract Method if a method is doing too much to have a meaningful name.
Inline Class if a class is too abstract to have a meaningful name.
Single Table Inheritance (STI)
Using subclasses is a common method of achieving reuse in object-oriented
software. Rails provides a mechanism for storing instances of dierent classes
in the same table, called Single Table Inheritance. Rails will take care of most
of the details, writing the classs name to the type column and instantiating the
correct class when results come back from the database.
Inheritance has its own pitfalls - see Composition Over Inheritance - and STI
introduces a few new gotchas that may cause you to consider an alternate
solution.
Symptoms
You need to change from one subclass to another.
Behavior is shared among some subclasses but not others.
One subclass is a fusion of one or more other subclasses.
25
CHAPTER 10. SINGLE TABLE INHERITANCE (STI) 26
Example
This method on Question changes the question to a new type. Any necessary
attributes for the new subclass are provided to the attributes method.
# app/models/question.rb
def switch_to(type, new_attributes)
attributes = self.attributes.merge(new_attributes)
new_question = type.constantize.new(attributes.except('id', 'type'))
new_question.id = id
begin
Question.transaction do
destroy
new_question.save!
end
rescue ActiveRecord::RecordInvalid
end
new_question
end
This transition is dicult for a number of reasons:
You need to worry about common Question validations.
You need to make sure validations for the old subclass are not used.
You need to make sure validations for the new subclass are used.
You need to delete data from the old subclass, including associations.
You need to support data from the new subclass.
Common attributes need to remain the same.
The implementation achieves all these requirements, but is awkward:
You cant actually change the class of an instance in Ruby, so you need
to return the instance of the new class.
CHAPTER 10. SINGLE TABLE INHERITANCE (STI) 27
The implementation requires deleting and creating records, but part of
the transaction (destroy) must execute before we can validate the new
instance. This results in control ow using exceptions.
The STI abstraction leaks into the model, because it needs to understand
that it has a type column. STI models normally dont need to understand
that theyre implemented using STI.
Its hard to understand why this method is implemented the way it is, so
other developers xing bugs or refactoring in the future will have a hard
time navigating it.
Solutions
If youre using STI to reuse common behavior, use Replace Subclasses
with Strategies to switch to a composition-based model.
If youre using STI so that you can easily refer to several dierent classes
in the same table, switch to using a polymorphic association instead.
Prevention
By following Composition Over Inheritance, youll use STI as a solution less
often.
Comments
Comments can be used appropriately to introduce classes and provide docu-
mentation, but used incorrectly, they mask readability and process problems
by further obfuscating already unreadable code.
Symptoms
Comments within method bodies.
More than one comment per method.
Comments that restate the method name in English.
TODO comments.
Commented out, dead code.
Example
# app/models/open_question.rb
def summary
# Text for each answer in order as a comma-separated string
answers.order(:created_at).pluck(:text).join(', ')
end
This comment is trying to explain what the following line of code does, because
the code itself is too hard to understand. A better solution would be to improve
the legibility of the code.
Some comments add no value at all and can safely be removed:
28
CHAPTER 11. COMMENTS 29
class Invitation
# Deliver the invitation
def deliver
Mailer.invitation_notification(self, message).deliver
end
end
If there isnt a useful explanation to provide for a method or class beyond the
name, dont leave a comment.
Solutions
Introduce Explaining Variable to make obfuscated lines easier to read in
pieces.
Extract Method to break up methods that are dicult to read.
Move TODO comments into a task management system.
Delete commented out code, and rely on version control in the event
that you want to get it back.
Delete superuous comments that dont add more value than the method
or class name.
Mixin
Inheritance is a common method of reuse in object-oriented software. Ruby
supports single inheritance using subclasses and multiple inheritance using
mixins. Mixins can be used to package common helpers or provide a common
public interface.
However, mixins have some drawbacks:
They use the same namespace as classes theyre mixed into, which can
cause naming conicts.
Although they have access to instance variables from classes theyre
mixed into, mixins cant easily accept initializer arguments, so they cant
have their own state.
They inate the number of methods available in a class.
Theyre not easy to add and remove at runtime.
Theyre dicult to test in isolation, since they cant be instantiated.
Symptoms
Methods in mixins that accept the same parameters over and over.
Methods in mixins that dont reference the state of the class theyre mixed
into.
Business logic that cant be used without using the mixin.
Classes which have few public methods except those from a mixin.
30
CHAPTER 12. MIXIN 31
Example
In our example application, users can invite their friends by email to take sur-
veys. If an invited email matches an existing user, a private message will be
created. Otherwise, a message is sent to that email address with a link.
The logic to generate the invitation message is the same regardless of the de-
livery mechanism, so this behavior is encapsulated in a mixin:
# app/models/inviter.rb
module Inviter
extend ActiveSupport::Concern
included do
include AbstractController::Rendering
include Rails.application.routes.url_helpers
self.view_paths = 'app/views'
self.default_url_options = ActionMailer::Base.default_url_options
end
private
def render_message_body
render template: 'invitations/message'
end
end
CHAPTER 12. MIXIN 32
Each delivery strategy mixes in Inviter and calls render_message_body:
# app/models/message_inviter.rb
class MessageInviter < AbstractController::Base
include Inviter
def initialize(invitation, recipient)
@invitation = invitation
@recipient = recipient
end
def deliver
Message.create!(
recipient: @recipient,
sender: @invitation.sender,
body: render_message_body
)
end
end
Although the mixin does a good job of preventing duplicated code, its dicult
to test or understand in isolation, it obfuscates the inviter classes, and it tightly
couples the inviter classes to a particular message body implementation.
Solutions
Extract Class to liberate business logic trapped in mixins.
Replace Mixin with Composition to improve testability, exibility, and
readability.
Prevention
Mixins are a form of inheritance. By following Composition Over Inheritance,
youll be less likely to introduce mixins.
Reserve mixins for reusable framework code like common associations and
callbacks, and youll end up with a more exible and comprehensible system.
Callback
Callbacks are a convenient way to decorate the default save method with cus-
tom persistence logic, without the drudgery of template methods, overriding,
or calling super.
However, callbacks are frequently abused by adding non-persistence logic to
the persistence life cycle, such as sending emails or processing payments.
Models riddled with callbacks are harder to refactor and prone to bugs, such as
accidentally sending emails or performing external changes before a database
transaction is committed.
Symptoms
Callbacks which contain business logic such as processing payments.
Attributes which allow certain callbacks to be skipped.
Methods such as save_without_sending_email which skip callbacks.
Callbacks which need to be invoked conditionally.
33
CHAPTER 13. CALLBACK 34
Example
# app/models/survey_inviter.rb
def deliver_invitations
recipients.map do |recipient_email|
Invitation.create!(
survey: survey,
sender: sender,
recipient_email: recipient_email,
status: 'pending',
message: @message
)
end
end
# app/models/invitation.rb
after_create :deliver
# app/models/invitation.rb
def deliver
Mailer.invitation_notification(self).deliver
end
In the above code, the SurveyInviter is simply creating Invitation records, and
the actual delivery of the invitation email is hidden behind Invitation.create!
via a callback.
If one of several invitations fails to save, the user will see a 500 page, but some
of the invitations will already have been saved and delivered. The user will be
unable to tell which invitations were sent.
Because delivery is coupled with persistence, theres no way to make sure that
all of the invitations are saved before starting to deliver emails.
Solutions
Replace Callback with Method if the callback logic is unrelated to persis-
tence.
Part II
Solutions
35
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 devel-
opers to avoid conditionals using polymorphism. Rather than using if/else or
case/when to create a conditional path for each possible situation, you can im-
plement a method dierently in dierent 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
dont need to change when the application changes.
Uses
Removes Divergent Change from classes that need to alter their behav-
ior based on the outcome of the condition.
Removes 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.
36
CHAPTER 14. REPLACE CONDITIONAL WITH POLYMORPHISM 37
Example
This Question class summarizes its answers dierently depending on its
question_type:
# app/models/question.rb
class Question < ActiveRecord::Base
include ActiveModel::ForbiddenAttributesProtection
SUBMITTABLE_TYPES = %w(Open MultipleChoice Scale).freeze
validates :maximum, presence: true, if: :scale?
validates :minimum, presence: true, if: :scale?
validates :question_type, presence: true, inclusion: SUBMITTABLE_TYPES
validates :title, presence: true
belongs_to :survey
has_many :answers
has_many :options
accepts_nested_attributes_for :options, reject_if: :all_blank
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
CHAPTER 14. REPLACE CONDITIONAL WITH POLYMORPHISM 38
def scale?
question_type == 'Scale'
end
def summarize_multiple_choice_answers
total = answers.count
counts = answers.group(:text).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(:created_at).pluck(:text).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 isnt the only place in the application that checks question
types, meaning that new types will cause Shotgun Surgery.
CHAPTER 14. REPLACE CONDITIONAL WITH POLYMORPHISM 39
Replace Type Code With Subclasses
Lets 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 sub-
classes of Question, we have to tell ActiveRecord which subclass to instantiate
when it fetches records fromthe questions table. The mechanismRails uses for
storing instances of dierent 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 rst 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 Multi-
pleChoice, which wont make good class names; names like OpenQuestion
would be better, so lets 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
CHAPTER 14. REPLACE CONDITIONAL WITH POLYMORPHISM 40
# 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 renam-
ing, and possible breaks from using STI. Therefore, lets 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
dened, so lets add some placeholder classes:
# app/models/open_question.rb
class OpenQuestion < Question
end
# app/models/scale_question.rb
class ScaleQuestion < Question
end
CHAPTER 14. REPLACE CONDITIONAL WITH POLYMORPHISM 41
# 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 well need to update a few more references. For
example, well 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 :questions, :question_type, :type
end
def down
rename_column :questions, :type, :question_type
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:
CHAPTER 14. REPLACE CONDITIONAL WITH POLYMORPHISM 42
# app/controllers/questions_controller.rb
def build_question
@question = type.constantize.new(question_params)
@question.survey = @survey
end
def type
params[:question][:type]
end
At this point, were ready to proceed with a regular refactoring.
Extracting Type-Specic Code
The next step is to move type-specic code from Question into the subclass for
each specic type.
Lets 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 rst step is to use Extract Method to move each path to its own method. In
this case, we already extractedmethods called summarize_multiple_choice_answers,
summarize_open_answers, and summarize_scale_answers, so we can proceed im-
mediately.
CHAPTER 14. REPLACE CONDITIONAL WITH POLYMORPHISM 43
The next step is to use Move Method to move the extracted method to the ap-
propriate class. First, lets 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(:text).order('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, weve already created all our subclasses, but you can use Extract
Class to create themif youre 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 doesnt 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:
CHAPTER 14. REPLACE CONDITIONAL WITH POLYMORPHISM 44
# app/views/questions/_question.html.erb
<% if question.type == 'MultipleChoiceQuestion' -%>
<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.type == 'ScaleQuestion' -%>
<ol>
<% question.steps.each do |step| -%>
<li>
<%= submission_fields.radio_button :text, step %>
<%= submission_fields.label "text_#{step}", label: step %>
</li>
<% end -%>
</ol>
<% end -%>
<% if question.type == 'OpenQuestion' -%>
<%= submission_fields.text_field :text %>
<% end -%>
In the previous example, we moved type-specic code into Question subclasses.
However, moving viewcode would violate MVC (introducing Divergent Change
into the subclasses), and more importantly, it would be ugly and hard to under-
stand.
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
CHAPTER 14. REPLACE CONDITIONAL WITH POLYMORPHISM 45
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-specic view code into a view for each type:
# app/views/open_questions/_open_question.html.erb
<%= submission_fields.text_field :text %>
You can see the full change in commit 8243493.
Multiple Polymorphic Views
Our application also has dierent elds on the question formdepending on the
question type. Currently, that also performs type-checking:
# app/views/questions/new.html.erb
<% if @question.type == 'MultipleChoiceQuestion' -%>
<%= form.fields_for(:options, @question.options_for_form) do |option_fields| -%>
<%= option_fields.input :text, label: 'Option' %>
<% end -%>
<% end -%>
<% if @question.type == 'ScaleQuestion' -%>
<%= form.input :minimum %>
<%= form.input :maximum %>
<% end -%>
We already used views like open_questions/_open_question.html.erb for show-
ing a question, so we cant just put the edit code there. Rails doesnt support
prexes or suxes 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.
CHAPTER 14. REPLACE CONDITIONAL WITH POLYMORPHISM 46
Drawbacks
Its worth noting that, although this refactoring improved our particular example,
replacing conditionals with polymorphism is not without drawbacks.
Using polymorphismlike this makes it easier to add newtypes, because adding
a newtype means you just need to add a newclass and implement the required
methods. Adding a newtype wont require changes to any existing classes, and
its easy to understand what the types are, because each type is encapsulated
within a class.
However, this change makes it harder to add newbehaviors. Adding a newbe-
havior will mean nding every type and adding a new method. Understanding
the behavior becomes more dicult, because the implementations are spread
out among the types. Object-oriented languages lean towards polymorphic im-
plementations, but if you nd yourself adding behaviors much more often than
adding types, you should look into using observers or visitors instead.
Also, using STI has specic 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 aect the newtypes, watching out for Shot-
gun Surgery that can result from splitting up classes.
Replace conditional with Null
Object
Every Ruby developer is familiar with nil, and Ruby on Rails comes with a full
complement 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
47
CHAPTER 15. REPLACE CONDITIONAL WITH NULL OBJECT 48
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
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
CHAPTER 15. REPLACE CONDITIONAL WITH NULL OBJECT 49
# app/models/user.rb
def answer_text_for(question)
question.answers.for_user(self).text
end
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 15. REPLACE CONDITIONAL WITH NULL OBJECT 50
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,
CHAPTER 15. REPLACE CONDITIONAL WITH NULL OBJECT 51
you may be confused to nd that most_recent_answer returned an instance
of NullAnswer and not Answer.
Its possible some methods will need to distinguish between NullAnswers
and real Answers. This is common in views, when special markup is re-
quired to denote missing values. In this case, youll need to add explicit
present? checks and dene present? to return false on your null object.
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 val-
ues to grow annoyed. And make sure the removal of the nil-handling logic
outweighs the drawbacks above.
Next Steps
Look for other nil checks of 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.
CHAPTER 15. REPLACE CONDITIONAL WITH NULL OBJECT 52
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.
53
CHAPTER 16. EXTRACT METHOD 54
Example
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.
Any of these tasks can be extracted to a method. Lets start by extracting the
task of building the question.
CHAPTER 16. EXTRACT METHOD 55
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.
CHAPTER 16. EXTRACT METHOD 56
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
Other Examples
For more examples of Extract Method, take a look at these chapters:
Extract Class: b434954d, 000babe1
Extract Decorator: 15f5b96e
Introduce Explaining Variable (inline)
Move Method: d5b4871
Replace Conditional with Null Object: 1e35c68
Next Steps
Check the original method and the extracted method to make sure nei-
ther 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. If you nd some, you
may wish to employ Move Method to provide the new method with a
better home.
CHAPTER 16. EXTRACT METHOD 57
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.
Rename Method
Renaming a method allows developers to improve the language of the domain
as their understanding naturally evolves during development.
The process is straightforward if there arent too many references:
Choose a new name for the method. This is the hard part!
Change the method denition to the new name.
Find and replace all references to the old name.
If there are a large number of references to the method you want to rename,
you can rename the callers one at a time while keeping everything in working
order. The process is mostly the same:
Choose a new name for the method. This is the hard part!
Give the method its new name.
Add an alias to keep the old name working.
Find and replace all references to the old name.
Remove the alias.
Uses
Eliminate Uncommunicative Names.
Change method names to conform to common interfaces.
58
CHAPTER 17. RENAME METHOD 59
Example
In our example application, we generate summaries from answers to surveys.
We allowmore than one type of summary, so strategies are employed to handle
the variations. There are a number of methods and dependencies that make
this work.
SummariesController#show depends on Survey#summarize:
# app/controllers/summaries_controller.rb
@summaries = @survey.summarize(summarizer)
Survey#summarize depends on Question#summarize:
# app/models/survey.rb
def summarize(summarizer)
questions.map do |question|
question.summarize(summarizer)
end
end
Question#summarize depends on summarize from its summarizer argument (a strat-
egy):
# app/models/question.rb
def summarize(summarizer)
value = summarizer.summarize(self)
Summary.new(title, value)
end
There are several summarizer classes, each of which respond to summarize.
This is confusing, largely because the word summarize is used to mean several
dierent things:
Survey#summarize accepts a summarizer and returns an array of Summary
instances.
CHAPTER 17. RENAME METHOD 60
Question#summarize accepts a summarizer and returns a single Summary in-
stance.
summarize on summarizer strategies accepts a Question and returns a
String.
Lets rename these methods so that each name is used uniquely and consis-
tently in terms of what it accepts, what it returns, and what it does.
First, well rename Survey#summarize to reect the fact that it returns a collection.
# app/models/survey.rb
def summaries_using(summarizer)
Then, well update the only reference to the old method:
# app/controllers/summaries_controller.rb
@summaries = @survey.summaries_using(summarizer)
Next, well rename Question#summarize to be consistent with the naming intro-
duced in Survey:
# app/models/question.rb
def summary_using(summarizer)
Finally, well update the only reference in Survey#summaries_using:
# app/models/survey.rb
question.summary_using(summarizer)
We now have consistent and clearer naming:
summarize means taking a question and returning a string value repre-
senting its answers.
summary_using means taking a summarizer and using it to build a Summary.
summaries_using means taking a set of questions and building a Summary
for each one.
CHAPTER 17. RENAME METHOD 61
Next Steps
Check for explanatory comments that are no longer necessary now that
the code is clearer.
If the newname for a method is long, see if you can extract methods from
it make it smaller.
Extract Class
Dividing responsibilities into classes is the primary way to manage complexity
in object-oriented software. Extract Class is the primary mechanism for intro-
ducing new classes. This refactoring takes one class and splits it into two by
moving one or more methods and instance variables into a new class.
The process for extracting a class looks like this:
1. Create a new, empty class.
2. Instantiate the new class from the original class.
3. Move a method from the original class to the new class.
4. Repeat step 3 until youre happy with the original class.
Uses
Removes Large Class by splitting up the class.
Eliminates Divergent Change by moving one reason to change into a
new class.
Provides a cohesive set of functionality with a meaningful name, making
it easier to understand and talk about.
Fully encapsulates a concern within a single class, following the Single
Responsibility Principle and making it easier to change and reuse that
functionality.
62
CHAPTER 18. EXTRACT CLASS 63
Example
The InvitationsController is a Large Class hidden behind a Long Method:
# app/controllers/invitations_controller.rb
class InvitationsController < ApplicationController
EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
def new
@survey = Survey.find(params[:survey_id])
end
def create
@survey = Survey.find(params[:survey_id])
@recipients = params[:invitation][:recipients]
recipient_list = @recipients.gsub(/\s+/, '').split(/[\n,;]+/)
@invalid_recipients = recipient_list.map do |item|
unless item.match(EMAIL_REGEX)
item
end
end.compact
@message = params[:invitation][:message]
if @invalid_recipients.empty? && @message.present?
recipient_list.each do |email|
invitation = Invitation.create(
survey: @survey,
sender: current_user,
recipient_email: email,
status: 'pending'
)
Mailer.invitation_notification(invitation, @message)
end
redirect_to survey_path(@survey), notice: 'Invitation successfully sent'
CHAPTER 18. EXTRACT CLASS 64
else
render 'new'
end
end
end
Although it contains only two methods, theres a lot going on under the hood.
It parses and validates emails, manages several pieces of state which the view
needs to knowabout, handles control owfor the user, and creates and delivers
invitations.
A liberal application of Extract Method to break up this Long Method will reveal
the complexity:
# app/controllers/invitations_controller.rb
class InvitationsController < ApplicationController
EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
def new
@survey = Survey.find(params[:survey_id])
end
def create
@survey = Survey.find(params[:survey_id])
if valid_recipients? && valid_message?
recipient_list.each do |email|
invitation = Invitation.create(
survey: @survey,
sender: current_user,
recipient_email: email,
status: 'pending'
)
Mailer.invitation_notification(invitation, message)
end
redirect_to survey_path(@survey), notice: 'Invitation successfully sent'
else
@recipients = recipients
@message = message
CHAPTER 18. EXTRACT CLASS 65
render 'new'
end
end
private
def valid_recipients?
invalid_recipients.empty?
end
def valid_message?
message.present?
end
def invalid_recipients
@invalid_recipients ||= recipient_list.map do |item|
unless item.match(EMAIL_REGEX)
item
end
end.compact
end
def recipient_list
@recipient_list ||= recipients.gsub(/\s+/, '').split(/[\n,;]+/)
end
def recipients
params[:invitation][:recipients]
end
def message
params[:invitation][:message]
end
end
Lets extract all of the non-controller logic into a new class. Well start by den-
ing and instantiating a new, empty class:
CHAPTER 18. EXTRACT CLASS 66
# app/controllers/invitations_controller.rb
@survey_inviter = SurveyInviter.new
# app/models/survey_inviter.rb
class SurveyInviter
end
At this point, weve created a staging area for using Move Method to transfer
complexity from one class to the other.
Next, well move one method from the controller to our new class. Its best
to move methods which depend on few private methods or instance variables
from the original class, so well start with a method which only uses one private
method:
# app/models/survey_inviter.rb
def recipient_list
@recipient_list ||= @recipients.gsub(/\s+/, '').split(/[\n,;]+/)
end
We need the recipients for this method, so well accept it in the initialize
method:
# app/models/survey_inviter.rb
def initialize(recipients)
@recipients = recipients
end
And pass it from our controller:
# app/controllers/invitations_controller.rb
@survey_inviter = SurveyInviter.new(recipients)
The original controller method can delegate to the extracted method:
CHAPTER 18. EXTRACT CLASS 67
# app/controllers/invitations_controller.rb
def recipient_list
@survey_inviter.recipient_list
end
Weve moved a little complexity out of our controller, and we now have a re-
peatable process for doing so: we can continue to move methods out until we
feel good about whats left in the controller.
Next, lets move out invalid_recipients from the controller, since it depends on
recipient_list, which we already moved:
# app/models/survey_inviter.rb
def invalid_recipients
@invalid_recipients ||= recipient_list.map do |item|
unless item.match(EMAIL_REGEX)
item
end
end.compact
end
Again, the original controller method can delegate:
# app/controllers/invitations_controller.rb
def invalid_recipients
@survey_inviter.invalid_recipients
end
This method references a constant from the controller. This was the only place
where the constant was used, so we can move it to our new class:
# app/models/survey_inviter.rb
EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
We can remove an instance variable in the controller by invoking this method
directly in the view:
CHAPTER 18. EXTRACT CLASS 68
# app/views/invitations/new.html.erb
<% if @survey_inviter.invalid_recipients %>
<div class="error">
Invalid email addresses:
<%= @survey_inviter.invalid_recipients.join(', ') %>
</div>
<% end %>
Now that parsing email lists is moved out of our controller, lets extract and del-
egate the only method in the controller which depends on invalid_recipients:
# app/models/survey_inviter.rb
def valid_recipients?
invalid_recipients.empty?
end
Now we can remove invalid_recipients from the controller entirely.
The valid_recipients? method is only used in the compound validation condi-
tion:
# app/controllers/invitations_controller.rb
if valid_recipients? && valid_message?
If we extract valid_message? as well, we can fully encapsulate validation within
SurveyInviter.
# app/models/survey_inviter.rb
def valid_message?
@message.present?
end
We need message for this method, so well add that to initialize:
# app/models/survey_inviter.rb
def initialize(message, recipients)
@message = message
@recipients = recipients
end
CHAPTER 18. EXTRACT CLASS 69
And pass it in:
# app/controllers/invitations_controller.rb
@survey_inviter = SurveyInviter.new(message, recipients)
We can now extract a method to encapsulate this compound condition:
# app/models/survey_inviter.rb
def valid?
valid_message? && valid_recipients?
end
And use that new method in our controller:
# app/controllers/invitations_controller.rb
if @survey_inviter.valid?
Now these methods can be private, trimming down the public interface for
SurveyInviter:
# app/models/survey_inviter.rb
private
def valid_message?
@message.present?
end
def valid_recipients?
invalid_recipients.empty?
end
Weve pulled out most of the private methods, so the remaining complexity is
largely from saving and delivering the invitations.
CHAPTER 18. EXTRACT CLASS 70
Lets extract and move a deliver method for that:
# app/models/survey_inviter.rb
def deliver
recipient_list.each do |email|
invitation = Invitation.create(
survey: @survey,
sender: @sender,
recipient_email: email,
status: 'pending'
)
Mailer.invitation_notification(invitation, @message)
end
end
We need the sender (the currently signed in user) as well as the survey fromthe
controller to do this. This pushes our initialize method up to four parameters,
so lets switch to a hash:
# app/models/survey_inviter.rb
def initialize(attributes = {})
@survey = attributes[:survey]
@message = attributes[:message] || ''
@recipients = attributes[:recipients] || ''
@sender = attributes[:sender]
end
And extract a method in our controller to build it:
# app/controllers/invitations_controller.rb
def survey_inviter_attributes
params[:invitation].merge(survey: @survey, sender: current_user)
end
Now we can invoke this method in our controller:
CHAPTER 18. EXTRACT CLASS 71
# app/controllers/invitations_controller.rb
if @survey_inviter.valid?
@survey_inviter.deliver
redirect_to survey_path(@survey), notice: 'Invitation successfully sent'
else
@recipients = recipients
@message = message
render 'new'
end
The recipient_list method is now only used internally in SurveyInviter, so lets
make it private.
Weve moved most of the behavior out of the controller, but were still assigning
a number of instance variables for the view, which have corresponding private
methods in the controller. These values are also available on SurveyInviter,
which is already assigned to the view, so lets expose those using attr_reader:
# app/models/survey_inviter.rb
attr_reader :message, :recipients, :survey
CHAPTER 18. EXTRACT CLASS 72
And use them directly from the view:
# app/views/invitations/new.html.erb
<%= simple_form_for(
:invitation,
url: survey_invitations_path(@survey_inviter.survey)
) do |f| %>
<%= f.input(
:message,
as: :text,
input_html: { value: @survey_inviter.message }
) %>
<% if @invlid_message %>
<div class="error">Please provide a message</div>
<% end %>
<%= f.input(
:recipients,
as: :text,
input_html: { value: @survey_inviter.recipients }
) %>
Only the SurveyInviter is used in the controller now, so we can remove the
remaining instance variables and private methods.
CHAPTER 18. EXTRACT CLASS 73
Our controller is now much simpler:
# app/controllers/invitations_controller.rb
class InvitationsController < ApplicationController
def new
@survey_inviter = SurveyInviter.new(survey: survey)
end
def create
@survey_inviter = SurveyInviter.new(survey_inviter_attributes)
if @survey_inviter.valid?
@survey_inviter.deliver
redirect_to survey_path(survey), notice: 'Invitation successfully sent'
else
render 'new'
end
end
private
def survey_inviter_attributes
params[:invitation].merge(survey: survey, sender: current_user)
end
def survey
Survey.find(params[:survey_id])
end
end
It only assigns one instance variable, it doesnt have too many methods, and all
of its methods are fairly small.
CHAPTER 18. EXTRACT CLASS 74
The newly extracted SurveyInviter class absorbed much of the complexity, but
still isnt as bad as the original controller:
# app/models/survey_inviter.rb
class SurveyInviter
EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
def initialize(attributes = {})
@survey = attributes[:survey]
@message = attributes[:message] || ''
@recipients = attributes[:recipients] || ''
@sender = attributes[:sender]
end
attr_reader :message, :recipients, :survey
def valid?
valid_message? && valid_recipients?
end
def deliver
recipient_list.each do |email|
invitation = Invitation.create(
survey: @survey,
sender: @sender,
recipient_email: email,
status: 'pending'
)
Mailer.invitation_notification(invitation, @message)
end
end
def invalid_recipients
@invalid_recipients ||= recipient_list.map do |item|
unless item.match(EMAIL_REGEX)
item
end
end.compact
CHAPTER 18. EXTRACT CLASS 75
end
private
def valid_message?
@message.present?
end
def valid_recipients?
invalid_recipients.empty?
end
def recipient_list
@recipient_list ||= @recipients.gsub(/\s+/, '').split(/[\n,;]+/)
end
end
We can take this further by extracting more classes fromSurveyInviter. See our
full solution on GitHub.
Drawbacks
Extracting classes decreases the amount of complexity in each class, but in-
creases the overall complexity of the application. Extracting too many classes
will create a maze of indirection which developers will be unable to navigate.
Every class also requires a name. Introducing new names can help to explain
functionality at a higher level and facilitates communication between devel-
opers. However, introducing too many names results in vocabulary overload,
which makes the system dicult to learn for new developers.
Extract classes in response to pain and resistance, and youll end up with just
the right number of classes and names.
Next Steps
Check the newly extracted class to make sure it isnt a Large Class, and
extract another class if it is.
CHAPTER 18. EXTRACT CLASS 76
Check the original class for Feature Envy of the extracted class, and use
Move Method if necessary.
Extract Value Object
Value Objects are objects that represent a value (such as a dollar amount) rather
than a unique, identiable entity (such as a particular user).
Value Objects often implement information derived froma primitive object, such
as the dollars and cents froma oat, or the user name and domain froman email
string.
Uses
Remove Duplicated Code from making the same observations of primi-
tive objects throughout the code base.
Remove Large Classes by splitting out query methods associated with a
particular variable.
Make the code easier to understand by fully-encapsulating related logic
into a single class, following the Single Responsibility Principle.
Eliminate Divergent Change by extracting code related to an embedded
semantic type.
Example
InvitationsController is bloated with methods and logic relating to parsing a
string that contains a list of email addresses:
# app/controllers/invitations_controller.rb
def recipient_list
77
CHAPTER 19. EXTRACT VALUE OBJECT 78
@recipient_list ||= recipients.gsub(/\s+/, '').split(/[\n,;]+/)
end
def recipients
params[:invitation][:recipients]
end
We can extract a new class to ooad this responsibility:
# app/models/recipient_list.rb
class RecipientList
include Enumerable
def initialize(recipient_string)
@recipient_string = recipient_string
end
def each(&block)
recipients.each(&block)
end
def to_s
@recipient_string
end
private
def recipients
@recipient_string.to_s.gsub(/\s+/, '').split(/[\n,;]+/)
end
end
# app/controllers/invitations_controller.rb
def recipient_list
@recipient_list ||= RecipientList.new(params[:invitation][:recipients])
end
CHAPTER 19. EXTRACT VALUE OBJECT 79
Next Steps
Search the application for Duplicated Code related to the newly
extracted class.
Value Objects should be Immutable. Make sure the extracted class
doesnt have any writer methods.
Extract Decorator
Decorators can be used to lay new concerns on top of existing objects with-
out modifying existing classes. They combine best with small classes with few
methods, and make the most sense when modifying the behavior of existing
methods, rather than adding new methods.
The steps for extracting a decorator vary depending on the initial state, but they
often include the following:
1. Extract a new decorator class, starting with the alternative behavior.
2. Compose the decorator in the original class.
3. Move state specic to the alternate behavior into the decorator.
4. Invert control, applying the decorator to the original class from its con-
tainer, rather than composing the decorator from the original class.
Uses
Eliminate Large Classes by extracting concerns.
Eliminate Divergent Change and follow the Open Closed Principle by
making it easier to modify behavior without modifying existing classes.
Prevent conditional logic from leaking by making decisions earlier.
Example
In our example application, users can view a summary of the answers to each
question on a survey. In order to prevent the summary frominuencing a users
80
CHAPTER 20. EXTRACT DECORATOR 81
own answers, users dont see summaries for questions they havent answered
yet by default. Users can click a link to override this decision and view the
summary for every question. This concern is mixed across several levels, and
introducing the change aected several classes. Lets see if we can refactor
our application to make similar changes easier in the future.
Currently, the controller determines whether or not unanswered questions
should display summaries:
# app/controllers/summaries_controller.rb
def constraints
if include_unanswered?
{}
else
{ answered_by: current_user }
end
end
def include_unanswered?
params[:unanswered]
end
It passes this decision into Survey#summaries_using as a hash containing boolean
ag:
# app/controllers/summaries_controller.rb
@summaries = @survey.summaries_using(summarizer, constraints)
CHAPTER 20. EXTRACT DECORATOR 82
Survey#summaries_using uses this information to decide whether each question
should return a real summary or a hidden summary:
# app/models/survey.rb
def summaries_using(summarizer, options = {})
questions.map do |question|
if !options[:answered_by] || question.answered_by?(options[:answered_by])
question.summary_using(summarizer)
else
Summary.new(question.title, NO_ANSWER)
end
end
end
This method is pretty dense. We can start by using Extract Method to clarify
and reveal complexity:
# app/models/survey.rb
def summaries_using(summarizer, options = {})
questions.map do |question|
summary_or_hidden_answer(summarizer, question, options[:answered_by])
end
end
private
def summary_or_hidden_answer(summarizer, question, answered_by)
if hide_unanswered_question?(question, answered_by)
hide_answer_to_question(question)
else
question.summary_using(summarizer)
end
end
def hide_unanswered_question?(question, answered_by)
answered_by && !question.answered_by?(answered_by)
end
CHAPTER 20. EXTRACT DECORATOR 83
def hide_answer_to_question(question)
Summary.new(question.title, NO_ANSWER)
end
The summary_or_hidden_answer method reveals a pattern thats well-captured by
using a Decorator:
Theres a base case: returning the real summary for the questions an-
swers.
Theres an alternate, or decorated, case: returning a summary with a
hidden answer.
The conditional logic for using the base or decorated case is unrelated
to the base case: answered_by is only used for determining which path to
take, and isnt used by to generate summaries.
As a Rails developer, this may seem familiar to you: many pieces of Rack mid-
dleware follow a similar approach.
Now that weve recognized this pattern, lets refactor to use a Decorator.
Move decorated case to decorator
Lets start by creating an empty class for the decorator and moving one method
into it:
# app/models/unanswered_question_hider.rb
class UnansweredQuestionHider
NO_ANSWER = "You haven't answered this question".freeze
def hide_answer_to_question(question)
Summary.new(question.title, NO_ANSWER)
end
end
The method references a constant from Survey, so moved that, too.
Now we update Survey to compose our new class:
CHAPTER 20. EXTRACT DECORATOR 84
# app/models/survey.rb
def summary_or_hidden_answer(summarizer, question, answered_by)
if hide_unanswered_question?(question, answered_by)
UnansweredQuestionHider.new.hide_answer_to_question(question)
else
question.summary_using(summarizer)
end
end
At this point, the decorated path is contained within the decorator.
Move conditional logic into decorator
Next, we can move the conditional logic into the decorator. Weve already ex-
tracted this to its own method on Survey, so we can simply move this method
over:
# app/models/unanswered_question_hider.rb
def hide_unanswered_question?(question, user)
user && !question.answered_by?(user)
end
Note that the answered_by parameter was renamed to user. Thats because, now
that the context is more specic, its clear what role the user is playing.
# app/models/survey.rb
def summary_or_hidden_answer(summarizer, question, answered_by)
hider = UnansweredQuestionHider.new
if hider.hide_unanswered_question?(question, answered_by)
hider.hide_answer_to_question(question)
else
question.summary_using(summarizer)
end
end
CHAPTER 20. EXTRACT DECORATOR 85
Move body into decorator
Theres just one summary-relatedmethod left in Survey: summary_or_hidden_answer.
Lets move this into the decorator:
# app/models/unanswered_question_hider.rb
def summary_or_hidden_answer(summarizer, question, user)
if hide_unanswered_question?(question, user)
hide_answer_to_question(question)
else
question.summary_using(summarizer)
end
end
# app/models/survey.rb
def summaries_using(summarizer, options = {})
questions.map do |question|
UnansweredQuestionHider.new.summary_or_hidden_answer(
summarizer,
question,
options[:answered_by]
)
end
end
At this point, every other method in the decorator can be made private.
CHAPTER 20. EXTRACT DECORATOR 86
Promote parameters to instance variables
Nowthat we have a class to handle this logic, we can move some of the parame-
ters into instance state. In Survey#summaries_using, we use the same summarizer
and user instance; only the question varies as we iterate through questions to
summarize. Lets move everything but question into instance variables on the
decorator:
# app/models/unanswered_question_hider.rb
def initialize(summarizer, user)
@summarizer = summarizer
@user = user
end
def summary_or_hidden_answer(question)
if hide_unanswered_question?(question)
hide_answer_to_question(question)
else
question.summary_using(@summarizer)
end
end
# app/models/survey.rb
def summaries_using(summarizer, options = {})
questions.map do |question|
UnansweredQuestionHider.new(summarizer, options[:answered_by]).
summary_or_hidden_answer(question)
end
end
Our decorator now just needs a question to generate a Summary.
Change decorator to follow component interface
In the end, the component we want to wrap with our decorator is the summa-
rizer, so we want the decorator to obey the same interface as its component,
CHAPTER 20. EXTRACT DECORATOR 87
the summarizer. Lets rename our only public method so that it follows the sum-
marizer interface:
# app/models/unanswered_question_hider.rb
def summarize(question)
# app/models/survey.rb
UnansweredQuestionHider.new(summarizer, options[:answered_by]).
summarize(question)
Our decorator now follows the component interface in name, but not behavior.
In our application, summarizers return a string which represents the answers
to a question, but our decorator is returning a Summary instead. Lets x our
decorator to follow the component interface by returning just a string:
# app/models/unanswered_question_hider.rb
def summarize(question)
if hide_unanswered_question?(question)
hide_answer_to_question(question)
else
@summarizer.summarize(question)
end
end
# app/models/unanswered_question_hider.rb
def hide_answer_to_question(question)
NO_ANSWER
end
# app/models/survey.rb
def summaries_using(summarizer, options = {})
questions.map do |question|
hider = UnansweredQuestionHider.new(summarizer, options[:answered_by])
question.summary_using(hider)
end
end
CHAPTER 20. EXTRACT DECORATOR 88
Our decorator now follows the component interface.
That last method on the decorator (hide_answer_to_question) isnt pulling its
weight anymore: it just returns the value from a constant. Lets inline it to slim
down our class a bit:
# app/models/unanswered_question_hider.rb
def summarize(question)
if hide_unanswered_question?(question)
NO_ANSWER
else
@summarizer.summarize(question)
end
end
Now we have a decorator that can wrap any summarizer, nicely-factored and
ready to use.
Invert control
Nowcomes one of the most important steps: we can invert control by removing
any reference to the decorator fromSurvey and passing in an already-decorated
summarizer.
The summaries_using method is simplied:
# app/models/survey.rb
def summaries_using(summarizer)
questions.map do |question|
question.summary_using(summarizer)
end
end
Instead of passing the boolean ag down from the controller, we can make the
decision to decorate there and pass a decorated or undecorated summarizer:
CHAPTER 20. EXTRACT DECORATOR 89
# app/controllers/summaries_controller.rb
def show
@survey = Survey.find(params[:survey_id])
@summaries = @survey.summaries_using(decorated_summarizer)
end
private
def decorated_summarizer
if include_unanswered?
summarizer
else
UnansweredQuestionHider.new(summarizer, current_user)
end
end
This isolates the decision to one class and keeps the result of the decision close
to the class that makes it.
Another important eect of this refactoring is that the Survey class is now re-
verted back to the way it was before we started hiding unanswered question
summaries. This means that we can nowadd similar changes without modifying
Survey at all.
Drawbacks
Decorators must keep up-to-date with their component interface. Our
decorator follows the summarizer interface. Every decorator we add for
this interface is one more class that will need to change any time we
change the interface.
We removed a concern from Survey by hiding it behind a decorator, but
this may make it harder for a developer to understand howa Survey might
return the hidden response text, as that text doesnt appear anywhere in
that class.
The component we decorated had the smallest possible interface: one
public method. Classes with more public methods are more dicult to
decorate.
CHAPTER 20. EXTRACT DECORATOR 90
Decorators can modify methods in the component interface easily,
but adding new methods wont work with multiple decorators without
metaprogramming like method_missing. These constructs are harder to
follow and should be used with care.
Next Steps
Its unlikely that your automated test suite has enough coverage to check
every component implementation with every decorator. Run through the
application in a browser after introducing new decorators. Test and x
any issues you run into.
Make sure that inverting control didnt push anything over the line into a
Large Class.
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 viewto change.
Group common code.
Reduce view size and complexity.
Steps
Create a new le for partial prexed with an underscore (_le-
name.html.erb).
Move common code into newly created le.
Render the partial from the source le.
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).
91
CHAPTER 21. EXTRACT PARTIAL 92
# 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:
CHAPTER 21. EXTRACT PARTIAL 93
question:
create: 'Create Question'
update: 'Update Question'
Then render the partial fromeach 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 applica-
tion and replace them with the newly extracted partial.
Extract Validator
A form of Extract Class used to remove complex validation details from
ActiveRecord models. This technique also prevents duplication of validation
code across several les.
Uses
Keep validation implementation details out of models.
Encapsulate validation details into a single le.
Remove duplication among classes performing the same validation logic.
Example
The Invitation class has validation details in-line. It checks that the
repient_email matches the formatting of the regular expression EMAIL_REGEX.
# app/models/invitation.rb
class Invitation < ActiveRecord::Base
EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
validates :recipient_email, presence: true, format: EMAIL_REGEX
end
We extract the validation details into a new class EmailValidator, and place the
new class into the app/validators directory.
94
CHAPTER 22. EXTRACT VALIDATOR 95
# app/validators/email_validator.rb
class EmailValidator < ActiveModel::EachValidator
EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
def validate_each(record, attribute, value)
unless value.match EMAIL_REGEX
record.errors.add(attribute, "#{value} is not a valid email")
end
end
end
Once the validator has been extracted. Rails has a convention for using the new
validation class. EmailValidator is used by setting email: true in the validation
arguments.
# app/models/invitation.rb
class Invitation < ActiveRecord::Base
validates :recipient_email, presence: true, email: true
end
The convention is to use the validation class name (in lowercase, and removing
Validator from the name). For exmaple, if we were validating an attribute with
ZipCodeValidator wed set zip_code: true as an argument to the validation call.
When validating an array of data as we do in SurveyInviter, we use the
EnumerableValidator to loop over the contents of an array.
# app/models/survey_inviter.rb
validates_with EnumerableValidator,
attributes: [:recipients],
unless: 'recipients.nil?',
validator: EmailValidator
CHAPTER 22. EXTRACT VALIDATOR 96
The EmailValidator is passed in as an argument, and each element in the array
is validated against it.
# app/validators/enumerable_validator.rb
class EnumerableValidator < ActiveModel::EachValidator
def validate_each(record, attribute, enumerable)
enumerable.each do |value|
validator.validate_each(record, attribute, value)
end
end
private
def validator
options[:validator].new(validator_options)
end
def validator_options
options.except(:validator).merge(attributes: attributes)
end
end
Next Steps
Verify the extracted validator does not have any Long Methods.
Check for other models that could use the validator.
Introduce Explaining Variable
This refactoring allows you to break up a complex, hard-to-read statement by
placing part of it in a local variable. The only dicult part is nding a good name
for the variable.
Uses
Improves legibility of code.
Makes it easier to Extract Methods by breaking up long statements.
Removes the need for extra Comments.
Example
This line of code was hard enough to understand that a comment was added:
# app/models/open_question.rb
def summary
# Text for each answer in order as a comma-separated string
answers.order(:created_at).pluck(:text).join(', ')
end
Adding an explaining variable makes the line easy to understand without a com-
ment:
97
CHAPTER 23. INTRODUCE EXPLAINING VARIABLE 98
# app/models/open_question.rb
def summary
text_from_ordered_answers = answers.order(:created_at).pluck(:text)
text_from_ordered_answers.join(', ')
end
You can follow up by using Replace Temp with Query.
def summary
text_from_ordered_answers.join(', ')
end
private
def text_from_ordered_answers
answers.order(:created_at).pluck(:text)
end
This increases the overall size of the class and moves text_from_ordered_answers
further away from summary, so youll want to be careful when doing this. The
most obvious reason to extract a method is to reuse the value of the variable.
However, theres another potential benet: it changes the way developers read
the code. Developers instinctively read code top-down. Expressions based on
variables place the details rst, which means that a developer will start with the
details:
text_from_ordered_answers = answers.order(:created_at).pluck(:text)
And work their way down to the overall goal of a method:
text_from_ordered_answers.join(', ')
Note that you naturally focus rst on the code necessary to nd the array of
texts, and then progress to see what happens to those texts.
Once a method is extracted, the high level concept comes rst:
CHAPTER 23. INTRODUCE EXPLAINING VARIABLE 99
def summary
text_from_ordered_answers.join(', ')
end
And then you progress to the details:
def text_from_ordered_answers
answers.order(:created_at).pluck(:text)
end
You can use this technique of extracting methods to make sure that developers
focus on whats important rst, and only dive into the implementation details
when necessary.
Next Steps
Replace Temp with Query if you want to reuse the expression or revert
the naturally order in which a developer reads the method.
Check the aected expression to make sure that its easy to read. If its
still too dense, try extracting more variables or methods.
Check the extracted variable or method for Feature Envy.
Introduce Form Object
A specialized type of Extract Class used to remove business logic from con-
trollers when processing data outside of an ActiveRecord model.
Uses
Keep business logic out of Controllers and Views.
Add validation support to plain old Ruby objects.
Display form validation errors using Rails conventions.
Set the stage for Extract Validator.
Example
The create action of our InvitationsController relies on user submitted data for
message and recipients (a comma delimited list of email addresses).
It performs a number of tasks:
Finds the current survey.
Validates the message is present.
Validates each of the recipients are email addresses.
Creates an invitation for each of the recipients.
Sends an email to each of the recipients.
Sets view data for validation failures.
100
CHAPTER 24. INTRODUCE FORM OBJECT 101
# app/controllers/invitations_controller.rb
class InvitationsController < ApplicationController
EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
def new
@survey = Survey.find(params[:survey_id])
end
def create
@survey = Survey.find(params[:survey_id])
if valid_recipients? && valid_message?
recipient_list.each do |email|
invitation = Invitation.create(
survey: @survey,
sender: current_user,
recipient_email: email,
status: 'pending'
)
Mailer.invitation_notification(invitation, message)
end
redirect_to survey_path(@survey), notice: 'Invitation successfully sent'
else
@recipients = recipients
@message = message
render 'new'
end
end
private
def valid_recipients?
invalid_recipients.empty?
end
def valid_message?
message.present?
end
CHAPTER 24. INTRODUCE FORM OBJECT 102
def invalid_recipients
@invalid_recipients ||= recipient_list.map do |item|
unless item.match(EMAIL_REGEX)
item
end
end.compact
end
def recipient_list
@recipient_list ||= recipients.gsub(/\s+/, '').split(/[\n,;]+/)
end
def recipients
params[:invitation][:recipients]
end
def message
params[:invitation][:message]
end
end
By introducing a form object we can move the concerns of data validation, in-
vitation creation, and notications to the new model SurveyInviter.
Including ActiveModel::Model allows us to leverage the familiar Active Record
Validation syntax.
CHAPTER 24. INTRODUCE FORM OBJECT 103
As we introduce the form object well also extract an enumerable class
RecipientList and validators EnumerableValidator and EmailValidator. They will
be covered in the chapters Extract Class and Extract Validator.
# app/models/survey_inviter.rb
class SurveyInviter
include ActiveModel::Model
attr_accessor :recipients, :message, :sender, :survey
validates :message, presence: true
validates :recipients, length: { minimum: 1 }
validates :sender, presence: true
validates :survey, presence: true
validates_with EnumerableValidator,
attributes: [:recipients],
unless: 'recipients.nil?',
validator: EmailValidator
def recipients=(recipients)
@recipients = RecipientList.new(recipients)
end
def invite
if valid?
deliver_invitations
end
end
private
def create_invitations
recipients.map do |recipient_email|
Invitation.create!(
survey: survey,
sender: sender,
recipient_email: recipient_email,
status: 'pending'
CHAPTER 24. INTRODUCE FORM OBJECT 104
)
end
end
def deliver_invitations
create_invitations.each do |invitation|
Mailer.invitation_notification(invitation, message).deliver
end
end
end
Moving business logic into the new form object dramatically reduces the size
and complexity of the InvitationsController. The controller is now focused on
the interaction between the user and the models.
# app/controllers/invitations_controller.rb
class InvitationsController < ApplicationController
def new
@survey = Survey.find(params[:survey_id])
@survey_inviter = SurveyInviter.new
end
def create
@survey = Survey.find(params[:survey_id])
@survey_inviter = SurveyInviter.new(survey_inviter_params)
if @survey_inviter.invite
redirect_to survey_path(@survey), notice: 'Invitation successfully sent'
else
render 'new'
end
end
private
def survey_inviter_params
params.require(:survey_inviter).permit(
CHAPTER 24. INTRODUCE FORM OBJECT 105
:message,
:recipients
).merge(
sender: current_user,
survey: @survey
)
end
end
Next Steps
Check that the controller no longer has Long Methods.
Verify the new form object is not a Large Class.
Check for places to re-use any new validators if Extract Validator was
used during the refactoring.
Introduce Parameter Object
A technique to reduce the number of input parameters to a method.
To introduce a parameter object:
Pick a name for the object that represents the grouped parameters.
Replace methods grouped parameters with the object.
Uses
Remove Long Parameter Lists.
Group parameters that naturally t together.
Encapsulate behavior between related parameters.
106
CHAPTER 25. INTRODUCE PARAMETER OBJECT 107
Example
Lets take a look at the example from Long Parameter List and improve it by
grouping the related parameters into an object:
# app/mailers/mailer.rb
class Mailer < ActionMailer::Base
default from: "from@example.com"
def completion_notification(first_name, last_name, email)
@first_name = first_name
@last_name = last_name
mail(
to: email,
subject: 'Thank you for completing the survey'
)
end
end
# app/views/mailer/completion_notification.html.erb
<%= @first_name %> <%= @last_name %>
CHAPTER 25. INTRODUCE PARAMETER OBJECT 108
By introducing the new parameter object recipient we can naturally group the
attributes first_name, last_name, and email together.
# app/mailers/mailer.rb
class Mailer < ActionMailer::Base
default from: "from@example.com"
def completion_notification(recipient)
@recipient = recipient
mail(
to: recipient.email,
subject: 'Thank you for completing the survey'
)
end
end
This also gives us the opportunity to create a new method full_name on the
recipient object to encapsulate behavior between the first_name and last_name.
# app/views/mailer/completion_notification.html.erb
<%= @recipient.full_name %>
Next Steps
Check to see if the same Data Clump exists elsewhere in the application,
and reuse the Parameter Object to group them together.
Verify the methods using the Parameter Object dont have Feature Envy.
Introduce Facade
STUB
109
Use class as Factory
An Abstract Factory is an object that knows how to build something, such as
one of several possible strategies for summarizing answers to questions on a
survey. An object that holds a reference to an abstract factory doesnt need to
knowwhat class is going to be used; it trusts the factory to return an object that
responds to the required interface.
Because classes are objects in Ruby, every class can act as an Abstract Factory.
Using a class as a factory allows us to remove most explicit factory objects.
Uses
Removes Duplicated Code and Shotgun Surgery by cutting out crufty
factory classes.
Combines with Convention Over Conguration to eliminate Shotgun
Surgery and Case Statements.
110
CHAPTER 27. USE CLASS AS FACTORY 111
Example
This controller uses one of several possible summarizer strategies to generate
a summary of answers to the questions on a survey:
# app/controllers/summaries_controller.rb
class SummariesController < ApplicationController
def show
@survey = Survey.find(params[:survey_id])
@summaries = @survey.summarize(summarizer)
end
private
def summarizer
case params[:id]
when 'breakdown'
Breakdown.new
when 'most_recent'
MostRecent.new
when 'your_answers'
UserAnswer.new(current_user)
else
raise "Unknown summary type: #{params[:id]}"
end
end
end
The summarizer method is a Factory Method. It returns a summarizer object
based on params[:id].
CHAPTER 27. USE CLASS AS FACTORY 112
We can refactor that using the Abstract Factory pattern:
def summarizer
summarizer_factory.build
end
def summarizer_factory
case params[:id]
when 'breakdown'
BreakdownFactory.new
when 'most_recent'
MostRecentFactory.new
when 'your_answers'
UserAnswerFactory.new(current_user)
else
raise "Unknown summary type: #{params[:id]}"
end
end
Nowthe summarizer method asks the summarizer_factory method for an Abstract
Factory, and it asks the factory to build the actual summarizer instance.
However, this means we need to provide an Abstract Factory for each summa-
rizer strategy:
class BreakdownFactory
def build
Breakdown.new
end
end
class MostRecentFactory
def build
MostRecent.new
end
end
CHAPTER 27. USE CLASS AS FACTORY 113
class UserAnswerFactory
def initialize(user)
@user = user
end
def build
UserAnswer.new(@user)
end
end
These factory classes are repetitive and dont pull their weight. We can rip
two of these classes out by using the actual summarizer class as the factory
instance. First, lets rename the build method to new to follow the Ruby conven-
tion:
def summarizer
summarizer_factory.new
end
class BreakdownFactory
def new
Breakdown.new
end
end
class MostRecentFactory
def new
MostRecent.new
end
end
CHAPTER 27. USE CLASS AS FACTORY 114
class UserAnswerFactory
def initialize(user)
@user = user
end
def new
UserAnswer.new(@user)
end
end
Now an instance of BreakdownFactory acts exactly like the Breakdown class itself,
and the same is true of MostRecentFactory and MostRecent. Therefore, lets use
the classes themselves instead of instances of the factory classes:
def summarizer_factory
case params[:id]
when 'breakdown'
Breakdown
when 'most_recent'
MostRecent
when 'your_answers'
UserAnswerFactory.new(current_user)
else
raise "Unknown summary type: #{params[:id]}"
end
end
Now we can delete two of our factory classes.
Next Steps
Use Convention Over Conguration to remove manual mappings and
possibly remove more classes.
Move method
Moving methods is generally easy. Moving a method allows you to place a
method closer to the state it uses by moving it to the class which owns the
related state.
To move a method:
Move the entire method denition and body into the new class.
Change any parameters which are part of the state of the new class to
simply reference the instance variables or methods.
Introduce any necessary parameters because of state which belongs to
the old class.
Rename the method if the new name no longer makes sense in the new
context (for example, rename invite_user to invite once the method is
moved to the User class).
Replace calls to the old method to calls to the new method. This may
require introducing delegation or building an instance of the new class.
Uses
Remove Feature Envy by moving a method to the class where the envied
methods live.
Make private, parameterized methods easier to reuse by moving them
to public, unparameterized methods.
Improve readability by keeping methods close to the other methods they
use.
115
CHAPTER 28. MOVE METHOD 116
Lets take a look at an example method that suers from Feature Envy and use
Extract Method and Move Method to improve it:
# app/models/completion.rb
def score
answers.inject(0) do |result, answer|
question = answer.question
result + question.score(answer.text)
end
end
The block in this method suers from Feature Envy: it references answer more
than it references methods or instance variables from its own class. We cant
move the entire method; we only want to move the block, so lets rst extract a
method:
# app/models/completion.rb
def score
answers.inject(0) do |result, answer|
result + score_for_answer(answer)
end
end
# app/models/completion.rb
def score_for_answer(answer)
question = answer.question
question.score(answer.text)
end
The score method no longer suers from Feature Envy, and the new
score_for_answer method is easy to move, because it only references its own
state. See the chapter on Extract Method for details on the mechanics and
properties of this refactoring.
Now that the Feature Envy is isolated, lets resolve it by moving the method:
# app/models/completion.rb
def score
CHAPTER 28. MOVE METHOD 117
answers.inject(0) do |result, answer|
result + answer.score
end
end
# app/models/answer.rb
def score
question.score(text)
end
The newly extracted and moved Question#score method no longer suers from
Feature Envy. Its easier to reuse, because the logic is freed from the internal
block in Completion#score. Its also available to other classes, because its no
longer a private method. Both methods are also easier to follow, because the
methods they invoke are close to the methods they depend on.
Dangerous: move and extract at the same time
Its tempting to do everything as one change: create a new method in Answer,
move the code over from Completion, and change Completion#score to call the
new method. Although this frequently works without a hitch, with practice, you
can perform the two, smaller refactorings just as quickly as the single, larger
refactoring. By breaking the refactoring into two steps, you reduce the duration
of down time for your code; that is, you reduce the amount of time during
which something is broken. Improving code in tiny steps makes it easier to
debug when something goes wrong and prevents you from writing more code
than you need to. Because the code still works after each step, you can simply
stop whenever youre happy with the results.
Next Steps
Make sure the new method doesnt suer from Feature Envy because of
state it used from its original class. If it does, try splitting the method up
and moving part of it back.
Check the class of the new method to make sure its not a Large Class.
Inline class
As an application evolves, new classes are introduced as new features are
added and existing code is refactored. Extracting classes will help to keep ex-
isting classes maintainable and make it easier to add new features. However,
features can also be removed or simplied, and youll inevitably nd that some
classes just arent pulling their weight. Removing dead-weight classes is just
as important as splitting up large classes, and inlining a class is the easiest way
to remove it.
Inlining a class is straightforward:
For each consumer class that uses the inlined class, inline or move each
method from the inlined class into the consumer class.
Remove the inlined class.
Note that this refactoring is dicult (and unwise!) if you have more than one or
two consumer classes.
Uses
Make classes easier to understand by eliminating the number of meth-
ods, classes, and les developers need to look through.
Eliminate Shotgun Surgery from changes that cascade through useless
classes.
Eliminate Feature Envy when the envied class can be inlined into the
envious class.
118
CHAPTER 29. INLINE CLASS 119
Example
In our example application, users can create surveys and invite other users to
answer them. Users are invited by listing email addresses to invite.
Any email addresses that match up with existing users are sent using a private
message that the user will see the next time he or she signs in. Invitations to
unrecognized addresses are sent using email messages.
The Invitation model delegates to a dierent strategy class based on whether
or not its recipient email is recognized as an existing user:
# app/models/invitation.rb
def deliver
if recipient_user
MessageInviter.new(self, recipient_user).deliver
else
EmailInviter.new(self).deliver
end
end
Weve decided that the private messaging feature isnt getting enough use, so
were going to remove it. This means that all invitations will nowbe delivered via
email, so we can simplify Invitation#deliver to always use the same strategy:
# app/models/invitation.rb
def deliver
EmailInviter.new(self).deliver
end
The EmailInviter class was useful as a strategy, but now that the strategy no
longer varies, it doesnt bring much to the table:
# app/models/email_inviter.rb
class EmailInviter
def initialize(invitation)
@invitation = invitation
CHAPTER 29. INLINE CLASS 120
@body = InvitationMessage.new(@invitation).body
end
def deliver
Mailer.invitation_notification(@invitation, @body).deliver
end
end
It doesnt handle any concerns that arent already well-encapsulated by
InvitationMessage and Mailer, and its only used once (in Invitation). We
can inline this class into Invitation and drop a little overall complexity and
indirection from our application.
First, lets inline the EmailInviter#deliver method (and its dependent variables
from EmailInviter#initialize):
# app/models/invitation.rb
def deliver
body = InvitationMessage.new(self).body
Mailer.invitation_notification(self, body).deliver
end
Next, we can delete EmailInviter entirely.
After inlining the class, it requires fewer jumps through methods, classes, and
les to understand how invitations are delivered. Additionally, the application
is less complex overall. Flog gives us a total complexity score of 424.7 after
this refactoring, down slightly from427.6. This isnt a huge gain, but this was an
easy refactoring, and continually deleting or inlining unnecessary classes and
methods will have larger long term eects.
Drawbacks
Attempting to inline a class with multiple consumers will likely introduce
duplicated code.
Inlining a class may create large classes and cause divergent change.
Inlining a class will usually increase per-class or per-method complexity,
even if it reduces total complexity.
CHAPTER 29. INLINE CLASS 121
Next Steps
Use Extract Method if any inlined methods introduced long methods.
Use Extract Class if the merged class is a large class or beings suering
from divergent change.
Inject dependencies
Injecting dependencies allows you to keep dependency resolutions close to
the logic that aects them. It can prevent sub-dependencies from leaking
throughout the code base, and it makes it easier to change the behavior of
related components without modifying those components classes.
Although many people think of dependency injection frameworks and XML
when they hear dependency injection, injecting a dependency is usually as
simple as passing it as a parameter.
Changing code to use dependency injection only takes a few steps:
1. Move the dependency decision to a higher level component.
2. Pass the dependency as a parameter to the lower level component.
3. Remove any sub-dependencies from the lower level component.
Injecting dependencies is the simplest way to invert control.
Uses
Eliminates Shotgun Surgery from leaking sub-dependencies.
Eliminates Divergent Change by allowing runtime composition patterns,
such as decorators and strategies.
122
CHAPTER 30. INJECT DEPENDENCIES 123
Example
In our example applications, users can view a summary of the answers to each
question on a survey. Users can select from one of several dierent summary
types to view. For example, they can see the most recent answer to each ques-
tion, or they can see a percentage breakdown of the answers to a multiple
choice question.
The controller passes in the name of the summarizer that the user selected:
# app/controllers/summaries_controller.rb
def show
@survey = Survey.find(params[:survey_id])
@summaries = @survey.summaries_using(summarizer, options)
end
private
def summarizer
params[:id]
end
Survey#summaries_using asks each of its questions for a summary using that sum-
marizer and the given options:
# app/models/survey.rb
question.summary_using(summarizer, options)
Question#summary_using instantiates the requested summarizer with the
requested options, and then asks the summarizer to summarize the question:
# app/models/question.rb
def summary_using(summarizer_name, options)
summarizer_factory = "Summarizer::#{summarizer_name.classify}".constantize
summarizer = summarizer_factory.new(options)
value = summarizer.summarize(self)
Summary.new(title, value)
end
CHAPTER 30. INJECT DEPENDENCIES 124
This is hard to follow and causes shotgun surgery because the logic of building
the summarizer is in Question, far away from the choice of which summarizer to
use, which is in SummariesController. Additionally, the options parameter needs
to be passed down several levels so that summarizer-specic options can be
provided when building the summarizer.
Lets switch this up by having the controller build the actual summarizer in-
stance. First, well move that logic from Question to SummariesController:
# app/controllers/summaries_controller.rb
def show
@survey = Survey.find(params[:survey_id])
@summaries = @survey.summaries_using(summarizer, options)
end
private
def summarizer
summarizer_name = params[:id]
summarizer_factory = "Summarizer::#{summarizer_name.classify}".constantize
summarizer_factory.new(options)
end
Then, well change Question#summary_using to take an instance instead of a
name:
# app/models/question.rb
def summary_using(summarizer, options)
value = summarizer.summarize(self)
Summary.new(title, value)
end
CHAPTER 30. INJECT DEPENDENCIES 125
That options argument is no longer necessary, because it was only used to build
the summarizer, which is now handled by the controller. Lets remove it:
# app/models/question.rb
def summary_using(summarizer)
value = summarizer.summarize(self)
Summary.new(title, value)
end
We also dont need to pass it from Survey:
# app/models/survey.rb
question.summary_using(summarizer)
This interaction has already improved, because the options argument is no
longer uselessly passed around through two models. Its only used in the con-
troller where the summarizer instance is built. Building the summarizer in the
controller is appropriate, because the controller knows the name of the sum-
marizer we want to build, as well as which options are used when building it.
Now that were using dependency injection, we can take this even further.
In order to prevent the summary from inuencing a users own answers, users
dont see summaries for questions they havent answered yet by default. Users
can click a link to override this decision and view the summary for every ques-
tion.
The information that determines whether or not to hide unanswered questions
lives in the controller:
# app/controllers/summaries_controller.rb
end
def constraints
if include_unanswered?
{}
else
{ answered_by: current_user }
end
end
CHAPTER 30. INJECT DEPENDENCIES 126
However, this information is passed into Survey#summaries_using:
# app/controllers/summaries_controller.rb
@summaries = @survey.summaries_using(summarizer, options)
Survey#summaries_using decides whether to hide the answer to each question
based on that setting:
# app/models/survey.rb
def summaries_using(summarizer, options = {})
questions.map do |question|
summary_or_hidden_answer(summarizer, question, options)
end
end
private
def summary_or_hidden_answer(summarizer, question, options)
if hide_unanswered_question?(question, options[:answered_by])
hide_answer_to_question(question)
else
question.summary_using(summarizer)
end
end
def hide_unanswered_question?(question, answered_by)
answered_by && !question.answered_by?(answered_by)
end
def hide_answer_to_question(question)
Summary.new(question.title, NO_ANSWER)
end
end
Again, the decision is far away from the dependent behavior.
We can combine our dependency injection with a decorator to remove the du-
plicate decision:
CHAPTER 30. INJECT DEPENDENCIES 127
# app/models/unanswered_question_hider.rb
class UnansweredQuestionHider
NO_ANSWER = "You haven't answered this question".freeze
def initialize(summarizer, user)
@summarizer = summarizer
@user = user
end
def summarize(question)
if hide_unanswered_question?(question)
NO_ANSWER
else
@summarizer.summarize(question)
end
end
private
def hide_unanswered_question?(question)
!question.answered_by?(@user)
end
end
Well decide whether or not to decorate the base summarizer in our controller:
# app/controllers/summaries_controller.rb
def decorated_summarizer
if include_unanswered?
summarizer
else
UnansweredQuestionHider.new(summarizer, current_user)
end
end
Now, the decision of whether or not to hide answers is completely removed
from Survey:
CHAPTER 30. INJECT DEPENDENCIES 128
# app/models/survey.rb
def summaries_using(summarizer)
questions.map do |question|
question.summary_using(summarizer)
end
end
For more explanation of using decorators, as well as step-by-step instructions
for how to introduce them, see the chapter on Extract Decorator.
Drawbacks
Injecting dependencies in our example made each class - SummariesController,
Survey, Question, and UnansweredQuestionHider - easier to understand as a unit.
However, its now dicult to understand why kind of summaries will be pro-
duced just by looking at Survey or Question. You need to follow the stack up
to SummariesController to understand the dependencies, and then look at each
class to understand how theyre used.
In this case, we believe that using dependency injection resulted in an overall
win for readability and exibility. However, its important to remember that the
further you move a dependencys resolution from its use, the harder it is to
gure out whats actually being used in lower level components.
In our example, there isnt an easy way to know which class will be instantiated
for the summarizer parameter to Question#summary_using:
# app/models/question.rb
def summary_using(summarizer)
value = summarizer.summarize(self)
Summary.new(title, value)
end
In our case, that will be one of Summarizer::Breakdown, Summarizer::MostRecent,
or Summarizer::UserAnswer, or a UnansweredQuestionHider that decorates one
of the above. Developers will need to trace back up through Survey to
SummariesController to gather all the possible implementations.
CHAPTER 30. INJECT DEPENDENCIES 129
Next Steps
When pulling dependency resolution up into a higher level class, check
that class to make sure it doesnt become a Large Class because of all
the logic surrounding dependency resolution.
If a class is suering fromDivergent Change because of newor modied
dependencies, try moving dependency resolution further up the stack to
a container class whose sole responsibility is managing dependencies.
If methods contain Long Parameter Lists, consider wrapping up several
dependencies in a Parameter Object or Fascade.
Replace Subclasses with
Strategies
Subclasses are a common method of achieving reuse and polymorphism, but
inheritance has its drawbacks. See Composition Over Inheritance for reasons
why you might decide to avoid an inheritance-based model.
During this refactoring, we will replace the subclasses with individual strategy
classes. Each strategy class will implement a common interface. The original
base class is promoted from an abstract class to the composition root, which
composes the strategy classes.
This allows for smaller interfaces, stricter separation of concerns, and easier
testing. It also makes it possible to swap out part of the structure, which would
require converting to a new type in an inheritance-based model.
When applying this refactoring to an ActiveRecord::Base subclass, STI is re-
moved, often in favor of a polymorphic association.
Uses
Eliminate Large Classes by splitting up a bloated base class.
Convert STI to a composition-based scheme.
Make it easier to change part of the structure by separating the parts that
change from the parts that dont.
130
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 131
Example
The switch_to method on Question changes the question to a new type. Any
necessary attributes for the new subclass are provided to the attributes
method.
# app/models/question.rb
def switch_to(type, new_attributes)
attributes = self.attributes.merge(new_attributes)
new_question = type.constantize.new(attributes.except('id', 'type'))
new_question.id = id
begin
Question.transaction do
destroy
new_question.save!
end
rescue ActiveRecord::RecordInvalid
end
new_question
end
Using inheritance makes changing question types awkward for a number of
reasons:
You cant actually change the class of an instance in Ruby, so you need
to return the instance of the new class.
The implementation requires deleting and creating records, but part of
the transaction (destroy) must execute before we can validate the new
instance. This results in control ow using exceptions.
Its hard to understand why this method is implemented the way it is, so
other developers xing bugs or refactoring in the future will have a hard
time navigating it.
We can make this operation easier by using composition instead of inheritance.
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 132
This is a dicult change that becomes larger as more behavior is added to the
inheritance tree. We can make the change easier by breaking it down into
smaller steps, ensuring that the application is in a fully-functional state with
passing tests after each change. This allows us to debug is smaller sessions
and create safe checkpoint commits that we can retreat to if something goes
wrong.
Use Extract Class to Extract Non-Railsy Methods From Subclasses
The easiest way to start is by extracting a strategy class fromeach subclass and
moving (and delegating) as many methods as you can to the newclass. Theres
some class-level wizardry that goes on in some Rails features like associations,
so lets start by moving simple, instance-level methods that arent part of the
framework.
Lets start with a simple subclass: OpenQuestion.
Heres the OpenQuestion class using an STI model:
# app/models/open_question.rb
class OpenQuestion < Question
def score(text)
0
end
def breakdown
text_from_ordered_answers = answers.order(:created_at).pluck(:text)
text_from_ordered_answers.join(', ')
end
end
We can start by creating a new strategy class:
class OpenSubmittable
end
When switching from inheritance to composition, you need to add a new word
to the applications vocabulary. Before, we had questions, and dierent sub-
classes of questions handled the variations in behavior and data. Now, were
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 133
switching to a model where theres only one question class, and question will
compose something that will handle the variations. In our case, that something
is a submittable. In our new model, each question is just a question, and ev-
ery question composes a submittable that decides how the question can be
submitted. Thus, our rst extracted class is called OpenSubmittable, extracted
from OpenQuestion.
Lets move our rst method over to OpenSubmittable:
# app/models/open_submittable.rb
class OpenSubmittable
def score(text)
0
end
end
And change OpenQuestion to delegate to it:
# app/models/open_question.rb
class OpenQuestion < Question
def score(text)
submittable.score(text)
end
def breakdown
text_from_ordered_answers = answers.order(:created_at).pluck(:text)
text_from_ordered_answers.join(', ')
end
def submittable
OpenSubmittable.new
end
end
Each question subclass implements the score method, so we repeat this pro-
cess for MultipleChoiceQuestion and ScaleQuestion. You can see the full change
for this step in the example app.
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 134
At this point, weve introduced a parallel inheritance hierarchy. During a longer
refactor, things may get worse before they get better. This is one of several
reasons that its always best to refactor on a branch, separately fromany feature
work. Well make sure that the parallel inheritance hierarchy is removed before
merging.
Pull Up Delegate Method Into Base Class
After the rst step, each subclass implements a submittable method to build its
parallel strategy class. The score method in each subclass simply delegates to
its submittable. We can now pull the score method up into the base Question
class, completely removing this concern from the subclasses.
First, we add a delegator to Question:
# app/models/question.rb
delegate :score, to: :submittable
Then, we remove the score method from each subclass.
You can see this change in full in the example app.
Move Remaining Common API Into Strategies
We can now repeat the rst two steps for every non-Railsy method that the
subclasses implement. In our case, this is just the breakdown method.
The most interesting part of this change is that the breakdown method requires
state from the subclasses, so the question is now provided to the submittable:
# app/models/multiple_choice_question.rb
def submittable
MultipleChoiceSubmittable.new(self)
end
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 135
# app/models/multiple_choice_submittable.rb
def answers
@question.answers
end
def options
@question.options
end
You can view this change in the example app.
Move Remaining Non-Railsy Public Methods Into Strategies
We can take a similar approach for the uncommon API; that is, public methods
that are only implemented in one subclass.
First, move the body of the method into the strategy:
# app/models/scale_submittable.rb
def steps
(@question.minimum..@question.maximum).to_a
end
Then, add a delegator. This time, the delegator can live directly on the subclass,
rather than the base class:
# app/models/scale_question.rb
def steps
submittable.steps
end
Repeat this step for the remaining public methods that arent part of the Rails
framework. You can see the full change for this step in our example app.
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 136
Remove Delegators From Subclasses
Our subclasses now contain only delegators, code to instantiate the submit-
table, and framework code. Eventually, we want to completely delete these
subclasses, so lets start stripping them down. The delegators are easiest to
delete, so lets take them on before the framework code.
First, nd where the delegators are used:
# app/views/multiple_choice_questions/_multiple_choice_question_form.html.erb
<%= form.fields_for(:options, question.options_for_form) do |option_fields| -%>
<%= option_fields.input :text, label: 'Option' %>
<% end -%>
And change the code to directly use the strategy instead:
# app/views/multiple_choice_questions/_multiple_choice_question_form.html.erb
<%= form.fields_for(:options, submittable.options_for_form) do |option_fields| -%>
<%= option_fields.input :text, label: 'Option' %>
<% end -%>
You may need to pass the strategy in where the subclass was used before:
# app/views/questions/_form.html.erb
<%= render(
"#{question.to_partial_path}_form",
submittable: question.submittable,
form: form
) %>
We can come back to these locations later and see if we need to pass in the
question at all.
After xing the code that uses the delegator, remove the delegator from the
subclass. Repeat this process for each delegator until theyve all been re-
moved.
You can see how we do this in the example app.
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 137
Instantiate Strategy Directly From Base Class
If you look carefully at the submittable method from each question subclass,
youll notice that it simply instantiates a class based on its own class name and
passes itself to the initialize method:
# app/models/open_question.rb
def submittable
OpenSubmittable.new(self)
end
This is a pretty strong convention, so lets apply some Convention Over Con-
guration and pull the method up into the base class:
# app/models/question.rb
def submittable
submittable_class_name = type.sub('Question', 'Submittable')
submittable_class_name.constantize.new(self)
end
We can then delete submittable from each of the subclasses.
At this point, the subclasses contain only Rails-specic code like associations
and validations.
You can see the full change in the example app.
Also, note that you may want to scope the constantize call in order to make the
strategies easy for developers to discover and close potential security vulner-
abilities.
A Fork In the Road
At this point, were faced with a dicult decision. At a glance, it seems as
though only associations and validations live in our subclasses, and we could
easily move those to our strategy. However, there are two major issues.
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 138
First, you cant move the association to a strategy class without making that
strategy an ActiveRecord::Base subclass. Associations are deeply coupled with
ActiveRecord::Base, and they simply wont work in other situations.
Also, one of our submittable strategies has state specic to that strategy. Scale
questions have a minimum and maximum. These elds are only used by scale
questions, but theyre on the questions table. We cant remove this pollution
without creating a table for scale questions.
There are two obvious ways to proceed:
Continue without making the strategies ActiveRecord::Base subclasses.
Keep the association for multiple choice questions and the minimumand
maximum for scale questions on the Question class, and use that data
from the strategy. This will result in Divergent Change and probably a
Large Class on Question, as every change in the data required for new or
existing strategies will require new behavior on Question.
Convert the strategies to ActiveRecord::Base subclasses. Move the as-
sociation and state specic to strategies to those classes. This involves
creating a table for each strategy and adding a polymorphic associa-
tion to Question. This will avoid polluting the Question class with future
strategy changes, but is awkward right now, because the tables for mul-
tiple choice questions and open questions would contain no data except
the primary key. These tables provide a placeholder for future strategy-
specic data, but those strategies may never require any more data and
until they do, the tables are a waste of queries and developer mental
space.
In this example, Imgoing to move forward with the second approach, because:
Its easier with ActiveRecord. ActiveRecord will take care of instantiating
the strategy in most situations if its an association, and it has special
behavior for associations using nested attribute forms.
Its the easiest way to avoid Divergent Change and Large Classes in a
Rails application. Both of these smells can cause problems that are hard
to x if you wait too long.
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 139
Convert Strategies to ActiveRecord subclasses
Continuing with our refactor, well change each of our strategy classes to inherit
from ActiveRecord::Base.
First, simply declare that the class is a child of ActiveRecord::Base:
# app/models/open_submittable.rb
class OpenSubmittable < ActiveRecord::Base
Your tests will complain that the corresponding table doesnt exist, so create it:
# db/migrate/20130131205432_create_open_submittables.rb
class CreateOpenSubmittables < ActiveRecord::Migration
def change
create_table :open_submittables do |table|
table.timestamps null: false
end
end
end
Our strategies currently accept the question as a parameter to initialize and
assign it as an instance variable. In an ActiveRecord::Base subclass, we dont
control initialize, so lets change question from an instance variable to an as-
sociation and pass a hash:
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 140
# app/models/open_submittable.rb
class OpenSubmittable < ActiveRecord::Base
has_one :question, as: :submittable
def breakdown
text_from_ordered_answers = answers.order(:created_at).pluck(:text)
text_from_ordered_answers.join(', ')
end
def score(text)
0
end
private
def answers
question.answers
end
end
# app/models/question.rb
def submittable
submittable_class = type.sub('Question', 'Submittable').constantize
submittable_class.new(question: self)
end
Our strategies are nowready to use Rails-specic functionality like associations
and validations.
View the full change on GitHub.
Introduce A Polymorphic Association
Now that our strategies are persistable using ActiveRecord, we can use them
in a polymorphic association. Lets add the association:
# app/models/question.rb
belongs_to :submittable, polymorphic: true
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 141
And add the necessary columns:
# db/migrate/20130131203344_add_submittable_type_and_id_to_questions.rb
class AddSubmittableTypeAndIdToQuestions < ActiveRecord::Migration
def change
add_column :questions, :submittable_id, :integer
add_column :questions, :submittable_type, :string
end
end
Were currently dening a submittable method that overrides the association.
Lets change that to a method that will build the association based on the STI
type:
# app/models/question.rb
def build_submittable
submittable_class = type.sub('Question', 'Submittable').constantize
self.submittable = submittable_class.new(question: self)
end
Previously, the submittable method built the submittable on demand, but now
its persisted in an association and built explicitly. Lets change our controllers
accordingly:
# app/controllers/questions_controller.rb
def build_question
@question = type.constantize.new(question_params)
@question.build_submittable
@question.survey = @survey
end
View the full change on GitHub.
Pass Attributes to Strategies
Were persisting the strategy as an association, but the strategies currently
dont have any state. We need to change that, since scale submittables need
a minimum and maximum.
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 142
Lets change our build_submittable method to accept attributes:
# app/models/question.rb
def build_submittable(attributes)
submittable_class = type.sub('Question', 'Submittable').constantize
self.submittable = submittable_class.new(attributes.merge(question: self))
end
We can quickly change the invocations to pass an empty hash, and were back
to green.
Next, lets move the minimum and maximum elds over to the scale_submittables
table:
# db/migrate/20130131211856_move_scale_question_state_to_scale_submittable.rb
add_column :scale_submittables, :minimum, :integer
add_column :scale_submittables, :maximum, :integer
Note that this migration is rather lengthy, because we also need to move over
the minimum and maximum values for existing questions. The SQL in our ex-
ample app will work on most databases, but is cumbersome. If youre using
Postgresql, you can handle the down method easier using an UPDATE FROM state-
ment.
Next, well move validations for these attributes over from ScaleQuestion:
# app/models/scale_submittable.rb
validates :maximum, presence: true
validates :minimum, presence: true
And change ScaleSubmittable methods to use those attributes directly, rather
than looking for them on question:
# app/models/scale_submittable.rb
def steps
(minimum..maximum).to_a
end
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 143
We can pass those attributes in our formby using fields_for and accepts_nested_attributes_for:
# app/views/scale_questions/_scale_question_form.html.erb
<%= form.fields_for :submittable do |submittable_fields| -%>
<%= submittable_fields.input :minimum %>
<%= submittable_fields.input :maximum %>
<% end -%>
# app/models/question.rb
accepts_nested_attributes_for :submittable
In order to make sure the Question fails when its submittable is invalid, we can
cascade the validation:
# app/models/question.rb
validates :submittable, associated: true
Now we just need our controllers to pass the appropriate submittable params:
# app/controllers/questions_controller.rb
def build_question
@question = type.constantize.new(question_params)
@question.build_submittable(submittable_params)
@question.survey = @survey
end
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 144
# app/controllers/questions_controller.rb
def question_params
params.
require(:question).
permit(:title, :options_attributes)
end
def submittable_params
if submittable_attributes = params[:question][:submittable_attributes]
submittable_attributes.permit(:minimum, :maximum)
else
{}
end
end
All behavior and state is now moved from ScaleQuestion to ScaleSubmittable,
and the ScaleQuestion class is completely empty.
You can view the full change in the example app.
Move Remaining Railsy Behavior Out of Subclasses
We can now repeat this process for remaining Rails-specic behavior. In our
case, this is the logic to handle the options association for multiple choice ques-
tions.
We can move the association and behavior over to the strategy class:
# app/models/multiple_choice_submittable.rb
has_many :options, foreign_key: :question_id
has_one :question, as: :submittable
accepts_nested_attributes_for :options, reject_if: :all_blank
Again, we remove the options method which delegated to question and rely on
options being directly available. Then we update the form to use fields_for
and move the allowed attributes in the controller from question to submittable.
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 145
At this point, every question subclass is completely empty.
You can view the full change in the example app.
Backll Strategies For Existing Records
Now that everything is moved over to the strategies, we need to make sure
that submittables exist for every existing question. We can write a quick backll
migration to take care of that:
# db/migrate/20130207164259_backfill_submittables.rb
class BackfillSubmittables < ActiveRecord::Migration
def up
backfill 'open'
backfill 'multiple_choice'
end
def down
connection.delete 'DELETE FROM open_submittables'
connection.delete 'DELETE FROM multiple_choice_submittables'
end
private
def backfill(type)
say_with_time "Backfilling #{type} submittables" do
connection.update(<<-SQL)
UPDATE questions
SET
submittable_id = id,
submittable_type = '#{type.camelize}Submittable'
WHERE type = '#{type.camelize}Question'
SQL
connection.insert(<<-SQL)
INSERT INTO #{type}_submittables
(id, created_at, updated_at)
SELECT
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 146
id, created_at, updated_at
FROM questions
WHERE questions.type = '#{type.camelize}Question'
SQL
end
end
end
We dont port over scale questions, because we took care of themin a previous
migration.
Pass the Type When Instantiating the Strategy
At this point, the subclasses are just dead weight. However, we cant delete
them just yet. Were relying on the type column to decide what type of strategy
to build, and Rails will complain if we have a type column without corresponding
subclasses.
Lets remove our dependence on that type column. Accept a type when building
the submittable:
# app/models/question.rb
def build_submittable(type, attributes)
submittable_class = type.sub('Question', 'Submittable').constantize
self.submittable = submittable_class.new(attributes.merge(question: self))
end
And pass it in when calling:
# app/controllers/questions_controller.rb
@question.build_submittable(type, submittable_params)
Full Change
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 147
Always Instantiate the Base Class
Nowwe can remove our dependence on the STI subclasses by always building
an instance of Question.
In our controller, we change this line:
# app/controllers/questions_controller.rb
@question = type.constantize.new(question_params)
To this:
# app/controllers/questions_controller.rb
@question = Question.new(question_params)
Were still relying on type as a parameter in forms and links to decide what type
of submittable to build. Lets change that to submittable_type, which is already
available because of our polymorphic association:
# app/controllers/questions_controller.rb
params[:question][:submittable_type]
# app/views/questions/_form.html.erb
<%= form.hidden_field :submittable_type %>
Well also need to revisit views that rely on polymorphic partials based on the
question type and change them to rely on the submittable type instead:
# app/views/surveys/show.html.erb
<%= render(
question.submittable,
submission_fields: submission_fields
) %>
Now we can nally remove our type column entirely:
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 148
# db/migrate/20130207214017_remove_questions_type.rb
class RemoveQuestionsType < ActiveRecord::Migration
def up
remove_column :questions, :type
end
def down
add_column :questions, :type, :string
connection.update(<<-SQL)
UPDATE questions
SET type = REPLACE(submittable_type, 'Submittable', 'Question')
SQL
change_column_null :questions, :type, true
end
end
Full Change
Remove Subclasses
Nowfor a quick, glorious change: those Question subclasses are entirely empty
and unused, so we can delete them.
This also removes the parallel inheritance hierarchy that we introduced earlier.
At this point, the code is as good as we found it.
Simplify Type Switching
If you were previously switching from one subclass to another as we did to
change question types, you can now greatly simplify that code.
Instead of deleting the old question and cloning it with a merged set of old
generic attributes and new specic attributes, you can simply swap in a new
strategy for the old one.
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 149
# app/models/question.rb
def switch_to(type, attributes)
old_submittable = submittable
build_submittable type, attributes
transaction do
if save
old_submittable.destroy
end
end
end
Our new switch_to method is greatly improved:
This method no longer needs to return anything, because theres no
need to clone. This is nice because switch_to is now simply a command
method (it does something) rather than a mixed command and query
method (it does something and returns something).
The method no longer needs to delete the old question, and the new
submittable is valid before we delete the old one. This means we no
longer need to use exceptions for control ow.
Its simpler and its code is obvious, so other developers will have no
trouble refactoring or xing bugs.
You can see the full change that resulted in our new method in the example
app.
Conclusion
Our new, composition-based model is improved in a number of ways:
Its easy to change types.
Each submittable is easy to use independently of its question, reducing
coupling.
Theres a clear boundary in the API for questions and submittables, mak-
ing it easier to test and making it less likely that concerns leak between
the two.
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 150
Shared behavior happens via composition, making it less likely that the
base class becomes a large class.
Its easy to add new state without eecting other types, because
strategy-specic state is stored on a table for that strategy.
You can view the entire refactor will all steps combined in the example app to
get an idea of what changed at the macro level.
This is a dicult transition to make, and the more behavior and data that you
shove into an inheritance scheme, the harder it becomes. In situations where
STI is not signicantly easier than using a polymorphic relationship, its better
to start with composition. STI provides few advantages over composition, and
its easier to merge models than to split them.
Drawbacks
Our application also got worse in a number of ways:
We introduced a new word into the application vocabulary. This can
increase understanding of a complex system, but vocabulary overload
makes simpler systems unnecessarily hard to learn.
We now need two queries to get a questions full state, and well need
to query up to four tables to get information about a set of questions.
We introduced useless tables for two of our question types. This will
happen whenever you use ActiveRecord to back a strategy without state.
We increased the overall complexity of the system. In this case, it may
have been worth it, because we reduced the complexity per component.
However, its worth keeping an eye on.
Before performing a large change like this, try to imagine what will be easy to
change in the new world thats hard right now.
After performing a large change, keep track of dicult changes you make.
Would they have been easier in the old world?
Answering this questions will increase your ability to judge whether or not to
use composition or inheritance in future situations.
CHAPTER 31. REPLACE SUBCLASSES WITH STRATEGIES 151
Next Steps
Check the extracted strategy classes to make sure they dont have Fea-
ture Envy related to the original base class. You may want to use Move
Method to move methods between strategies and the root class.
Check the extracted strategy classes for Duplicated Code introduced
while splitting up the base class. Use Extract Method or Extract Class to
extract common behavior.
Replace mixin with
composition
Mixins are one of two mechanisms for inheritance in Ruby. This refactoring
provides safe steps for cleanly removing mixins that have become troublesome.
Removing a mixin in favor of composition involves the following steps:
Extract a class for the mixin.
Compose and delegate to the extracted class from each mixed in
method.
Replace references to mixed in methods with references to the com-
posed class.
Remove the mixin.
Uses
Liberate business logic trapped in mixins.
Eliminate name clashes from multiple mixins.
Make methods in the mixins easier test.
Example
In our example applications, invitations can be delivered either by email or pri-
vate message (to existing users). Each invitation method is implemented in its
own class:
152
CHAPTER 32. REPLACE MIXIN WITH COMPOSITION 153
# app/models/message_inviter.rb
class MessageInviter < AbstractController::Base
include Inviter
def initialize(invitation, recipient)
@invitation = invitation
@recipient = recipient
end
def deliver
Message.create!(
recipient: @recipient,
sender: @invitation.sender,
body: render_message_body
)
end
end
# app/models/email_inviter.rb
class EmailInviter < AbstractController::Base
include Inviter
def initialize(invitation)
@invitation = invitation
end
def deliver
Mailer.invitation_notification(@invitation, render_message_body).deliver
end
end
The logic to generate the invitation message is the same regardless of the de-
livery mechanism, so this behavior has been extracted.
Its currently extracted using a mixin:
# app/models/inviter.rb
module Inviter
CHAPTER 32. REPLACE MIXIN WITH COMPOSITION 154
extend ActiveSupport::Concern
included do
include AbstractController::Rendering
include Rails.application.routes.url_helpers
self.view_paths = 'app/views'
self.default_url_options = ActionMailer::Base.default_url_options
end
private
def render_message_body
render template: 'invitations/message'
end
end
Lets replace this mixin with composition.
CHAPTER 32. REPLACE MIXIN WITH COMPOSITION 155
First, well extract a new class for the mixin:
# app/models/invitation_message.rb
class InvitationMessage < AbstractController::Base
include AbstractController::Rendering
include Rails.application.routes.url_helpers
self.view_paths = 'app/views'
self.default_url_options = ActionMailer::Base.default_url_options
def initialize(invitation)
@invitation = invitation
end
def body
render template: 'invitations/message'
end
end
This class contains all the behavior the formerly resided in the mixin. In order
to keep everything working, well compose and delegate to the extracted class
from the mixin:
# app/models/inviter.rb
module Inviter
private
def render_message_body
InvitationMessage.new(@invitation).body
end
end
CHAPTER 32. REPLACE MIXIN WITH COMPOSITION 156
Next, we can replace references to the mixed in methods (render_message_body
in this case) with direct references to the composed class:
# app/models/message_inviter.rb
class MessageInviter
def initialize(invitation, recipient)
@invitation = invitation
@recipient = recipient
@body = InvitationMessage.new(@invitation).body
end
def deliver
Message.create!(
recipient: @recipient,
sender: @invitation.sender,
body: @body
)
end
end
# app/models/email_inviter.rb
class EmailInviter
def initialize(invitation)
@invitation = invitation
@body = InvitationMessage.new(@invitation).body
end
def deliver
Mailer.invitation_notification(@invitation, @body).deliver
end
end
In our case, there was only one method to move. If your mixin has multiple
methods, its best to move them one at a time.
Once every reference to a mixed in method is replaced, you can remove the
mixed in method. Once every mixed in method is removed, you can remove
the mixin entirely.
CHAPTER 32. REPLACE MIXIN WITH COMPOSITION 157
Next Steps
Inject Dependencies to invert control and allow the composing classes
to use dierent implementations for the composed class.
Check the composing class for Feature Envy of the extracted class. Tight
coupling is common between mixin methods and host methods, so you
may need to use move method a few times to get the balance right.
Replace Callback with Method
If your models are hard to use and change because their persistence logic is
coupled with business logic, one way to loosen things up is by replacing call-
backs.
Uses
Reduces coupling persistence logic with business logic.
Makes it easier to extract concerns from models.
Fixes bugs from accidentally triggered callbacks.
Fixes bugs from callbacks with side eects when transactions roll back.
Steps
Use Extract Method if the callback is an anonymous block.
Promote the callback method to a public method if its private.
Call the public method explicitly rather than relying on save and callbacks.
158
CHAPTER 33. REPLACE CALLBACK WITH METHOD 159
Example
# app/models/survey_inviter.rb
def deliver_invitations
recipients.map do |recipient_email|
Invitation.create!(
survey: survey,
sender: sender,
recipient_email: recipient_email,
status: 'pending',
message: @message
)
end
end
# app/models/invitation.rb
after_create :deliver
# app/models/invitation.rb
private
def deliver
Mailer.invitation_notification(self).deliver
end
In the above code, the SurveyInviter is simply creating Invitation records, and
the actual delivery of the invitation email is hidden behind Invitation.create!
via a callback.
If one of several invitations fails to save, the user will see a 500 page, but some
of the invitations will already have been saved and delivered. The user will be
unable to tell which invitations were sent.
Because delivery is coupled with persistence, theres no way to make sure that
all of the invitations are saved before starting to deliver emails.
Lets make the callback method public so that it can be called from
SurveyInviter:
CHAPTER 33. REPLACE CALLBACK WITH METHOD 160
# app/models/invitation.rb
def deliver
Mailer.invitation_notification(self).deliver
end
private
Then remove the after_create line to detach the method from persistence.
Now we can split invitations into separate persistence and delivery phases:
# app/models/survey_inviter.rb
def deliver_invitations
create_invitations.each(&:deliver)
end
def create_invitations
Invitation.transaction do
recipients.map do |recipient_email|
Invitation.create!(
survey: survey,
sender: sender,
recipient_email: recipient_email,
status: 'pending',
message: @message
)
end
end
end
If any of the invitations fail to save, the transaction will roll back. Nothing will be
committed, and no messages will be delivered.
Next Steps
Find other instances where the model is saved to make sure that the
extracted method doesnt need to be called.
Use convention over
conguration
Rubys metaprogramming allows us to avoid boilerplate code and duplication
by relying on conventions for class names, le names, and directory structure.
Although depending on class names can be constricting in some situations,
careful use of conventions will make your applications less tedious and more
bug-proof.
Uses
Eliminate Case Statements by nding classes by name.
Eliminate Shotgun Surgery by removing the need to register or congure
new strategies and services.
Remove Duplicated Code by removing manual associations from identi-
ers to class names.
161
CHAPTER 34. USE CONVENTION OVER CONFIGURATION 162
Example
This controller accepts an id parameter identifying which summarizer strategy
to use and renders a summary of the survey based on the chosen strategy:
# app/controllers/summaries_controller.rb
class SummariesController < ApplicationController
def show
@survey = Survey.find(params[:survey_id])
@summaries = @survey.summarize(summarizer)
end
private
def summarizer
case params[:id]
when 'breakdown'
Breakdown.new
when 'most_recent'
MostRecent.new
when 'your_answers'
UserAnswer.new(current_user)
else
raise "Unknown summary type: #{params[:id]}"
end
end
end
The controller is manually mapping a given strategy name to an object that
can perform the strategy with the given name. In most cases, a strategy name
directly maps to a class of the same name.
We can use the constantize method from Rails to retrieve a class by name:
params[:id].classify.constantize
This will nd the MostRecent class from the string "most_recent", and so on. This
means we can rely on a convention for our summarizer strategies: each named
CHAPTER 34. USE CONVENTION OVER CONFIGURATION 163
strategy will map to a class implementing that strategy. The controller can use
the class as an Abstract Factory and obtain a summarizer.
However, we cant immediately start using constantize in our example, because
theres one outlier case: the UserAnswer class is referenced using "your_answers"
instead of "user_answer", and UserAnswer takes dierent parameters than the
other two strategies.
Before refactoring the code to rely on our newconvention, lets refactor to obey
it. All our names should map directly to class names, and each class should
accept the same parameters:
# app/controllers/summaries_controller.rb
def summarizer
case params[:id]
when 'breakdown'
Breakdown.new(user: current_user)
when 'most_recent'
MostRecent.new(user: current_user)
when 'user_answer'
UserAnswer.new(user: current_user)
else
raise "Unknown summary type: #{params[:id]}"
end
end
CHAPTER 34. USE CONVENTION OVER CONFIGURATION 164
Now that we know we can instantiate any of the summarizer classes the same
way, lets extract a method for determining the summarizer class:
# app/controllers/summaries_controller.rb
def summarizer
summarizer_class.new(user: current_user)
end
def summarizer_class
case params[:id]
when 'breakdown'
Breakdown
when 'most_recent'
MostRecent
when 'user_answer'
UserAnswer
else
raise "Unknown summary type: #{params[:id]}"
end
end
Nowthe extracted class performs exactly the same logic as constantize, so lets
use it:
# app/controllers/summaries_controller.rb
def summarizer
summarizer_class.new(user: current_user)
end
def summarizer_class
params[:id].classify.constantize
end
Now well never need to change our controller when adding a new strategy;
we just add a new class following the naming convention.
CHAPTER 34. USE CONVENTION OVER CONFIGURATION 165
Scoping constantize
Our controller currently takes a string directly from user input (params) and in-
stantiates a class with that name.
There are two issues with this approach that should be xed:
Theres no list of available strategies, so a developer would need to per-
form a complicated search to nd the relevant classes.
Without a whitelist, a user can make the application instantiate any class
they want by hacking parameters. This can result in security vulnerabili-
ties.
We can solve both easily by altering our convention slightly: scope all the strat-
egy classes within a module.
We change our strategy factory method:
# app/controllers/summaries_controller.rb
def summarizer
summarizer_class.new(user: current_user)
end
def summarizer_class
params[:id].classify.constantize
end
To:
# app/controllers/summaries_controller.rb
def summarizer_class
"Summarizer::#{params[:id].classify}".constantize
end
With this convention in place, you can nd all strategies by just looking in the
Summarizer module. In a Rails application, this will be in a summarizer directory
by convention.
Users also wont be able to instantiate anything they want by abusing our
constantize, because only classes in the Summarizer module are available.
CHAPTER 34. USE CONVENTION OVER CONFIGURATION 166
Drawbacks
Weak Conventions
Conventions are most valuable when theyre completely consistent.
The convention is slightly forced in this case because UserAnswer needs dierent
parameters than the other two strategies. This means that we nowneed to add
no-op initializer methods to the other two classes:
# app/models/summarizer/breakdown.rb
class Summarizer::Breakdown
def initialize(options)
end
def summarize(question)
question.breakdown
end
end
This isnt a deal-breaker, but it makes the other classes a little noisier, and adds
the risk that a developer will waste time trying to remove the unused parameter.
Every compromise made weakens the convention, and having a weak conven-
tion is worse than having no convention. If you have to change the convention
for every class you add that follows it, try something else.
Class-Oriented Programming
Another drawback to this solution is that its entirely class-based, which means
you cant assemble strategies at run-time. This means that reuse requires in-
heritance.
Also, this class-based approach, while convenient when developing an appli-
cation, is more likely to cause frustration when writing a library. Forcing devel-
opers to pass a class name instead of an object limits the amount of runtime
information strategies can use. In our example, only a user was required. When
CHAPTER 34. USE CONVENTION OVER CONFIGURATION 167
you control both sides of the API, its ne to assume that this is safe. When writ-
ing a library that will interface with other developers applications, its better not
to rely on class names.
Part III
Principles
168
DRY
The DRYprinciple - short for dont repeat yourself - comes fromThe Pragmatic
Programmer.
The principle states:
..

Every piece of knowledge must have a single, unambiguous, au-


thoritative representation within a system.
Following this principle is one of the best ways to prevent bugs and move faster.
Every duplicated piece of knowledge is a bug waiting to happen. Many devel-
opment techniques are really just ways to prevent and eliminate duplication,
and many smells are just ways to detect existing duplication.
When knowledge is duplicated, changing it means making the same change in
several places. Leaving duplication introduces a risk that the various duplicate
implementations will slowly diverge, making them harder to merge and making
it more likely that a bug remains in one or more incarnations after being xed.
Duplication leads to frustration and paranoia. Rampant duplication is a common
reason that developers reach for a Grand Rewrite.
Duplicated Knowledge vs Duplicated Text
Its important to understand that this principle states that knowledge should not
be repeated; it does not state that text should never be repeated.
169
CHAPTER 35. DRY 170
For example, this sample does not violate the DRY principle, even though the
word save is repeated several times:
def sign_up
@user.save
@account.save
@subscription.save
end
However, this code contains duplicated knowledge that could be extracted:
def sign_up_free
@user.save
@account.save
@trial.save
end
def sign_up_paid
@user.save
@account.save
@subscription.save
end
Application
The following smells may point towards duplicated code and can be avoided
by following the DRY principle:
Shotgun Surgery caused by changing the same knowledge in several
places.
Long Paramter Lists caused by not encapsulating related properties.
Feature Envy caused by leaking internal knowledge of a class that can
be encapsulated and reused.
You can use these solutions to remove duplication and make knowledge easier
to reuse:
CHAPTER 35. DRY 171
Extract Classes to encapsulate knowledge, allowing it to be reused.
Extract Methods to reuse behavior within a class.
Extract Partials to remove duplication in views.
Extract Validators to encapsulate validations.
Replace Conditionals with Null Objects to encapsulate behavior related
to nothingness.
Replace Conditionals With Polymorphism to make it easy to reuse be-
havioral branches.
Replace Mixins With Composition to make it easy to combine compo-
nents in new ways.
Use Convention Over Conguration to infer knowledge, making it impos-
sible to duplicate.
Applying these techniques before duplication occurs will make it less likely that
duplication will occur. If you want to prevent duplication, make knowledge eas-
ier to reuse by keeping classes small and focused.
Related principles include the Law of Demeter and the Single Responsibility
Principle.
Single responsibility principle
The Single Responsibility Principle, often abbreviated as SRP, was introduced
by Uncle Bob Martin, and states:
..

A class should have only one reason to change.


Classes with fewer responsibilities are more likely to be reusable, easier to
understand, and faster to test. They are easy to change and require fewer
changes after being written.
Although this is a very simple principle at a glance, deciding whether or not
any two pieces of behavior introduce two reasons to change is dicult, and
obeying SRP rigidly can be frustrating.
Reasons to change
One of the challenges in identifying reasons to change is that you need to de-
cide what granularity to be concerned with.
In our example application, users can invite their friends to take surveys. When
an invitation is sent, we encapsulate that invitation in a basic ActiveRecord sub-
class:
172
CHAPTER 36. SINGLE RESPONSIBILITY PRINCIPLE 173
# app/models/invitation.rb
class Invitation < ActiveRecord::Base
EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
STATUSES = %w(pending accepted)
belongs_to :sender, class_name: 'User'
belongs_to :survey
before_create :set_token
validates :recipient_email, presence: true, format: EMAIL_REGEX
validates :status, inclusion: { in: STATUSES }
def to_param
token
end
def deliver
body = InvitationMessage.new(self).body
Mailer.invitation_notification(self, body).deliver
end
private
def set_token
self.token = SecureRandom.urlsafe_base64
end
end
Everything in this class has something to do with invitations. You could make
the blunt assessment that this class obeys SRP, because it will only change
when invitation-related functionality changes. However, looking more carefully
at how invitations are implemented, several other reasons to change can be
identied:
The format of invitation tokens changes.
A bug is identied in our validation of email addresses.
CHAPTER 36. SINGLE RESPONSIBILITY PRINCIPLE 174
We need to deliver invitations using some mechanism other than email.
Invitations need to be persisted in another way, such as in a NoSQL
database.
The API for ActiveRecord or ActiveSupport changes during an update.
The application switches to a new framework besides Rails.
That gives us half a dozen reasons this class might change, leading to the prob-
able conclusion that this class does not follow SRP. So, should this class be
refactored?
Stability
Not all reasons to change are created equal.
As a developer, you know which changes are likely from experience or just
common sense. For example, attributes and business rules for invitations are
likely to change, so we know that this class will change as invitations evolve in
the application.
Regular expressions are powerful but tricky beasts, so its likely that well have
to adjust our regular expression. It might be nice to encapsulate that some-
where else, such as in a custom validator.
It would be unwise to guess as to what delivery mechanisms may loom in the
distant future, but its not out of the realm of possibility that well need to send
messages using an internal private messaging system or another service like
Facebook or Twitter. Therefore, it may be worthwhile to use dependency in-
jection to remove the details of delivery from this model. This may also make
testing easier and make the class easier to understand as a unit, because it will
remove distracting details relating to email delivery.
NoSQL databases have their uses, but we have no reason to believe well ever
need to move these records into another type of database. ActiveRecord has
proven to be a safe and steady default choice, so its probably not worth the
eort to protect ourselves against that change.
Some of our business logic is expressed using APIs from libraries that could
change, such as validations and relationships. We could write our own adapter
CHAPTER 36. SINGLE RESPONSIBILITY PRINCIPLE 175
to protect ourselves from those changes, but the maintenance burden is un-
likely to be worth the benet, and it will make the code harder to understand,
as there will be unnecessary indirection between the model and the framework.
Lastly, we could protect our application against framework changes by pre-
venting any business logic from leaking into the framework classes, such as
controllers and ActiveRecord models. Again, this would add a thick layer of
indirection to protect against an unlikely change.
However, if youre trying out a new database, object-relational mapper, or
framework, it may be worth adding some increased protection. The rst time
you use a new database, youll be less sure of that decision. If you prevent any
business logic from mixing with the persistence logic, it will make it easier for
you to undo that decision and fall back to a familiar solution like ActiveRecord
in case the new database turns against you.
The less sure you are about a decision, the more you should isolate that deci-
sion from the rest of your application.
Cohesion
One of the primary goals of SRP is to promote cohesive classes. The more
closely related the methods and properties are to each other, the more cohe-
sive a class is.
Classes with high cohesion are easier to understand, because the pieces t
naturally together. Theyre also easier to change and reuse, because they wont
be coupled to any unexpected dependencies.
Following this principle will lead to high cohesion, but its important to focus on
the output of each change made to follow the principle. If you notice an extra
responsibility in a class, think about the benets of extracting that responsibility.
If you think noticeably higher cohesion will be the result, charge ahead. If you
think it will simply be a way to spend an afternoon, make a note of it and move
on.
CHAPTER 36. SINGLE RESPONSIBILITY PRINCIPLE 176
Responsibility Magnets
Every application develops a few black holes that like to suck up as much re-
sponsibility as possible, slowly turning into God Classes.
User is a common responsibility magnet. Generally, each application has a focal
point in its user interface that sucks up responsibility as well. Our example ap-
plications main feature allows users to answer questions on surveys, so Survey
is a natural junk drawer for behavior.
Its easy to get sucked into a responsibility magnet by falling prey to just-one-
more syndrome. Whenever youre about to add a new behavior to an existing
class, rst check the history of that class. If there are previous commits that
show developers attempting to pull functionality out of this class, chances are
good that its a responsibility over-eater. Dont feed the problem; add a new
class instead.
Tension with Tell, Dont Ask
Extracting reasons to change can make it harder to follow Tell, Dont Ask.
For example, consider a Purchase model that knows how to charge a user:
class Purchase
def charge
purchaser.charge_credit_card(total_amount)
end
end
This method follows Tell, Dont Ask, because we can simply tell any Purchase to
charge, without examining any state on the Purchase.
However, it violates the Single Responsibility Principle, because Purchase has
more than one reason to change. If the rules around charging credit cards
change or the rules for calculating purchase totals change, this class with have
to change.
You can more closely adhere to SRP by extracting a new class for the charge
method:
CHAPTER 36. SINGLE RESPONSIBILITY PRINCIPLE 177
class PurchaseProcessor
def initialize(purchase, purchaser)
@purchase = purchase
@purchaser = purchaser
end
def charge
@purchaser.charge_credit_card @purchase.total_amount
end
end
This class can encapsulate rules around charging credit cards and remain im-
mune to other changes, thus following SRP. However, it now violates Tell, Dont
Ask, because it must ask the @purchase for its total_amount in order to place the
charge.
These two principles are often at odds with each other, and you must make a
pragmatic decision about which direction works best for your own classes.
Drawbacks
There are a number of drawbacks to following this principle too rigidly:
As outlined above, following this principle may lead to violations of Tell,
Dont Ask.
This principle causes an increase in the number of classes, potentially
leading to shotgun surgery and vocabulary overload.
Classes that follow this principle may introduce additional indirection,
making it harder to understand high level behavior by looking at indi-
vidual classes.
Application
If you nd yourself ghting any of these smells, you may want to refactor to
follow the Single Responsibility Principle:
CHAPTER 36. SINGLE RESPONSIBILITY PRINCIPLE 178
Divergent Change doesnt exist in classes that follow this principle.
Classes following this principle are easy to reuse, reducing the likelihood
of Duplicated Code.
Large Classes almost certainly have more than one reason to change.
Following this principle eliminates most large classes.
Code containing these smells may need refactoring before they can follow this
principle:
Case Statements make this principle dicult to follow, as every case
statement introduces a new reason to change.
Long Methods make it harder to extract concerns, as behavior can only
be moved once its encapsulated in a small, cohesive method.
Mixins, Single-Table Inheritance, and inheritance in general make it
harder to follow this principle, as the boundary between parent and
child class responsibilities is always fuzzy.
These solutions may be useful on the path towards SRP:
Extract Classes to move responsibilities to their own class.
Extract Decorators to layer responsibilities onto existing classes without
burdening the class denition with that knowledge.
Extract Validators to prevent classes fromchanging when validation rules
change.
Extract Value Objects to prevent rules about a type like currency or
names from leaking into other business logic.
Extract Methods to make responsibilities easier to move.
Move Methods to place methods in a more cohesive environment.
Inject Dependencies to relieve classes of the burden of changing with
their dependencies.
Replace Mixins with Composition to make it easier to isolate concerns.
Replace Subclasses with Strategies to make variations usable without
their base logic.
Following Composition Over Inheritance and the Dependency Inversion Prinici-
ple may make this principle easier to follow, as those principles make it easier
CHAPTER 36. SINGLE RESPONSIBILITY PRINCIPLE 179
to extract responsibilities. Following this principle will make it easier to follow
the Open-Closed Priniciple but may introduce violations of Tell, Dont Ask.
Tell, Dont Ask
The Tell, Dont Ask principle advises developers to tell objects what you want
done, rather than querying objects and making decisions for them.
Consider the following example:
class Order
def charge(user)
if user.has_valid_credit_card?
user.charge(total)
else
false
end
end
end
This example doesnt follow Tell, Dont Ask. It rst asks the user if it has a valid
credit card, and then makes a decision based on the users state.
In order to follow Tell, Dont Ask, we can move this decision into User#charge:
180
CHAPTER 37. TELL, DONT ASK 181
class User
def charge(total)
if has_valid_credit_card?
payment_gateway.charge(credit_card, total)
true
else
false
end
end
end
Now Order#charge can just delegate to User#charge, passing its own relevant
state (total):
class Order
def charge(user)
user.charge(total)
end
end
Following this principle has a number of benets.
Encapsulation of Logic
Following Tell, Dont Ask encapsulates the conditions under which an operation
can be performed in one place. In the above example, User should know when
it makes sense to charge.
Encapsulation of State
Referencing another objects state directly couples two objects together based
on what they are, rather than just what they do. By following Tell, Dont Ask, you
encapsulate state within the object that uses it, exposing only the operations
that can be performed based on that state, and hiding the state itself within
private methods and instance variables.
CHAPTER 37. TELL, DONT ASK 182
Minimal Public Interface
In many cases, following Tell, Dont Ask will result in the smallest possible public
interface between classes. In the above example, has_valid_credit_card? can
now be made private, because it becomes an internal concern encapsulated
within User.
Public methods are a liability. Before they can be changed, moved, renamed, or
removed, you need to nd every consumer class and update themaccordingly.
Tension with MVC
This principle can be dicult to follow while also following MVC.
Consider a view that uses the above Order model:
<%= form_for @order do |form| %>
<% unless current_user.has_valid_credit_card? %>
<%= render 'credit_card/fields', form: form %>
<% end %>
<!-- Order Fields -->
<% end %>
The view doesnt display the credit card elds if the user already has a valid
credit card saved. The view needs to ask the user a question and then change
its behavior based on that question, violating Tell, Dont Ask.
You could obey Tell, Dont Ask by making the user know how to render the
credit card form:
<%= form_for @order do |form| %>
<%= current_user.render_credit_card_form %>
<!-- Order Fields -->
<% end %>
However, this violates MVC by including view logic in the User model. In this
case, its better to keep the model, view, and controller concerns separate and
step across the Tell, Dont Ask line.
CHAPTER 37. TELL, DONT ASK 183
When writing interactions between other models and support classes, though,
make sure to give commands whenever possible, and avoid deviations in be-
havior based on another classs state.
Application
These smells may be a sign that you should be following Tell, Dont Ask more:
Feature Envy is frequently a sign that a method or part of a method
should be extracted and moved to another class, reducing the number
of questions that method must ask of another object.
Shotgun Surgery may result from state and logic leaks. Consolidating
conditionals using Tell, Dont Ask may reduce the number of changes
required for new functionality.
If you nd classes with these smells, they may require refactoring before you
can follow Tell, Dont Ask:
Case Statements that inect on methods from another object generally
get in the way.
Mixins blur the lines between responsibilities, as mixed in methods op-
erate on the state of the objects theyre mixed into.
If youre trying to refactor classes to follow Tell, Dont Ask, these solutions may
be useful:
Extract Method to encapsulate multiple conditions into one.
Move Method to move methods closer to the state they operate on.
Inline Class to remove unnecessary questions between two classes with
highly cohesive behavior.
Relace Conditionals with Polymorphism to reduce the number of ques-
tions being asked around a particular operation.
Replace Conditionals with Null Object to remove checks for nil.
CHAPTER 37. TELL, DONT ASK 184
Many Law of Demeter violations point towards violations of Tell, Dont Ask. Fol-
lowing Tell, Dont Ask may lead to violations of the Single Responsibility Prin-
ciple and the Open/Closed Principle, as moving operations onto the best class
may require modifying an existing class and adding a new responsibility.
Law of Demeter
The Lawof Demeter was developed at Northeastern University. Its named after
the Demeter Project, which is itself named after Demeter, the Greek goddess
of the harvest. There is widespread disagreement as to its pronunciation, but
the correct pronunciation emphasizes the second syllable; you can trust us on
that.
This principle states that:
..

A method of an object should invoke only the methods of the fol-


lowing kinds of objects:
1. itself
2. its parameters
3. any objects it creates/instantiates
4. its direct component objects
Like many principles, the Lawof Demeter is an attempt to help developers man-
age dependencies. The law restricts how deeply a method can reach into an-
other objects dependency graph, preventing any one method from becoming
tightly coupled to another objects structure.
Multiple Dots
The most obvious violation of the Law of Demeter is multiple dots, meaning a
chain of methods being invoked on each others return values.
185
CHAPTER 38. LAW OF DEMETER 186
Example:
class User
def discounted_plan_price(discount_code)
coupon = Coupon.new(discount_code)
coupon.discount(account.plan.price)
end
end
The call to account.plan.price above violates the Law of Demeter by invoking
price on the return value of plan. The price method is not a method on User, its
parameter discount_code, its instantiated object coupon, or its direct component
account.
The quickest way to avoid violations of this nature is to delegate the method:
class User
def discounted_plan_price(discount_code)
account.discounted_plan_price(discount_code)
end
end
class Account
def discounted_plan_price(discount_code)
coupon = Coupon.new(discount_code)
coupon.discount(plan.price)
end
end
In a Rails application, you can quickly delegate methods using ActiveSupports
delegate class method:
class User
delegate :discounted_plan_price, to: :account
end
If you nd yourself writing lots of delegators, consider changing the consumer
class to take a dierent object. For example, if you need to delegate lots of User
CHAPTER 38. LAW OF DEMETER 187
methods to Account, its possible that the code referencing User should actually
reference an instance of Account instead.
Multiple Assignments
Law of Demeter violations are often hidden behind multiple assignments.
class User
def discounted_plan_price(discount_code)
coupon = Coupon.new(discount_code)
plan = account.plan
coupon.discount(plan.price)
end
end
The above discounted_plan_price method no longer has multiple dots on one
line, but it still violates the Law of Demeter, because plan isnt a parameter,
instantiated object, or direct subcomponent.
The Spirit of the Law
Although the letter of the Law of Demeter is rigid, the message is broader. The
goal is to avoid over-entangling a method with another objects dependencies.
This means that xing a violation shouldnt be your objective; removing the
problem that caused the violation is a better idea. Here are a few tips to avoid
misguided xes to Law of Demeter violations:
Many delegate methods to the same object are an indicator that your
object graph may not accurately reect the real world relationships they
represent.
Delegate methods with prexes (Post#author_name) are ne, but its worth
a check to see if you can remove the prex. If not, make sure you didnt
actually want a reference to the prex object (Post#author).
Avoidmultiple prexes for delegate methods, such as User#account_plan_price.
Avoid assigning to instance variables to work around violations.
CHAPTER 38. LAW OF DEMETER 188
Objects vs Types
The version of the law quoted at the beginning of this chapter is the object
formulation from the original paper. The rst formulation was expressed in
terms of types:
..

For all classes C, and for all methods Mattached to C, all objects to
which Msends a message must be instances of classes associated
with the following classes:
1. The argument classes of M (including C).
2. The instance variable classes of C.
(Objects created by M, or by functions or methods which M calls,
and objects in global variables are considered as arguments of M.)
This formulation allows some more freedom when chaining using a uent syn-
tax. Essentially, it allows chaining as long as each step of the chain returns the
same type.
Examples:
# Mocking APIs
user.should_receive(:save).once.and_return(true)
# Ruby's Enumerable
users.select(&:active?).map(&:name)
# String manipulation
collection_name.singularize.classify.constantize
# ActiveRecord chains
users.active.without_posts.signed_up_this_week
Duplication
The Law of Demeter is related to the DRY principle, in that Law of Demeter
violations frequently duplicate knowledge of dependencies.
CHAPTER 38. LAW OF DEMETER 189
Example:
class CreditCardsController < ApplicationController
def charge_for_plan
if current_user.account.credit_card.valid?
price = current_user.account.plan.price
current_user.account.credit_card.charge price
end
end
end
In this example, the knowledge that a user has a credit card through its account
is duplicated. That knowledge is declared somewhere in the User and Account
classes when the relationship is dened, and then knowledge of it spreads to
two more locations in charge_for_plan.
Like most duplication, each instance isnt too harmful, but in aggregate, dupli-
cation will make refactoring slowly become impossible.
Application
The following smells may cause or result from Law of Demeter violations:
Feature Envy frommethods that reach through a dependency chain mul-
tiple times.
Shotgun Surgery resulting from changes in the dependency chain.
You can use these solutions to follow the Law of Demeter:
Move Methods that reach through a dependency to the owner of that
dependency.
Inject Dependencies so that methods have direct access to the depen-
dencies that they need.
Inline Classes that are adding hops to the dependency chain without
providing enough value.
Composition over inheritance
In class-based object-oriented systems, composition and inheritance are the
two primary methods of reusing and assembling components. Composition
Over Inheritance suggests that, when there isnt a strong case for using inheri-
tance, developers implement reuse and assembly using composition instead.
Lets look at a simple example implemented using both composition and inheri-
tance. In our example application, users can invite their friends to take surveys.
Users can be invited using either an email or an internal private message. Each
delivery strategy is implemented using a separate class.
Inheritance
In the inheritance model, we use an abstract base class called Inviter to
implement common invitation-sending logic. We then use EmailInviter and
MessageInviter subclasses to implement the delivery details.
190
CHAPTER 39. COMPOSITION OVER INHERITANCE 191
# app/models/inviter.rb
class Inviter < AbstractController::Base
include AbstractController::Rendering
include Rails.application.routes.url_helpers
self.view_paths = 'app/views'
self.default_url_options = ActionMailer::Base.default_url_options
private
def render_message_body
render template: 'invitations/message'
end
end
# app/models/email_inviter.rb
class EmailInviter < Inviter
def initialize(invitation)
@invitation = invitation
end
def deliver
Mailer.invitation_notification(@invitation, render_message_body).deliver
end
end
CHAPTER 39. COMPOSITION OVER INHERITANCE 192
# app/models/message_inviter.rb
class MessageInviter < Inviter
def initialize(invitation, recipient)
@invitation = invitation
@recipient = recipient
end
def deliver
Message.create!(
recipient: @recipient,
sender: @invitation.sender,
body: render_message_body
)
end
end
Note that there is no clear boundary between the base class and the sub-
classes. The subclasses access reusable behavior by invoking private methods
like render_message_body inherited from the base class.
Composition
In the composition model, we use a concrete InvitationMessage class to
implement common invitation-sending logic. We then use that class from
EmailInviter and MessageInviter to reuse the common behavior, and the inviter
classes implement delivery details.
CHAPTER 39. COMPOSITION OVER INHERITANCE 193
# app/models/invitation_message.rb
class InvitationMessage < AbstractController::Base
include AbstractController::Rendering
include Rails.application.routes.url_helpers
self.view_paths = 'app/views'
self.default_url_options = ActionMailer::Base.default_url_options
def initialize(invitation)
@invitation = invitation
end
def body
render template: 'invitations/message'
end
end
# app/models/email_inviter.rb
class EmailInviter
def initialize(invitation)
@invitation = invitation
@body = InvitationMessage.new(@invitation).body
end
def deliver
Mailer.invitation_notification(@invitation, @body).deliver
end
end
CHAPTER 39. COMPOSITION OVER INHERITANCE 194
# app/models/message_inviter.rb
class MessageInviter
def initialize(invitation, recipient)
@invitation = invitation
@recipient = recipient
@body = InvitationMessage.new(@invitation).body
end
def deliver
Message.create!(
recipient: @recipient,
sender: @invitation.sender,
body: @body
)
end
end
Note that there is now a clear boundary between the common behavior in
InvitationMessage and the variant behavior in EmailInviter and MessageInviter.
The inviter classes access reusable behavior by invoking public methods like
body on the shared class.
Dynamic vs Static
Although the two implementations are fairly similar, one dierence between
themis that, in the inheritance model, the components are assembled statically,
whereas the composition model assembles the components dynamically.
Ruby is not a compiled language and everything is evaluated at runtime, so
claiming that anything is assembled statically may sound like nonsense. How-
ever, there are several ways in which inheritance hierarchies are essentially
written in stone, or static:
You cant swap out a superclass once its assigned.
You cant easily add and remove behaviors after an object is instantiated.
You cant inject a superclass as a dependency.
CHAPTER 39. COMPOSITION OVER INHERITANCE 195
You cant easily access an abstract classs methods directly.
On the other hand, everything in a composition model is dynamic:
You can easily change out a composed instance after instantiation.
You can add and remove behaviors at any time using decorators, strate-
gies, observers, and other patterns.
You can easily inject composed dependencies.
Composed objects arent abstract, so you can use their methods any-
where.
Dynamic Inheritance
There are very few rules in Ruby, so many of the restrictions that apply to in-
heritance in other languages can be worked around in Ruby. For example:
You can reopen and modify classes after theyre dened, even while an
application is running.
You can extend objects with modules after theyre instantiated to add
behaviors.
You can call private methods by using send.
You can create new classes at runtime by calling Class.new.
These features make it possible to overcome some of the rigidity of inheritance
models. However, performing all of these operations is simpler with objects
than it is with classes, and doing too much dynamic type denition will make
the application harder to understand by diluting the type system. After all, if
none of the classes are ever fully formed, what does a class represent?
The trouble With Hierarchies
Using subclasses introduces a subtle problem into your domain model: it as-
sumes that your models follow a hierarchy; that is, it assumes that your types
fall into a tree-like structure.
CHAPTER 39. COMPOSITION OVER INHERITANCE 196
Continuing with the above example, we have a root type, Inviter, and two sub-
types, EmailInviter and MessageInviter. What if we want invitations sent by ad-
mins to behave dierently than invitations sent by normal users? We can create
an AdminInviter class, but what will its superclass be? How will we combine it
with EmailInviter and MessageInviter? Theres no easy way to combine email,
message, and admin functionality using inheritance, so youll end up with a
proliferation of conditionals.
Composition, on the other hand, provides several ways out of this mess, such
as using a decorator to add admin functionality to the inviter. Once you build
objects with a reasonable interface, you can combine them endlessly with min-
imal modication to the existing class structure.
Mixins
Mixins are Rubys answer to multiple inheritance.
However, mixins need to be mixed into a class before they can be used. Unless
you plan on building dynamic classes at runtime, youll need to create a class for
each possible combination of modules. This will result in a ton of little classes,
such as AdminEmailInviter.
Again, composition provides a clean answer to this problem, as you can create
as many anonymous combinations of objects as your little heart desires.
Ruby does allow dynamic use of mixins using the extend method. This tech-
nique does work, but it has its own complications. Extending an objects type
dynamically in this way dilutes the meaning of the word type, making it harder
to understand what an object is. Additionally, using runtime extend can lead to
performance issues in some Ruby implementations.
Single Table Inheritance
Rails provides a way to persist an inheritance hierarchy, known as Single Table
Inheritance, often abbreviated as STI. Using STI, a cluster of subclasses is per-
sisted to the same table as the base class. The name of the subclass is also
CHAPTER 39. COMPOSITION OVER INHERITANCE 197
saved on the row, allowing Rails to instantiate the correct subclass when pulling
records back out of the database.
Rails also provides a clean way to persist composed structures using polymor-
phic associations. Using a polymorphic association, Rails will store both the
primary key and the class name of the associated object.
Because Rails provides a clean implementation for persisting both inheritance
and composition, the fact that youre using ActiveRecord should have little in-
uence on your decision to design using inheritance versus composition.
Drawbacks
Although composed objects are largely easy to write and assemble, there are
situations where they hurt more than inheritance trees.
Inheritance cleanly represents hierarchies. If you really do have a hier-
archy of object types, use inheritance.
Subclasses always know what their superclass is, so theyre easy to in-
stantiate. If you use composition, youll need to instantiate at least two
objects to get a usable instance: the composing object, and the com-
posed object.
Using composition is more abstract, which means you need a name for
the composed object. In our earlier example, all three classes were in-
viters in the inheritance model, but the composition model introduced
the invitation message concept. Excessive composition can lead to
vocabulary overload.
Application
If you see these smells in your application, they may be a sign that you should
switch some classes from inheritance to composition:
Divergent Change caused by frequent leaks into abstract base classes.
Large Classes acting as abstract base classes.
Mixins serving to allow reuse while preserving the appearance of a hier-
archy.
CHAPTER 39. COMPOSITION OVER INHERITANCE 198
Classes with these smells may be dicult to transition to a composition model:
Duplicated Code will need to be pulled up into the base class before
subclasses can be switched to strategies.
Shotgun Surgery may represent tight coupling between base classes
and subclasses, making it more dicult to switch to composition.
These solutions will help move from inheritance to composition:
Extract Classes to liberate private functionality from abstract base
classes.
Extract Method to make methods smaller and easier to move.
Move Method to slim down bloated base classes.
Replace Mixins with Composition to make it easier to dissolve hierar-
chies.
Replace Subclasses with Strategies to implement variations dynamically.
After replacing inheritance models with composition, youll be free to use these
solutions to take your code further:
Extract decorators to make it easy to add behaviors dynamically.
Inject Dependencies to make it possible to compose objects in new
ways.
Following this principle will make it much easier to follow the Dependency In-
version Principle and the Open/Closed Principle.
Open/closed principle
This principle states that:
..

Software entities (classes, modules, functions, etc.) should be


open for extension, but closed for modication.
The purpose of this principle is to make it possible to change or extend the
behavior of an existing class without actually modifying the source code to
that class.
Making classes extensible in this way has a number of benets:
Every time you modify a class, you risk breaking it, along with all classes
that depend on that class. Reducing churn in a class reduces bugs in
that class.
Changing the behavior or interface to a class means that you need to
update any classes that depend on the old behavior or interface.
Allowing per-use extensions to a class eliminates this domino eect.
Strategies
It may sound nice to never need to change existing classes again, but
achieving this is dicult in practice. Once youve identied an area that keeps
changing, there are a few strategies you can use to make is possible to
199
CHAPTER 40. OPEN/CLOSED PRINCIPLE 200
extend without modications. Lets go through an example with a few of those
strategies.
In our example application, we have a Invitation class which can deliver itself
to an invited user:
# app/models/invitation.rb
def deliver
body = InvitationMessage.new(self).body
Mailer.invitation_notification(self, body).deliver
end
However, we need a way to allow users to unsubscribe from these
notications. We have an Unsubscribe model that holds the email addresses of
users that dont want to be notied.
The most direct way to add this check is to modify Invitation directly:
# app/models/invitation.rb
def deliver
unless unsubscribed?
body = InvitationMessage.new(self).body
Mailer.invitation_notification(self, body).deliver
end
end
However, that would violate the Open/Closed Principle. Lets see how we can
introduce this change without violating the principle.
CHAPTER 40. OPEN/CLOSED PRINCIPLE 201
Inheritance
One of the most common ways to extend an existing class without modifying
it is to create a new subclass.
We can use a new subclass to handle unsubscriptions:
# app/models/unsubscribeable_invitation.rb
class UnsubscribeableInvitation < Invitation
def deliver
unless unsubscribed?
super
end
end
private
def unsubscribed?
Unsubscribe.where(email: recipient_email).exists?
end
end
CHAPTER 40. OPEN/CLOSED PRINCIPLE 202
This can be a little awkward when trying to use the new behavior, though. For
example, we need to create an instance of this class, even though we want to
save it to the same table as Invitation:
# app/models/survey_inviter.rb
def create_invitations
Invitation.transaction do
recipients.map do |recipient_email|
UnsubscribeableInvitation.create!(
survey: survey,
sender: sender,
recipient_email: recipient_email,
status: 'pending',
message: @message
)
end
end
end
This works alright for creation, but using the ActiveRecord pattern, well end
up with an instance of Invitation instead if we ever reload from the database.
That means that inheritance is easiest to use when the class youre extending
doesnt require persistence.
Inheritance also requires some creativity in unit tests to avoid duplication.
CHAPTER 40. OPEN/CLOSED PRINCIPLE 203
Decorators
Another way to extend an existing class is to write a decorator.
Using Rubys DelegateClass method, we can quickly create decorators:
# app/models/unsubscribeable_invitation.rb
class UnsubscribeableInvitation < DelegateClass(Invitation)
def deliver
unless unsubscribed?
super
end
end
private
def unsubscribed?
Unsubscribe.where(email: recipient_email).exists?
end
end
The implementation is extremely similar to the subclass, but it can now be
applied at runtime to instances of Invitation:
# app/models/survey_inviter.rb
def deliver_invitations
create_invitations.each do |invitation|
UnsubscribeableInvitation.new(invitation).deliver
end
end
The unit tests can also be greatly simplied using stubs.
This makes it easier to combine with persistence. However, Rubys
DelegateClass doesnt combine well with ActionPacks polymorphic URLs.
CHAPTER 40. OPEN/CLOSED PRINCIPLE 204
Dependency Injection
This method requires more forethought in the class you want to extend, but
classes that follow Inversion of Control can inject dependencies to extend
classes without modifying them.
We can modify our Invitation class slightly to allow client classes to inject a
mailer:
# app/models/invitation.rb
def deliver(mailer)
body = InvitationMessage.new(self).body
mailer.invitation_notification(self, body).deliver
end
CHAPTER 40. OPEN/CLOSED PRINCIPLE 205
Now we can write a mailer implementation that checks to see if a user is
unsubscribed before sending them messages:
# app/mailers/unsubscribeable_mailer.rb
class UnsubscribeableMailer
def self.invitation_notification(invitation, body)
if unsubscribed?(invitation)
NullMessage.new
else
Mailer.invitation_notification(invitation, body)
end
end
private
def self.unsubscribed?(invitation)
Unsubscribe.where(email: invitation.recipient_email).exists?
end
class NullMessage
def deliver
end
end
end
And we can use dependency injection to substitute it:
# app/models/survey_inviter.rb
def deliver_invitations
create_invitations.each do |invitation|
invitation.deliver(UnsubscribeableMailer)
end
end
CHAPTER 40. OPEN/CLOSED PRINCIPLE 206
Everything is Open
As youve followed along with these strategies, youve probably noticed that
although weve found creative ways to avoid modifying Invitation, weve had
to modify other classes. When you change or add behavior, you need to
change or add it somewhere. You can design your code so that most new or
changed behavior takes place by writing a new class, but something,
somewhere in the existing code will need to reference that new class.
Its dicult to determine what you should attempt to leave open when writing
a class. Its hard to know where to leave extension hooks without anticipating
every feature you might ever want to write.
Rather than attempting to guess what will require extension in the future, pay
attention as you modify existing code. After each modication, check to see if
theres a way you can refactor to make similar extensions possible without
modifying the underlying class.
Code tends to change in the same ways over and over, so by making each
change easy to apply as you need to make it, youre making the next change
easier.
CHAPTER 40. OPEN/CLOSED PRINCIPLE 207
Monkey Patching
As a Ruby developer, you probably know that one quick way to extend a class
without changing its source code is to use a monkey patch:
# app/monkey_patches/invitation_with_unsubscribing.rb
Invitation.class_eval do
alias_method :deliver_unconditionally, :deliver
def deliver
unless unsubscribed?
deliver_unconditionally
end
end
private
def unsubscribed?
Unsubscribe.where(email: recipient_email).exists?
end
end
Although monkey patching doesnt literally modify the classs source code, it
does modify the existing class. That means that you risk breaking it, including
all classes that depend on it. Since youre changing the original behavior,
youll also need to update any client classes that depend on the old behavior.
In addition to all the drawbacks of directly modifying the original class,
monkey patches also introduce confusion, as developers will need to look in
multiple locations to understand the full denition of a class.
In short, monkey patching has most of the drawbacks of modifying the original
class without any of the benets of following the Open Closed Principle.
Drawbacks
Although following this principle will make code easier to change, it may make
it more dicult to understand. This is because the gained exibility requires
CHAPTER 40. OPEN/CLOSED PRINCIPLE 208
introducing indirection and abstraction. Although each of the three strategies
outlined in this chapter are more exible than the original change, directly
modifying the class is the easiest to understand.
This principle is most useful when applied to classes with high reuse and
potentially high churn. Applying it everywhere will result in extra work and
more obscure code.
Application
If you encounter the following smells in a class, you may want to begin
following this principle:
Divergent Change caused by a lack of extensibility.
Large Classes and long methods which can be eliminated by extracting
and injecting dependent behavior.
You may want to eliminate the following smells if youre having trouble
following this principle:
Case statements make it hard to obey this principle, as you cant add to
the case statement without modifying it.
You can use the following solutions to make code more compliant with this
principle:
Extract Decorator to extend existing classes without modication.
Inject Dependencies to allow future extensions without modication.
Dependency inversion
principle
The Dependency Inversion Principle, sometimes abbreviated as DIP, was
created by Uncle Bob Martin.
The principle states:
..

A. High-level modules should not depend on low-level modules.


Both should depend on abstractions.
B. Abstractions should not depend upon details. Details should
depend upon abstractions.
This is a very technical way of proposing that developers invert control.
Inversion of Control
Inversion of control is a technique for keeping software exible. It combines
best with small classes with single responsibilities. Inverting control means
assigning dependencies at run-time, rather than statically referencing
dependencies at each level.
This can be hard to understand as an abstract concept, but its fairly simple in
practice. Lets jump into an example:
209
CHAPTER 41. DEPENDENCY INVERSION PRINCIPLE 210
# app/models/survey.rb
def summaries_using(summarizer, options = {})
questions.map do |question|
hider = UnansweredQuestionHider.new(summarizer, options[:answered_by])
question.summary_using(hider)
end
end
# app/controllers/summaries_controller.rb
def show
@survey = Survey.find(params[:survey_id])
@summaries = @survey.summaries_using(summarizer, constraints)
end
# app/controllers/summaries_controller.rb
def constraints
if include_unanswered?
{}
else
{ answered_by: current_user }
end
end
The summaries_using method builds a summary of the answers to each of the
surveys questions.
However, we also want to hide the answers to questions that the user hasnt
answered themselves, so we decorate the summarizer with an
UnansweredQuestionHider. Note that were statically referencing the concrete,
lower-level detail UnansweredQuestionHider from Survey rather than depending
on an abstraction.
In the current implementation, the Survey#summaries_using method will need to
change whenever something changes about the summaries. For example,
hiding the unanswered questions required changes to this method.
Also, note that the conditional logic is spread across several layers.
SummariesController decides whether or not to hide unanswered questions.
That knowledge is passed into Survey#summaries_using. SummariesController
CHAPTER 41. DEPENDENCY INVERSION PRINCIPLE 211
also passes the current user down into Survey#summaries_using, and from there
its passed into UnansweredQuestionHider:
# app/models/unanswered_question_hider.rb
class UnansweredQuestionHider
NO_ANSWER = "You haven't answered this question".freeze
def initialize(summarizer, user)
@summarizer = summarizer
@user = user
end
def summarize(question)
if hide_unanswered_question?(question)
NO_ANSWER
else
@summarizer.summarize(question)
end
end
private
def hide_unanswered_question?(question)
@user && !question.answered_by?(@user)
end
end
CHAPTER 41. DEPENDENCY INVERSION PRINCIPLE 212
We can make changes like this easier in the future by inverting control:
# app/models/survey.rb
def summaries_using(summarizer)
questions.map do |question|
question.summary_using(summarizer)
end
end
# app/controllers/summaries_controller.rb
def show
@survey = Survey.find(params[:survey_id])
@summaries = @survey.summaries_using(decorated_summarizer)
end
private
def decorated_summarizer
if include_unanswered?
summarizer
else
UnansweredQuestionHider.new(summarizer, current_user)
end
end
Now the Survey#summaries_using method is completely ignorant of answer
hiding; it simply accepts a summarizer, and the client (SummariesController)
injects a decorated dependency. This means that adding similar changes
wont require changing the Summary class at all.
This also allows us to simplify UnansweredQuestionHider by removing a
condition:
# app/models/unanswered_question_hider.rb
def hide_unanswered_question?(question)
!question.answered_by?(@user)
end
CHAPTER 41. DEPENDENCY INVERSION PRINCIPLE 213
We no longer build UnansweredQuestionHider when a user isnt signed in, so we
dont need to check for a user.
Where To Decide Dependencies
While following the previous example, you probably noticed that we didnt
eliminate the UnansweredQuestionHider dependency; we just moved it around.
This means that, while adding new summarizers or decorators wont aect
Summary, they will aect SummariesController in the current implementation. So,
did we actually make anything better?
In this case, the code was improved because the information that aects the
dependency decision - params[:unanswered] - is now closer to where we make
the decision. Before, we needed to pass a boolean down into summaries_using,
causing that decision to leak across layers.
Push your dependency decisions up until they reach the layer that contains
the information needed to make those decisions, and youll prevent changes
from aecting several layers.
Drawbacks
Following this principle results in more abstraction and indirection, as its often
dicult to tell which class is being used for a dependency.
Looking at the example above, its now impossible to know in summaries_using
which class will be used for the summarizer:
# app/models/survey.rb
def summaries_using(summarizer)
questions.map do |question|
question.summary_using(summarizer)
end
end
This makes it dicult to know exactly whats going to happen. You can
mitigate this issue by using naming conventions and well-named classes.
CHAPTER 41. DEPENDENCY INVERSION PRINCIPLE 214
However, each abstraction introduces more vocabulary into the application,
making it more dicult for new developers to learn the domain.
Application
If you identify these smells in an application, you may want to adhere more
closely to the Dependency Inversion Principle:
Following DIP can eliminate Shotgun surgery by consolidating
dependency decisions.
Code suering from divergent change may improve after having some
of its dependencies injected.
Large classes and long methods can be reduced by injecting
dependencies, as this will outsource dependency resolution.
You may need to eliminate these smells in order to properly invert control:
Excessive use of callbacks will make it harder to follow this principle,
because its harder to inject dependencies into a callback.
Using mixins and STI for reuse will make following this principle more
dicult, because inheritance is always decided statically. Because a
class cant decide its parent class at runtime, inheritance cant follow
inversion of control.
You can use these solutions to refactor towards DIP-compliance:
Inject Dependencies to invert control.
Use Extract Class to make smaller classes that are easier to compose
and inject.
Use Extract Decorator to make it possible to package a decision that
involves multiple classes and inject it as a single dependency.
Replace Callbacks with Methods to make dependency injection easier.
Replace Conditional with Polymorphism to make dependency injection
easier.
CHAPTER 41. DEPENDENCY INVERSION PRINCIPLE 215
Replace Mixin with Composition and Replace Subclasses with
Strategies to make it possible to decide dependencies abstractly at
runtime.
Use Class as Factory to make it possible to abstractly instantiate
dependencies without knowing which class is being used and without
writing abstract factory classes.
Following Single Responsibility Principle and Composition Over Inheritance
will make it easier to follow this principle. Following this principle will make it
easier to obey the Open-Closed Principle.

You might also like