Dependency Inversion Principle

The Dependency Inversion Principle, sometimes abbreviated as “DIP,” was created by Uncle Bob Martin.

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.

# 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 survey’s questions.

However, we also want to hide the answers to questions that the user hasn’t answered themselves, so we decorate the summarizer with an UnansweredQuestionHider. Note that we’re statically referencing the concrete, lower-level detail 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 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 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 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.

  • ! Cố gắng loại bỏ sự phụ thuộc của survey vào summerizer, bằng cách tìm đúng decorator (summeraizer hoặc UnansweredQuestionHider bằng điều kiện ở ngoài controller), sau đó pass vào trong method @survey.summaries_using(decorated_summarizer)

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.

Push your dependency decisions up until they reach the layer that contains the information needed to make those decisions, and you’ll prevent changes from affecting several layers.

  • ! Trong ví dụ ở trên, việc quyết định dependency phụ thuộc vào params[:unanswered]. Với code cũ, chúng ta pass cái này lúc khởi tạo UnansweredQuestionHider. Việc này force cho class UnansweredQuestionHider phải đưa ra decision decision bị leak across layers.
  • ! Lời khuyên đó là: Nên đặt dependency decisions ở nơi mà ta cần đưa quyết định, việc này tránh ảnh hưởng tới các layers bên dưới.

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.

Cái khó của việc dùng technique này, cũng giống như việc sử dụng Đa hình, ta không hề biết được object thực sự đang được gọi là gì. Như trong ví dụ bên, ta không biết instance summarizer nào đang được dùng trong model survey

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.