As developers, the term "dependency injection" is probably not new to you. However, many developers use it without understanding how it works and why we need it.
So, in this article, I will discuss the top 5 reasons to use dependency injection, including practical examples, to help you decide when to use dependency injection in your project. I'll also talk about one major limitation that shouldn't be swept under the rug.
Dependency Injection (DI) Explained
When you rely on something, we use the term "Depend" to express it. Likewise, if your code relies on another implementation or another module, you can use the technique "dependency injection."
There are 3 main types of dependency injection:
1. Constructor Injection
In this method, a class constructor is used to provide dependencies. The below code snippet shows a simple example of constructor injection.
@Component
class Account {
private Category category;
//dependencies are provided as argumentsAccount(Category category) {
Objects.requireNonNull(category);
this.category = category;
}
Category getCategory() {
return category;
}
}
2. Setter Injection
In this method, a setter method is used to provide dependencies to a class. The below code snippet shows a simple example of a Setter injection.
@Component
class Country {
privateFestival festival;
//dependencies are provided as arguements to setter method@AutowiredvoidsetFestival(Festival festival) {
this.festival = festival;
}
FestivalgetFestival() {
return festival;
}
}
3. Field Injection
In this method, annotations are used, and dependencies are directly provided to fields using annotations. The below code snippet shows a simple example of field injection.
@Component
class Engine {
//injecting dependency via annotation to the field
@AutowiredprivateFuel fuelType;
voidFuelgetFuelType() {
return fuelType;
}
voidsetFuelType(Fuel fuelType) {
this.fuelType = fuelType;
}
...
}
Why is Dependency Injection useful?
Even though the dependency injection is mostly underrated among developers due to one limitation, the advantages of using this technique are highly effective.
Let's see why you should consider using dependency injection and where you need to be cautious when using the dependency injection below.
1. Highly Extensible Code
Your codebase is expected to evolve over time, and you'll often (or even constantly) have to fix bugs and defects. However, let's suppose you're using dependency injection because of its loosely coupled nature. In this case, you can improve your application quickly with far less effort.
On top of this advantage, there's another benefit due to the high extensibility of code. Because of the externally injected dependencies, developers can scale up the application without worrying about managing dependencies manually for each functionality. Instead, they can code simultaneously, making the development phase more efficient.
2. Highly Testable Code
Dependency injection helps to develop testable code, allowing developers to write unit tests easily. You can use mock databases with dependency injection, and test your application without affecting the actual database.
Here's an example. Suppose you want to test business logic, but it needs a database connection to execute. That's a pretty time-consuming and relatively complicated task. However, suppose you can mock the required database and isolate the actual database from the unit tests. In that case, your unit tests will become more reliable and reduce the unit testing effort a lot.
Watch React.js: The Documentary here. What did the creators of the most popular frontend framework have to say today?
3. Highly Reusable Code
The loosely coupled structure of code using dependency injection makes it easier to reuse business logic implementations in different locations around your codebase.
Suppose you need to pass an external database connection access to a method to read data from your database. You can follow the dependency injection technique and create a plug-and-play type of database access module. You can only inject this module to where it is required.
Great ZSH Terminal Plugins.
This allows you to implement more modularised code and promote reusability without any code changes.
4. Highly Readable Code
According to the dependency injection technique, you can mention all the required dependencies in a single file, which acts as an interface between the application's components. By doing so, all the dependencies are getting isolated from the actual implementation of the business logic.
Therefore, you do not need to go through all the code to figure out dependencies in your code. Instead, they're kept in a centralised place that can be easily referenced. All you have to do is check the interface. This also makes your actual implementation much more readable.
The below example shows how a repository can be injected as an external dependency. You can see that the IStudentRepository
acts as an external interface to the StudentService
class.
import { IStudentRepository } from'./StudentRepository.ts';
classStudentService {
privatereadonlystudentRepository: IStudentRepository;
//dependency injectionpublicconstructor (studentRepository: IStudentRepository {
this.studentRepository = studentRepository;
}
publicasyncfindStudentByIndex(index: string): Promise<Student> {
returnthis.studentRepository.findStudentByIndex(index);
}
}
In the above implementation, dependencies are isolated from the class, and you can't refer to the dependencies directly. However, you can navigate to the relevant interface and check the needful. For example, the code of the dependency may look like the below.
import { databaseDriver } from'pg-driver';
//interface exposed to use the dependencyexportinterfaceIUserRepository {
addStudent(student: Student): Promise<void>;
findStudentByIndex(index: string): Promise<Student>;
}
exportclassStudentRepositoryimplementsIStudentRepository {
publicasyncfindStudentByIndex(index: string): Promise<Student> {
//method implementation
}
}
This is a simple use case, but imagine having several dependencies to work on a single class. You will end up having large chunks of unnecessary codes within your class. Using dependency injection in these kinds of situations will be a lifesaver, helping to organize your codebase and make it more readable.
5. Highly Maintainable Code
Code maintainability is all about evolving the application, fixing bugs and defects, and feature enhancements. Using a loosely coupled design provides high maintainability for any project. If you use dependency injection correctly, your code becomes loosely coupled, making it more maintainable.
This is another tremendous advantage because having highly maintainable code will drastically reduce the total cost of ownership of the code as well as the effort required to maintain the code in the long run.
A Word of Caution Regarding Dependency Injection
Using dependency injection can be incredibly rewarding, but if you're a new developer, you should be especially careful, otherwise, you might create unexpected errors in your application.
If you're planning to inject an external dependency, you should have a good understanding of both dependency and the framework to ensure they are compatible. If not, there can be significant errors during the application runtime even if it gets compiled successfully.
As developers, we don't want to have surprises like that during application runtime, so (regardless of how cool dependency injection is), this is a significant concern.
Final Thoughts
To summarise, dependency injection can be really, really rewarding. It's much easier to manage the codebase and work together with multiple developers. Furthermore, the project's business logic implementation will be more readable, maintainable, and extensible. Meanwhile, your dependencies can be maintained separately as well.