Ruby Science

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 flexible. 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 it’s fairly simple in practice. Let’s jump into an example:

# app/models/survey.rb
def summaries_using(summarizer, options = {})
  questions.map do |question|
    hider = UnansweredQuestionHider.new(summarizer, options[])
    question.summary_using(hider)
  end
end
# app/controllers/summaries_controller.rb
def show
  @survey = Survey.find(params[])
  @summaries = @survey.summaries_using(summarizer, constraints)
end
# app/controllers/summaries_controller.rb
def constraints
  if include_unanswered?
    {}
  else
    { current_user }
  end
end

The summaries_using method builds a summary of the answers to each of the survey’s questions.

However, we also want to hide the answers to questions that the user has not personally answered, so we decorate the summarizer with an UnansweredQuestionHider. Note that we’re 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 requires 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 also passes the current user down into Survey#summaries_using, and from there it’s 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

We can make future changes like this easier 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[])
  @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 won’t 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

We no longer build UnansweredQuestionHider when a user isn’t signed in, so we don’t need to check for a user.

Where To Decide Dependencies

While following the previous example, you probably noticed that we didn’t eliminate the UnansweredQuestionHider dependency; we just moved it around. This means that, while adding new summarizers or decorators won’t affect Summary, they will affect SummariesController in the current implementation. So, did we actually make anything better?

In this case, the code was improved because the information that affects 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.

If you push your dependency decisions up until they reach the layer that contains the information needed to make those decisions, you will prevent changes from affecting several layers.

Drawbacks

Following this principle results in more abstraction and indirection, as it’s often difficult to tell which class is being used for a dependency.

Looking at the example above, it’s 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 difficult to know exactly what’s going to happen. You can mitigate this issue by using naming conventions and well-named classes. However, each abstraction introduces more vocabulary into the application, making it more difficult 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 (DIP):

  • Following DIP can eliminate shotgun surgery by consolidating dependency decisions.
  • Code suffering 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 the DIP, because it’s harder to inject dependencies into a callback.
  • Using mixins and STI for reuse will make following the DIP more difficult, because inheritance is always decided statically. Because a class can’t decide its parent class at run-time, inheritance can’t follow inversion of control.

You can use these solutions to refactor towards DIP-compliance:

Following the single responsibility principle and composition over inheritance will make it easier to follow the dependency inversion principle. Following this principle will make it easier to obey the open/closed principle.

Ruby Science

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

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