Dependency inversion

The D in SOLID stands for dependency inversion. It’s a simple, but powerful principle that allows to change the direction of dependencies within software and often enables the generalization of a concept, by depending on abstraction.

What I like most about the principle is that it can be used to structure and guide the architecture of the modules and components of a software system on a high level, but not also that, it can also be implemented through various software design patterns on the lower level.

Classes, Modules, Components…

%%{init: {'theme':'neutral'}}%% classDiagram namespace Component1.ModuleA { class A2 class A1 } namespace Component1.ModuleB { class B } namespace Component2.ModuleC { class C } A2 --|> A1 A1 --> B B <-- C

Relationships, and therefore dependencies exist on various levels of granularity: Classes and functions make up modules and modules form components. As we can see, the relationships of the classes also define the relationship of the higher-level constructs.

Dependencies between modules

In the example above, Module A might be a “high-level” module. Meaning, that a module, i.e. contains business logic, whereas Module B might be a module that contains “low-level” logic that implements a client for an external Service in Component 2.

As a guiding rule for DI, a high-level module shouldn’t depend on a low-level module, since both might change independently at any time. Instead, both modules should depend on abstractions. The abstraction also should not depend on details.

A. HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES. BOTH SHOULD DEPEND UPON ABSTRACTIONS.

B. ABSTRACTIONS SHOULD NOT DEPEND UPON DETAILS. DETAILS SHOULD DEPEND UPON ABSTRACTIONS

Source: objectmentor.com.

%%{init: {'theme':'neutral'}}%% graph LR A[High-level Module] -->|depends on| B[Abstraction] B -->|is implemented by| C[Low-level Module]

The dependency direction can be changed, by applying the dependency inversion principle: the high-level module depends on an abstraction like an interface or abstract class, whose implementation is provided by a low-level module.

%%{init: {'theme':'neutral'}}%% classDiagram direction LR namespace ModuleA { class A1 class A2 class I1 } namespace ModuleB { class B } I1 <|-- B A1 <|-- A2 I1 <-- A1

In the example above, the dependency between Module A and Module B was inverted, by introducing an interface I1, on which A1 and B depend. A1 doesn’t need to know anything about B though, since B provides any implementation of I1.

By setting up dependency rules, DI can be used to structure the architecture of a software system as it is described in the “clean architecture” or “hexagonal architecture”. Btw, I’m always a bit doubtful, when someone coins a term that implies that the proposed concept has the suggested quality, that’s why I don’t like the term “clean architecture”.

Nevertheless, it is easy to take that concept to the next higher level, to define the dependency between components.

Dependencies between components

Plugin

To make software flexible, it is often desirable to add a plugin interface so that other use-case-specific code can be loaded dynamically and put into its independent package.

This is an example where DI is applied on the component level, by providing an interface on high-level component A, while component B implements the interface and provides some specific functionality.

%%{init: {'theme':'neutral'}}%% classDiagram namespace C1.PlugUseMod { class A1 class A2 } namespace C2.PlugModA { class B } namespace C3.Interface { class I1 } A2 --|> A1 A1 --> I1 B --|> I1

Dependencies at the lower level

Ultimately, the dependency between the lower-level constructs defines the dependency of the high-level constructs. Of course, not all programming languages have the concept of classes, let alone inheritance, but most of them allow to define a contract via an interface-like concept and at least work with functions on the module level and the DI principle can be applied in an analog fashion.

Class dependencies

In the case of classes, there are various ways in which a class A dependent on another class B:

  • Class A has members of type B
  • Class A has a method which requires an object of class B
  • Class A inherits from Class B

The last example forms the strongest dependency since all methods and members are inherited directly. However, inheritance is not necessarily needed.

DI and design patterns

Dependency injection

I mention dependency injection here since it is often confused with dependency inversion, but after all, the former can be used to enable the latter. It is a technique to enable inversion of control.

This means that the object of class A is not instantiated within class B instead, it is injected from outside of class B so that class B is not responsible for creating an instance of class A.

%%{init: {'theme':'neutral'}}%% classDiagram class A { +operation(): void } class B { -dependency: A +constructor(dependency: A) +execute(): void } B ..> A : "uses"

By applying dependency injection, A can be independently created, while B still depends on A. Before that B created A.

Dependency injection is often very helpful in making classes testable, as it is quite easy to create an instance of class A with some test values.

Depending on abstractions

Now, in the example above, one still could say that B depends on the concrete class A, something that is against the DI principle. But of course, A can again be replaced by an interface, which is implemented by A. And voila, B doesn’t directly depend on A anymore.

%%{init: {'theme':'neutral'}}%% classDiagram class A { +operation(): void } class B { -dependency: I +constructor(dependency: I) +execute(): void } class I { +operation(): void } I <|-- A B ..> I : "uses"

Strategy pattern

Now this looks already very similar to the “strategy” behavioral design pattern. A pattern that can be used to implement the mentioned Plugin architecture on the component level.

%%{init: {'theme':'neutral'}}%% classDiagram class Context { - strategy +setStrategy(Strategy) +executeStrategy(): void } class IStrategy { +execute(): void } class ConcreteStrategyA { +execute(): void } class ConcreteStrategyB { +execute(): void } Context ..> IStrategy ConcreteStrategyA --|> IStrategy ConcreteStrategyB --|> IStrategy

In the strategy pattern one can see that dependency injection via a setter is applied, as well as dependency inversion by depending on an interface IStrategy.

A similar reliance on abstractions can be seen in the even more simple “template method” pattern.

%%{init: {'theme':'neutral'}}%% classDiagram class AbstractClass { +templateMethod(): void #primitiveOperation1(): void #primitiveOperation2(): void } class ConcreteClassA { #primitiveOperation1(): void #primitiveOperation2(): void } AbstractClass <|-- ConcreteClassA

Conclusion

As shown, by relying on abstractions on a lower level the direction of dependencies between classes, modules, and components can be changed. Some basic design patterns can help us to implement dependency inversion. Moreover, since dependency inversion allows us to arrange dependencies on various levels, it enables us to implement and enforce architectural rules to make classes, modules, and components more loosely coupled and independent from low-level changes.