Ruby Science
Inject Dependencies
Injecting dependencies allows you to keep dependency resolutions close to the logic that affects them. It can prevent sub-dependencies from leaking throughout the code base, and it simplifies changing 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:
- Move the dependency decision to a higher level component.
- Pass the dependency as a parameter to the lower level component.
- 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.
- Makes it easier to avoid subclassing, following composition over inheritance.
- Extend existing classes without modifying them, following the open/closed principle.
- Avoids burdening classes with the knowledge of constructing their own dependencies, following the single responsibility principle.
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 different summary types to view. For example, they can see the most recent answer to each question, 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
[:id]
paramsend
Survey#summaries_using
asks each of its questions for a
summary using that summarizer and the given options:
# app/models/survey.rb
.summary_using(summarizer, options) question
Question#summary_using
instantiates the requested
summarizer with the requested options, then asks the summarizer to
summarize the question:
# app/models/question.rb
def summary_using(summarizer_name, options)
= "Summarizer::#{summarizer_name.classify}".constantize
summarizer_factory = summarizer_factory.new(options)
summarizer = summarizer.summarize(self)
value Summary.new(title, value)
end
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-specific options can be provided when building the
summarizer.
Let’s remedy this by having the controller build the actual
summarizer instance. First, we’ll 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
= params[:id]
summarizer_name = "Summarizer::#{summarizer_name.classify}".constantize
summarizer_factory .new(options)
summarizer_factoryend
Then, we’ll change Question#summary_using
to take an
instance instead of a name:
# app/models/question.rb
def summary_using(summarizer, options)
= summarizer.summarize(self)
value Summary.new(title, value)
end
That options
argument is no longer necessary because it
was only used to build the summarizer—which is now handled by the
controller. Let’s remove it:
# app/models/question.rb
def summary_using(summarizer)
= summarizer.summarize(self)
value Summary.new(title, value)
end
We also don’t need to pass it from Survey
:
# app/models/survey.rb
.summary_using(summarizer) question
This interaction has already improved, because the
options
argument is no longer uselessly passed around
through two models. It’s only used in the controller where the
summarizer instance is built. Building the summarizer in the controller
is appropriate, because the controller knows the name of the summarizer
we want to build, as well as which options are used when building
it.
Now that we’re using dependency injection, we can take this even further.
By default, in order to prevent the summary from influencing a user’s own answers, users don’t see summaries for questions they haven’t answered yet. Users can click a link to override this decision and view the summary for every question.
The information that determines whether or not to hide unanswered questions lives in the controller:
# app/controllers/summaries_controller.rb
def constraints
if include_unanswered?
{}
else
{ answered_by: current_user }
end
end
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 = {})
.map do |question|
questions
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
.summary_using(summarizer)
questionend
end
def hide_unanswered_question?(question, answered_by)
&& !question.answered_by?(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 duplicate decision:
# 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)
.answered_by?(@user)
!questionend
end
We’ll decide whether or not to decorate the base summarizer in our controller:
# app/controllers/summaries_controller.rb
def decorated_summarizer
if include_unanswered?
summarizerelse
UnansweredQuestionHider.new(summarizer, current_user)
end
end
Now, the decision of whether or not to hide answers is completely
removed from Survey
:
# app/models/survey.rb
def summaries_using(summarizer)
.map do |question|
questions.summary_using(summarizer)
questionend
end
For more explanation of using decorators, as well as step-by-step instructions for how to introduce them, see the Extract Decorator chapter.
Drawbacks
Injecting dependencies in our example made each
class—SummariesController
, Survey
,
Question
and UnansweredQuestionHider
—easier to
understand as a unit. However, it’s now difficult to understand what
kind of summaries will be produced 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 they’re
used.
In this case, we believe that using dependency injection resulted in an overall win for readability and flexibility. However, it’s important to remember that the further you move a dependency’s resolution from its use, the harder it is to figure out what’s actually being used in lower level components.
In our example, there isn’t 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)
= summarizer.summarize(self)
value 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.
Next Steps
- When pulling dependency resolution up into a higher level class, check that class to make sure it doesn’t become a large class because of all the logic surrounding dependency resolution.
- If a class is suffering from divergent change because of new or modified 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 facade.
Ruby Science
The canonical reference for writing fantastic Rails applications from authors who have created hundreds.