As defined by the Object Mentor, the Dependency Inversion Principle states
- High level modules (or classes) should not depend upon low level modules. Both should depend on abstractions.
- Abstractions should not depend upon details. Details should depend upon abstractions.
So what does this even mean and why does it matter? Well I think we should start by defining the requirements of good code. Then we would just need to write code in a way that we meet all of the requirements. You will quickly see that the Dependency Inversion Principle (DIP) helps us get there.
What is good code?
Good code can be changed easily if the business requirements change without concern that other parts of your system will break. Here are three criteria to help us define good code.
Bad code: Hard to change because every change affects too many parts of the system
Good code: Easy to change because each class is not dependant on other classes
2) Not fragile
Bad code: When you make a change, unexpected parts of the system break
Good code: Easy to make changes because each part of the system is separate and only needs to be changed for one reason. Your code is also easy to test and should therefore be well-tested
Bad code: It is hard to reuse in another application because it cannot be disentangled from the current application.
Good code: It’s easy to reuse pieces of code because code is written without dependency on the details.
If your code meets all of its business requirements and it meets these three criteria, then most software engineers would agree that your code is clean. Now on to the DIP.
Dependency Inversion Principle Example
Let’s start with an example of a class that does not follow the dependency inversion principle.
class Newsperson def broadcast(news) Newspaper.new.print(news) end end class Newspaper def print(news) puts news end end laura = Newsperson.new laura.broadcast("Some Breaking News!") # => "Some Breaking News!"
This is a functioning broadcast method, but right now it’s tied to the Newspaper object. What if we change the name of the Newspaper class? What if we add more broadcast platforms? Both of these things would cause us to have to change our Newsperson object as well. Even though we have a very small dependency on the type of broadcasting our newsperson will be doing, it breaks all three of the requirements we defined above of good code.
Now let’s redo this code and implement the DIP.
class Newsperson def broadcast(news, platform = Newspaper) platform.new.broadcast(news) end end class Newspaper def broadcast(news) do_something_with news end end class Twitter def broadcast(news) tweets news end end class Television def broadcast(news) live_coverage news end end laura = Newsperson.new laura.broadcast("Breaking news!") #do_something_with "Breaking news!" laura.broadcast("Breaking news!", Twitter) #tweets "Breaking news!"
As you can see, we can now pass any news broadcasting platform through the broadcast method whether that’s Twitter, TV, or Newspaper. And if we know that Newspapers are usually going to be the platform of choice, we can make newspaper the default platform. This second strategy is flexible, mobile, and not fragile. We can change any of these classes and we won’t break the other classes. The higher level Newsperson class does not depend on the lower level classes and vice versa.
Finally, this second block of code is much easier to test. You can test each of the three platforms to make sure they produce the correct output. And you can test the higher level Newsperson class with a mock platform.