SOLID Principles #5 - Dependency Inversion Principle

Photo of Marcin Jakubowski

Marcin Jakubowski

Updated Feb 27, 2023 • 6 min read
adam-kool-11868-unsplash

Welcome to the five-part series of blog posts about SOLID Principles.

In each part, I will describe and analyze one of these principles. In the last part, expect a summary of the entire series containing a few tips and thoughts.

Let’s start. What are SOLID Principles? There are five general design principles of object-oriented programming intended to make software more understandable, extendable, maintainable and testable.

  1. SS
  2. Open/closed principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. Dependency Inversion Principle (DIP)

Today, more about fifth principle - Dependency Inversion Principle.

Dependency Inversion Principle

This principle contains two statements:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.

  • Abstractions should not depend on details. Details should depend on abstractions.

When it comes to this principle, I think it is best to go straight to the example.

Example

The code snippet shown below is an example of DIP violation.

class ReportGeneratorManager
  def initialize(data)
    @data = data
  end
  
  def call
    generate_xml_report
    additional_actions
  end
  
  private
  
  attr_reader :data
  
  def generate_xml_report
    XmlRaportGenerator.new(data).generate
  end
  
  def additional_actions
    ...
  end
end

What is wrong with that? First of all, classes ReportGenaratorManager (high-level) & XmlReportGenerator (low-level) are tightly coupled. High-level class depends on details (concrete implementation) of low-level class. Additionally, when there is a need to add another type of report generator, it will require modification in our high-level class - so we will be forced to make changes in high-level class because of changes in low-level class).

What we can do right here is to invert dependency. Let details depend on abstractions, not a specific implementation. Since Ruby is a dynamic-typed language, we can use the duck-typing technique. We do not have to create any abstract classes or interfaces because they do not exist in Ruby world.

We will also use the Dependency Injection pattern to achieve our goal but one thing must be pointed:

Dependency Inversion Principle =/= Dependency Injection

Dependency Injection is only technique that helps us fulfill this principle.

class ReportGeneratorManager
  def initialize(data, generator = XmlRaportGenerator)
    @data = data
    @generator = generator
  end
  
  def call
    generate_report
    additional_actions
  end
  
  private
  
  attr_reader :data, :generator
  
  def generate_report
    generator.new(data).generate
  end
  
  def additional_actions
    ...
  end
end

What we did is that we allow injecting specific generator class to our manager class via the constructor (we also provided default generator). Now, our high-level class operates only on the general interface which is common to all concrete generator classes. We can easily exchange our implementation class (e.g. use the different implementation in different places where we use ReportGeneratorManager class).

Provided solution is much more flexible and easier to test.

Unit testing

Unit testing of solution that we proposed is easy and pleasant. As we are already injecting our dependencies by the constructor, we can create dependencies doubles and inject them instead of real ones. No need of using:

any_instance_of(ClassToMock) ...
 
or 

ClassToMock.new.stub(:method_to_stub)

which somebody calls even a bad testing practice.

This is the main goal of unit testing - test single unit in isolation without concerning any dependencies. With our solution, we are on easy way to do it.

I will even go one step further and say that if you have a problem with mocking any of your class dependencies, it means that you have too rigid dependencies. It is not the first time when writing tests help us to find smells in our code or tells us that we overcomplicated something.

Confusing concepts

I already said that we should not confuse Dependency Inversion Principle with Dependency Injection. However, there is a third term that could be confused with previous two. It's Inversion of Control. My blog post is not exactly about it so I refer you to a great article by Martin Fowler: DIP in the Wild . Just single quote from this article to wrap it up:

DI is about wiring, IoC is about direction, and DIP is about shape. - Martin Fowler

Summary of the whole series

Like I already mentioned in one of the series parts - we use these principles (and any other patterns) to achieve specific benefit, not to simply apply it because it is "pro". If benefits are not clear and visible, we are just wasting our time. In the most cases applying some rule or pattern just to fulfill it is a very bad thing and leads to problems and overcomplicated codebase. Remember that these rules were written down to help you (improve your code) so try to not overcomplicate things more than you need. There is no silver bullet. Just keep common sense in everything you do during your work. Remember main ideas that come from each of these principles and treat them like a good guidance.

Finally, I must be honest - there is no way to read all these rules, principles, patterns etc. and say "Ok! Now, I will write perfect code!". This is a continuous process and requires a lot, lot written lines of code.

Photo of Marcin Jakubowski

More posts by this author

Marcin Jakubowski

Marcin graduated from Civil Engineering at the Poznań University of Technology. One year before...
Efficient software development  Build faster, deliver more  Start now!

Read more on our Blog

Check out the knowledge base collected and distilled by experienced professionals.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business