Dependency Inversion Principle
I don't like this name. It is highly deceiving. Let's see why I think so - and recalling how the software development evolved in the past decades helps me to prove my point.
In the real life, real people frequently use "top down design". First, one shows up with a general plan, then starts adding more details to it. At first, a family member says "We should spend our vacation at a nice place". Then there're some conversations within the family, and the common agreement becomes that "nice place" actually means a beach somewhere. Then more conversation occurs, and we add more details, like dates, budget, means of travel, and the location. But even the location is first just the name of the sea, like Adriatic Sea, and the town is selected later.
Software development is pretty similar. First we eclicit the large scale workflows or features. They are very close to the actual business processes. Then we analyze them, find the possible gaps, identify the risks, decide how to address these risks, and in general, we start break the big feature into pieces, and so on. For many years this method was the accepted software development methodology - first we wrote a "top level function", which called lower level functions, and so on. The data was shared among these functions, so unfortunately, these functions highly depended on each other. If the requirements changed later, and we had to change the data structure, then the thorough analyzis was mandatory to update the relevant code pieces everywhere. This kind of analyzis is very error prone, and it is insanely expensive too. So the need of eliminating this dependency was high.
The Object-oriented programming was invented. There might be different definitions and understandings what it really is, but it's irrelevant for our purpose. The data structures, and all the code that deals with it, was encapsulated into a single "class" (which is the template of the object). But one single class is not enough, nor would be that useful (the traditional way described in the previous chapter could have been imagined as one single class). So the classes cooperate with each other. If one class wants to deal with the data of another class, then it has to instantiate the other class. It was recognized that each class could be featured as a kind of "service provider" - "requests" are sent to the classes, and they gave back "responses". One does not need to understand the internals of the class (in fact, it was a big no-no to take care of the internals), it is enough if the "interface" of the class is known. The interface is the collection of the requests and the responses. Along this route testing also became more and more to be a science on its own - the unit tests appeared, and within the unit tests, the testers wanted to ensure that all external dependencies (the other classes) are instantiated with well defined behaviors. But it was very hard to reach this goal - after all, the other classes, the dependencies, were instantiated by the tested class.
Considering all that, the need of not instantiating other classes became more and more important. In other words, it means that we do not want any class to depend on other (sub)classes, rather we want them to depend on abstractions (interfaces). No class should delegate its behavior to lower level classes, because it is quite error prone still, and hard to test. In fact, programmers were able to infringe the well-defined contracts by using the instantiated classes for other purposes (after all, the undocumented features, side effects, and alike are almost unavoidable). If that happened, then the class was no longer depending on the abstraction, but again on the actual implementation. So the decision was made - when I design the class, it must depend on the contract (abstraction = interface) only.
In Java, the abstraction is incorporated into the language, it is the interface. So it was decided that a higher level class may depend only on interfaces, and the lower level classes are just possible implementations of these interfaces. So, the "dependency inversion" means, that the class does not depend on the implementation, it depends on the abstraction. But it is not "inversion" actually - this name would suggest as if the lower level classes, the implementors of the abstraction, would depend on the higher level class. But that's apparently not true. This is why I state that the name is deceiving. It's definition even emphasizes this fact:
领英推荐
The abstraction is the contract that the implementors must fulfill. So it would be better to say that it is "contract driven development". But that name was already used for something else. The "abstraction driven development" is the other possible name, but unfortunately, this name was never used, and now it's too late to introduce it.
So let's get used to it, and let's try to make it true. There's a way how it could be true. When one designs the higher levels, the expected request-response pairs are also designed. These pairs are the abstraction. Now the next step is to implement the classes that actually provide these services. So the way these lower level classes are implemented is inverted - we already have abstraction, the higher levels even already use them, when we start implementing the classes. As long as these classes implement the abstractions well, the software will work.
So testing became super easy - we'll provide test implementations for these abstractions, with well defined behavior, so the tested class will have these test implementation, which makes it easy to test.
Now we have only one problem to solve. It is said that the higher levels use interfaces, and the lower levels are the implementations of these interfaces, but how will the higher level instantiate these implementations? It is not possible to instantiate an interface in Java. So how is it possible, without re-introducing the dependency?
The good news is, that there are ways. One is "dependency injection", when the dependency is instantiated "from outside". But that's another story.
One final note. Please don't build the false impression that "dependency inversion" is needed only for making testing easier. That's rather only a very useful side effect. But the code written on this way is less error-prone too, plus the teams are free to implement their chosen abstraction in any way they want - as long as the contract is kept.
Now I hope the principle is clear, which was my major goal of writing this article.