Date and Time: 2023-10-17 21:36 Tags: CSharp, Design Patterns, Dependency Inversion Principle, Depend on Abstraction
Dependency Injection
Intro
The concept of Dependency Injection revolves around the idea of “dependencies.” In the context of programming, a dependency is when one object relies on another to perform its function. Traditionally, an object would create or find its dependencies internally, but this leads to a tightly-coupled design that’s hard to manage and test.
Dependency Injection addresses this by having dependencies provided to the object (or “injected”), typically through the object’s constructor, a method, or a property. This way, the object isn’t responsible for finding or creating its dependencies, leading to a more modular and flexible design.
Types of Dependency Injection
The three primary types of Dependency Injection are:
Constructor Injection:
The dependencies are provided through a class constructor. This is the most commonly used and the most recommended form of dependency injection.
In this example, MyClass
is dependent on ILogger
. The ILogger
dependency is injected via the constructor and can be easily replaced with any class that implements the ILogger
interface.
Setter Injection:
The client exposes a setter method that the injector uses to inject the dependency.
In this example, the ILogger
dependency is injected via the Logger
property.
Interface Injection:
The dependency provides an injector method that will inject the dependency.
Interface injection requires the dependent class to implement an interface that will be used to provide the dependency.
Here’s an example:
In this example, MyClass
implements the ILoggerSetter
interface to allow ILogger
dependency injection.
Each of these types has its uses, which we will explore further in the implementation section.
Inversion of Control
Inversion of Control (IoC) is a broader design principle that Dependency Injection falls under. It involves inverting the flow of control in a system, meaning that the framework or container calls the custom, user-written code, rather than the other way around.
In the context of Dependency Injection, IoC means inverting the control of managing dependencies. Instead of each object controlling its dependencies, this responsibility is given to an external entity (an IoC container). This external entity creates and wires up dependencies where they are needed.
Example
Standard Practices
The implementation of Dependency Injection in C# aligns with several standard practices. The examples previously given demonstrated how dependencies are injected through constructors, setters, or interfaces. However, there are further aspects to consider:
Suppose we have a NotificationService
class which is a high-level module in our application. This class depends on a EmailService
class which is a low-level module for sending notifications.
In the above code, NotificationService
is tightly coupled with EmailService
. This a bad practice because it violate Dependency Inversion Principle. And if we decided to add a new SMSService and decide to use it in NotifacationService
we have to change the code of NotifacationService
and it will violate another Open Closed Principle
We can solve this by Depend on Abstraction rather than depending directly on EmailService
. Let’s define an interface INotificationService
, and let the EmailService
implement this interface.
Now, NotificationService
depends on the abstraction INotificationService
, not on the low-level module EmailService
. If we need to change our notification method, we just need to create a new class implementing INotificationService
, for example, SMSService
, and inject it into NotificationService
. The NotificationService
class itself doesn’t need to change.
This decouples the high-level module from the low-level module and makes the system more modular and flexible.
Simple Example
Let’s consider a common scenario: you have a UserService
class that needs access to a UserRepository
to perform database operations. Without Dependency Injection, you might instantiate the UserRepository
inside the UserService
class. With Dependency Injection, you inject the UserRepository
into the UserService
from the outside. Here’s how it looks in code:
In the second example, the UserService
class’s dependency on IUserRepository
is injected through the constructor, promoting a more flexible and testable design.
Dependency Injection Containers
A Dependency Injection Container, also known as an IoC (Inversion of Control) Container, is a framework for implementing automatic dependency injection. It manages object creation and injects dependencies when required, making it easier to implement Dependency Injection in a consistent manner throughout an application.
.NET Core has built-in support for Dependency Injection and comes with its own lightweight IoC container. However, if you need more features, there are other more powerful containers available like Autofac, Ninject, and Unity.
In this example, the Startup
class has a ConfigureServices
method. This is where you configure the application’s services. In the method, a ConsoleLogger
is registered as a service that can fulfil the ILogger
dependency whenever it’s required. The AddTransient
method specifies that a new ConsoleLogger
instance should be created each time the ILogger
service is requested.
Benefits
Now that you have a basic understanding of Dependency Injection, let’s explore the benefits it offers to C# developers.
Testability
Dependency Injection simplifies unit testing by allowing you to easily substitute real implementations with mock objects or test doubles. In the absence of DI, testing can become challenging due to the tight coupling of components.
Consider you want to write a unit test for the UserService
class in the previous example. With Dependency Injection, you can pass a mock IUserRepository
to the constructor, allowing you to control the behavior of the repository for testing purposes. Without DI, testing might require complex workarounds or database interactions.
Reusability
DI promotes code reusability by making it easier to swap out components or extend functionality. For example, you can change the database provider or add caching to your application by creating new implementations of the interfaces and injecting them without affecting existing code.
Let’s say you decide to switch from using a SQL database to a NoSQL database. With Dependency Injection, you can create a new NoSqlUserRepository
implementing IUserRepository
and inject it into the UserService
without changing the UserService
code.
Maintainability
Dependency Injection leads to more maintainable code. When dependencies are explicitly injected, it’s easier to understand the relationships between classes and their dependencies. This clarity in the codebase simplifies maintenance, troubleshooting, and debugging.
Flexibility
DI enhances the flexibility of your application by making it easier to configure and adapt to different environments. You can configure the dependency injection container to provide different implementations based on configuration settings or other factors.
Conclusion
By understanding Dependency Injection and implementing it in your C# applications, you can create more modular, maintainable, and testable code while gaining the flexibility to adapt to changing requirements. It’s a powerful design pattern that every C# developer should have in their toolbox. When used correctly, Dependency Injection can lead to more reliable, scalable, and maintainable software. So, consider making it a core part of your development practices.
Reference:
https://artemasemenov.medium.com/mastering-dependency-injection-in-c-best-practices-pitfalls-and-future-trends-61189ad97f25 https://lewisjohnbaxter.medium.com/understanding-dependency-injection-in-c-567e65701a34