Maintaining code is often a repetitive and unappreciated job.
The code you write today will be good for a time, but your code will eventually rot.
It will become brittle, difficult to maintain, and difficult to comprehend. Moreover, the stress of maintaining code will only increase as the number of your dependencies grows. That is until dependency injection (DI) comes to the rescue. DI is a design pattern that – when used correctly – will keep your code healthy for the long haul.
Dependency injection (DI) is a popular software design pattern, used to reduce the tight coupling between the components of a system. Dependency injection ensures that the dependencies needed for a particular object are provided to the object, rather than the object creating them himself. This is a powerful technique for breaking up a big class into smaller, more manageable chunks of code.
This article will look at dependency injection, how it helps you with your code, and how to implement dependency injection in C#.
Definition of dependency injection in C#
In the current software engineering world, there is a heavy trend towards micro-services. The idea is that you should not create one massive service with everything in it but rather create lots of small, modular micro-services that can be combined to create larger solutions.
Well, the same thing of isolation between classes should apply to your codebase. And this is where the dependency injection design pattern plays an important role. Dependency injection (or dependency inversion) is a process of creating objects without specifying every detail of how to create their dependencies. It’s a way of making code more modular and reusable. In other words, you split your code into small classes and use dependency injection to provide necessary dependencies to the client class.
The most common way to abstract classes from each other is to use interfaces to define dependencies.
Dependency injection helps you to create clean, testable, and reusable code without resorting to the hacks of static methods, the global variables of singleton objects, or the magic of the global scope. If you’re a developer, it’s not unusual to come across dependency injection. It’s a popular design pattern in software development that many developers use to manage dependencies and promote code reuse.
How do you explain dependency injection to a 5-year-old?
Dependency injection is when something relies on something else to work. For example, let’s say you are a software developer but need to use the helicopter to go to a particular place in the hills. Now, you could learn how to drive a helicopter. But that is a very long and complicated process.
Instead, if you hire a pilot to drive the helicopter, you can go to the destination much faster. Not to mention it’s more secure to have an experienced and specialized person drive the helicopter. In that case, you rely on the pilot to achieve the goal of going to the hills.
The same is with the code. You could have one class that does everything (please don’t do that). And that class would be very complex. But you can also have lots of small classes. Each specialized in doing one thing. If they need to talk to each other, use dependency injection.
What problems does it solve?
To understand why dependency injection is important, let’s take a look at the following class.
public class WeatherService
{
public WeatherService()
{
var weatherClient = new WeatherClient(new HttpClient(),
new Logger());
}
}
In this case, WeatherService
class has a class dependency on WeatherClient
. WeatherClient
has two dependencies: HttpClient
and ILogger
. There are two problems with this code:
- WeatherService depends on a concrete class, instead of an interface –
WeatherService
depends on aWeatherClient
concrete class. This means there is no easy way to replaceWeatherClient
with another class with the same responsibilities. But instead, ifWeatherService
depends on an interface, it would be easy to swap different interface implementations. That means thatWeatherService
wouldn’t have to worry about which concrete implementation of theIWeatherClient
it uses. - WeatherService creates the dependency, so it knows all about it –
WeatherService
creates the instance of theWeatherClient
. This means that it has to know what dependencies it uses to perform its job. This is another problem with the code. If theWeatherClient
class has to change and take another dependency, theWeatherService
has to change as well. This is a symptom of a dirty code.
These are the problems that dependency injection aims to eliminate.
Why is dependency injection needed?
It is impossible to predict how the code will look like in the future while it’s being developed. In cases where several developers work on the same project, it may not be easy to identify the dependencies for a particular class. This is because they may have been hard-coded. This can lead to a series of errors and other issues arising.
Like in the previous example, dependencies are often hard-coded into the code. Again, this is a problem because developers have to understand the bigger part of code to debug and maintain it.
The solution is dependency injection.
Here are three good reasons to use dependency injection:
- To improve maintainability – Dependency injection is a technique that improves the maintainability of the code by separating the construction of a class from its use. A class doesn’t create its dependencies. Instead, they are provided externally, often as arguments to the constructor or passed to a method. In addition, dependency injection inspires the programmer to write modules that are more independent and loosely coupled.
- To improve readability – Poor code readability is one of the most prevalent problems found in software engineering. Dependency injection is an effective way to improve readability by ensuring that the parts of the system are decoupled. It makes the code easier to read because it separates one class and its functionality from related objects, making the code more lightweight and simple.
- To improve testability – The testability of the code is always a major concern for programmers. When the code is poorly tested, it may lead to errors in the production environment. Dependency injection can be used to make unit testing easier by removing dependencies that are difficult to replace. DI allows for different interfaces and implementations to be swapped out when testing, facilitating more thorough tests. This means you can easily inject a stub or a mock object. To start, a mocking library is necessary to provide some structure and simplify test implementation. One of the most popular mocking frameworks you can use is Moq.
The reasons mentioned above ultimately boil down to loose coupling.
What is loose coupling in software design?
Software design is all about managing complexity. The easiest way to do that is to break complex systems into smaller, more manageable chunks. Loose coupling in software design ensures that the components of an application are not dependent on each other.
The components can still work together but can also operate independently. In addition, loosely coupled code is easier to maintain and test. The easiest way to promote loose coupling is by using interfaces instead of concrete classes.
Why is this a good thing?
This is important because if a component depends on too many other components in a system, it has to deal with the complexity of those other components. On the other hand, if you have loosely coupled parts, you can make changes in one part of a system without impacting the other parts.
How to use dependency injection?
There are several ways to achieve a dependency injection pattern. Most often, you will inject the dependencies through the constructor. But that’s not the only way to do it.
There are three types of dependency injection:
- Constructor injection
- Property injection
- Method injection
Let me explain each type briefly.
What is the constructor injection?
The constructor injection is a way of injecting dependencies into the constructor of a class. This is usually the default type of injection.
The idea behind this type of dependency injection is that the constructor takes care of all the hard work of creating the dependency objects. But that’s just the beginning. When you inject all dependencies while creating a new instance of an object, you ensure that the object from the start has all the necessary dependencies to function properly.
The following example shows the constructor injection.
class BookController : IBookController
{
private readonly IBookService _bookService;
private readonly IOrderService _orderService;
public BookController(IBookService bookService, IOrderService orderService)
{
_bookService = bookService;
_orderService = orderService;
}
}
The BookController
class has two dependencies, IBookService
and IOrderService
. They are injected through the constructor of the class. We are injecting interfaces so that we can easily swap the implementations at runtime. If that’s necessary.
What is the property injection?
The second type of dependency injection is property injection. With the property injection, the client class gets the necessary dependency through its property. While constructor injection is the most common type of injection, property injection can also be useful in some cases. You can use it in three cases:
- You don’t know what instance of the dependency pass at the time of creating the dependent class.
- You want to change the injected dependency at runtime.
- You have some optional dependencies that you might or might not use.
The following example shows the property injection.
class BookController : IBookController
{
public IOrderService OrderService { get; set; }
}
In this case, IOrderService
dependency is obtained through the property of the class. The alternative of using a property of a class is to use a setter method, which you use to pass the dependency.
What is the method injection?
The third form of the injection is the method injection. The goal of method injection is to inject a required dependency as a method parameter. Usually, this means that the dependent object uses the dependency only in the current method.
The following example uses the method injection.
class BookController : IBookController
{
public void OrderBook(IOrderService orderService)
{
}
}
As you can see, the IOrderService is the parameter of the OrderBook method.
IoC (Inversion of Control) Container
A codebase can be complicated when it consists of many classes. Which is almost always the case. If you work with an IoC container, it can help with easing those complications.
Have you ever been curious about the difference between an inversion of control container and a dependency injection container? Inversion of control (IOC) and dependency injection (DI) inject dependencies into code. But here’s the difference: inversion of control containers take care of dependency injection for you.
Why do you need an IoC container?
An IoC container can help with separating code and managing dependencies. It helps to have a simple yet powerful framework that separates and maintains dependencies needed for the project. The most popular IoC containers used today are free and open-source. It’s also easy to use and configure them.
There are many useful features of the IoC container that make it a great tool when building a complex system. It’s an easy way to keep your infrastructure code separate from your application code. They help you retrieve the object of any class without needing to know how to instantiate the dependencies.
What IoC container libraries exist?
There are many different IoC containers for different languages, frameworks, and platforms. The most popular IoC container libraries used in C# are:
Advantages and disadvantages of Dependency Injection
Dependency injection has many advantages. But, unfortunately, it also has some minor disadvantages. Let’s go through each of them.
What are the advantages of dependency injection?
Dependency injection has the following advantages:
- Enables clean code – with DI, the classes don’t have to instantiate their own dependencies, which leads to simpler code.
- Enables unit testing – if you use DI, you can easily swap the dependencies in the unit test with fakes.
- Encourages decoupling – you decouple one class from another by injecting the interface instead of a concrete class.
- Improves code maintainability – by relying on interfaces, you can easily change the implementation of a particular feature. And the rest of the code stays the same.
- Centralizes code configuration – when you use the dependency injection framework, such as Autofac, you can set up your whole dependency graph in a single file.
What are the disadvantages of dependency injection?
Dependency injection can be an effective technique for adding additional complexities to your code, but there are disadvantages. It is a big topic that can be difficult to grasp. You’ll have little to no understanding of the process if you don’t have the right experience.
Also, the DI framework can impact performance. If the object is complicated to set up and has a lot of dependencies, it might take some time to create a new instance. However, this performance overhead is not something that should stop you from using the dependency injection.
Summary
As you can see, using dependency injection is a good practice because it makes the development of software easier. DI also improves the quality of code as well as makes the code more testable. Maybe it can be difficult to start using the dependency injection, but you will soon use it with ease once you try it.
If you need more information on why dependency injection is useful and awesome, check out the post about DI benefits.