Direct subsystems dependency – Architectural Principles
Figure 3.3: direct dependency graph divided into packages
The Core package depends on the SQL and Local packages leading to tight coupling.
Packages usually represent assemblies or namespaces. However, dividing responsibilities around assemblies allows loading only the implementations that the program need. For example, one program could load the Local assembly, another could load the SQL assembly, and a third could load both.
Enough said; let’s invert the dependency flow of those subsystems.
Inverted subsystems dependency
We discussed modules and packages, yet the example diagram of inverted dependency illustrated classes. Using a similar approach, we can reduce dependencies between subsystems and create more flexible programs by arranging our code in separate assemblies. This way, we can achieve loose coupling and improved modularity in our software. To continue the inverted dependency example, we can do the following:
- Create an abstraction assembly containing only interfaces.
- Create other assemblies that contain the implementation of the contracts from that first assembly.
- Create assemblies that consume the code through the abstraction assembly.
There are multiple examples of this in .NET, such as the Microsoft.Extensions.DependencyInjection.Abstractions and Microsoft.Extensions.DependencyInjection assemblies. We explore this concept further in Chapter 12, Layering and Clean Architecture.
Figure 3.4: inverted dependency examples divided into multiple packages
In the diagram, the Core package directly depends on the Abstractions package, while two implementations are available: Local and Sql. Since we only rely on abstractions, we can swap one implementation for the other without impacting Core, and the program will run just fine unless something is wrong with the implementation itself (but that has nothing to do with the DIP).We could also create a new CosmosDb package and a CosmosDbDataPersistence class that implements the IDataPersistence interface, then use it in the Core without breaking anything. Why? Because we are only directly depending on abstractions, leading to a loosely coupled system.Next, we dig into some code.