One of the most useful techniques I’ve found when building software is dependency inversion. It seems to be largely misunderstood by most programmers and confused with dependency injection. Yet, it is a cornerstone of Clean Architecture and is the main principle behind decoupling a system from its lower level dependencies.
The central idea in DI is that high level policies must not depend upon lower level details. Typically, the flow of dependency follows the same direction as the flow of control as a program executes. The inversion in DI refers to the inversion of dependency flow against control flow.
The inversion of dependency flow happens on the source code and not on runtime. This seems to be a common point of confusion when trying to grok DI. In the source code, dependency is expressed by the presence/utilization of a module (dependency) in another (dependent). To perform the inversion, an indirection of the dependency must be done and this is typically done using interfaces. Although, having written applications in JS which does not natively support interfaces, I think the interface language construct is not strictly necessary to achieve DI.
Rather than depending on a specific module, we depend on an interface. An interface consist of public functions, arguments, returns, and exceptions thrown. An interface must express the purpose of a module using these elements. Put differently, an interface is a precise and formal specification of purpose and structure.
To be usable for its purpose, modules must fulfill i.e. implement the interface. Without interfaces, the higher level module must “know how to use” the dependency. With interfaces, the lower level modules must “know how they will be used”. This is a big shift of responsibilities. Now, the dependencies must “look up” at the interface and they must be implemented to its specification. In essence, they now depend on the interface.
In a very fundamental way, an interface is a boundary. It is the point where dependency flow reverses and it is a boundary that separates higher level modules from lower level ones. Crossing an interface is crossing an abstraction layer. On one side, we have high level policies embodying the high value business logic while, on the other, we have lower level details serving as a business-agnostic foundation of the system.
When dependency flow is reversed, lower level modules can now be freely changed without affecting the structure of high level ones. Hence, we refer to lower modules as details. We expect them to change and vary in implementation while still providing the same “service” e.g. calculation, storage, retrieval, etc. This gives us a more flexible system, highly decoupled, and robust to changes in lower level dependencies.