SOLID principles are core software design principles, intended to make software design more understandable and maintainable. Every letter stands for a concept and below is the list of five concepts:
- Single Responsibility, there should never be more than one reason for a class to be changed
- Open-Closed Principle, the class should be open for extension but closed for modifications
- Liskov Substitution Principle, meaning any pointer to a base class should also be able to use the objects of derived classes, without knowing it
- Interface Segregation Principle, multiple client specific interfaces are always better than one big fat interface for a class
- Dependency Inversion, meaning rely on abstractions instead of relying on the concrete implementations
In this article, we are going to have a closer look at what is dependency inversion and how it can be used in .NET applications.
Scenario
When we write code for any application, usually it might involve multiple classes (Single Responsibility Principle from SOLID). So, let’s say we have a
Controller
class, which is dependent on aBusinessLogic
classBusinessLogic
class is dependent onRepository
classRepository
class might be dependent on some classes from .NET
We will use this scenario to discuss the next two concepts.
Dependency Tree
Most of the real world applications are written in this way, the direction of compile time dependencies flows in the direction of runtime execution. So, these dependencies can be plotted as a tree as shown below. This tree is called as dependency tree.
What is the issue ?
So, what is the real issue in above example ? The issue is, the consumer class need to know about all dependencies at compile time.
Why is this an issue?
If there is only one developer working for the application, the issue might never surface. But in case of real world application this is never the case.
Now a days, there is more focus on making the application cloud native and making the application modular, where each module can be fairly independently deployed. In such setup, there might be multiple teams working on different modules.
The compile time dependency on the concrete implementation can affect the development velocity as teams cannot continue in parallel.
The compile time dependency may also result into lots of issues just for maintaining the application. Let’s say a class was created to support external identity provider, let’s say, google, for authentication. Now, if you want to change the external identity provider, to let’s say facebook, then it might be very expensive change, if the assumptions made in the original class design, are valid or not.
Also, what if we want to unit test the consumer class ? How would we make the consumer class use the stub in place of actual dependency ?
Inverted Dependency Tree
Now for the same scenario. But instead of directly adding reference to a concrete implementation, let’s say an interface is extracted. So
CoreLogic
class, which is dependent on an interfaceIRepository
class to get the data from database, andIFileLogger
class for logging. These interfaces document all the assumptions between consumed classes and the consumer class and these interfaces are controlled mostly by the consumers.FileLogger
class might be dependent on some class (or classes) from .NET and it implements theIFileLogger
interface.Repository
class might be dependent on some classes from .NET, and it also implements theIRepository
interface
So, now CoreLogic
class would control the interfaces (because of interface segregation principle stated above).
And Repository
and FileLogger
will have to make sure they follow the same interface. Thus, the name, dependency inversion.
So, the compile time dependency tree would change as shown below. But run-time dependency would be still the same. But because of interfaces, we have now additional advantage of replacing the concrete implementation without having to do any code changes in consumer classes.
Why Inverted Tree is Better ?
Below are some of the high level reasons
- To decouple the execution from implementation. In above example, controller class will just use interface without knowing the concrete implementation, which can be resolved at run time.
- To free the classes and modules from assumptions. The assumptions are now in the interface.
- To make the code more maintainable,
- To prevent the side-effects while replacing the module. Replacing module is very easy as long as same interface is being used by the new replacement.
Dependency – Inversion vs Injection
Dependency Inversion
is a design principle. It is a guideline for decoupling modules from one another. The actual implementation of that principle might vary.
There are different implementations which are in use today. Some of them include:
- Service Locator Pattern
- Dependency Injection in .NET
- We can write custom implementation using Strategy pattern
Thus, Dependency Inversion
and Dependency Injection
are not same things. Dependency inversion is a principle and Dependency injection
is a way in which the dependency inversion principle
is implemented.
Dependency inversion is also known as inversion of control (IoC) pattern.
Inversion of control is sometimes facetiously referred to as the “Hollywood Principle: Don’t call us, we’ll call you”.
How to apply in .NET apps ?
Dependency injection is the first class citizen in .NET.
Below is the stepwise process for using dependency injection:
- Create interface (or base class) as an abstraction
- Create the concret implementation
- Registration of the dependency in a service container. The .NET provides a built-in service container, IServiceProvider.
- Services are typically registered at the app’s start-up, and appended to an IServiceCollection. Once all services are added, you use BuildServiceProvider to create the service container
- Inject the dependencies via constructor. Framework takes care of creating object and destroying them when they are not needed anymore.
The lifetimes Scoped, Transient and Singleton are supported. If it is a .NET Core web application, it can be very easy to register the dependencies. We just need to call AddScoped, AddTransient, AddSingleton methods in ConfigureServices
method of Startup
.
Below example shows the usage.
I hope this post was useful. Let me know your thoughts.
Pingback: The Code Blogger - Dependency Injection In .NET – Basic Terminology