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.
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.
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 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.
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
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.