Dependency inversion principle (DIP) – Architectural Principles
The DIP provides flexibility, testability, and modularity, by reducing tight coupling between classes or modules.Let’s continue with another quote from Robert C. Martin (including the implied context from Wikipedia):
One should “depend upon abstractions, [not] concretions.”
Are you wondering why not use abstract classes? While helpful at providing default behaviors over inheritance, they’re not fully abstract. If one is, it’s better to use an interface instead.
Interfaces are more flexible and powerful, acting as contracts between parts of a system. They also allow a class to implement multiple interfaces, boosting flexibility. However, don’t discard abstract classes mindlessly. Actually, don’t discard anything mindlessly.
Exposing interfaces can save countless hours of struggling to find complex workaround when writing unit tests. That is even more true when building a framework or library that others use. In that case, please pay even more attention to providing your consumers with interfaces to mock if necessary.All that talk about interfaces again is great, but how can we invert the flow of dependencies? Spoiler alert: interfaces!Let’s compare a direct dependency and an inverted dependency first.
A direct dependency occurs when a particular piece of code (like a class or a module) relies directly on another. For example, if Class A uses a method from Class B, then Class A directly depends on Class B, which is a typical scenario in traditional programming.Say we have a SomeService class that uses the SqlDataPersistence class for production but the LocalDataPersistence class during development and testing. Without inverting the dependency flow, we end up with the following UML dependency graph:
Figure 3.2: Direct dependency graph schema
With the preceding system, we could not change the SqlDataPersistence or LocalDataPersistence classes by the CosmosDbDataPersistence class (not in the diagram) without impacting the SomeService class.We call direct dependencies like these tight coupling.
An inverted dependency occurs when high-level modules (which provide complex logic) are independent of low-level modules (which provide basic, foundational operations). We can achieve this by introducing an abstraction (like an interface) between the modules. This means that instead of Class A depending directly on Class B, Class A would rely on an abstraction that Class B implements.Here is the updated schema that improves the direct dependency example:
Figure 3.3: Indirect dependency graph schema
In the preceding diagram, we successfully inverted the dependency flow by ensuring the SomeService class depends only on an IDataPersistance interface (abstraction) that the SqlDataPersistence and LocalDataPersistence classes implement. We could then use the CosmosDbDataPersistence class (not in the diagram) without impacting the SomeService class.We call inverted dependencies like these loose coupling.Now that we covered how to invert the dependency flow of classes, we look at inverting subsystems.