Direct subsystems dependency – Architectural Principles

The preceding direct dependency example divided into packages, which have the same issue, would look like the following:

 Figure 3.3: direct dependency graph divided into packagesFigure 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:

  1. Create an abstraction assembly containing only interfaces.
  2. Create other assemblies that contain the implementation of the contracts from that first assembly.
  3. 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.

Then, if we divide the inverted dependency examples into multiple packages, it would look like the following:

 Figure 3.4: inverted dependency examples divided into multiple packagesFigure 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.

You may also like