The principle of Dependency Inversion is a useful design pattern to be more flexible about dependencies. It is helpful in a layered architecture where high-level packages can depend on low-level packages but not the other way round. The observer pattern used in Model-View-Controller is a well-known example. We can visualize it like this:
On the left you can see that class X uses class Y directly, so package A depends on package B. This is a problem because A is lower level and should not depend on B. To invert that dependency, package A can define an interface Iy which Y in B implements (assuming Java semantics). Now package B depends on package A and we have inverted the relationship. Informally, arrows between packages going down is good.
Note that package B implements and package A defines the interface Iy. Interfaces should usually be defined where they are used, not where they are implemented.
Generalizing Design
What can you do with packages on the same layer? Assume we have a package C which uses A and B. Within C the two components A and B shall interact. We also want A and B to be self contained. Neither of them should depend on the other or on C. For example, there could be D, E, F, and G which are like C but use different versions of A and B. Any direct dependency between A and B leads to problems in such a scenario.
A cheap solution is to introduce a common Base package.
The risk here is that it might not scale well for D, E, F, and G, because all need to use the same version if Base. You probably cannot update package A without package B if it requires an update of Base as well.
Think again.
We can apply the same trick of dependency inversion: A defines an interface for B and vice versa. Now C can create adapter classes for the interaction and thus there is no dependency between A and B.
D, E, F, and G can also create their own adapters and depending on which versions of A and B they use, everything just modifies its adapters.
I do concede that this feels overengineered. Especially when interface and corresponding class are nearly identical because then the adapter class consists of trivial methods which just redirect to a member. This means you have to consider the cost before you apply the pattern.
This is a generalization of Dependency Inversion which I would name "Dependency Abstraction" since we abstract the dependency to an interface.
The special case that "something external" is on a layer above is dependency inversion. If that "something external" is on a layer below, it is probably not be necessary. Well, you can stub the lower level for testing so it might still be useful. If "something external" is on the same layer define an interface and use an adapter on higher levels.
Generalizing Beyond Design
We can apply the Dependency Abstraction pattern also on higher levels of software development.
The highest level would be requirements. We can understand requirements engineering as dependency abstraction where we design an interface and then write code which implements it. Of course, requirements usually use natural language and checking if the interface matches is a manual error-prone activity but the principle is the same.
Between the requirements and the design is the software architecture (even if only documented informally or not at all). On this level we use the principle as well.
- For example, A and B could be microservices using each other. They should specify each others interface and C would be the orchestration where the actual adaptation happens.
- A and B could be plugins in Wordpress, Photoshop, Eclipse, Firefox, Outlook or whatever. Usually, plugins do not interact but when they do the situation becomes suddenly a lot more complex.
- A and B could be microcontrollers in a car or AUTOSAR components which communicate with each other. Well, in my experience the Base solution is usually used here: An OEM like Volkswagen controls and coordinates the communication between components. We are talking about safety-critical real-time systems here and the communication bus is a bounded resource. Still, dependency abstraction might reduce communication overhead between companies.
- A and B could be libraries used by an application C.
The tricky thing about the architectural level is that there is no clear definition of "interface" like in Java.