Dependency Injection (DI) is a design pattern that allows an object to receive its dependencies from an external source rather than creating them itself. This pattern promotes loose coupling and makes your code more modular and testable, and less error prone.
I had an opportunity to refactor DI implemention at my workplace.
What is Dependency Injection?#
Dependency Injection is a technique where the dependencies (objects) of a class are provided (injected) by Spring, typically through constructors, setters, or interfaces. DI helps in separating the creation of dependencies from the business logic, thereby adhering to the principle of Inversion of Control (IoC).
Types of Dependency Injection#
- Constructor Injection: Dependencies are provided through a class constructor.
- Setter Injection: Dependencies are provided through setter methods.
- Field Injection: Dependencies are directly injected into the class fields using annotations.
Problem statement#
Our first implementation relied on a tightly coupled instantiation of services into a component
public class Subsidiary {
String name;
Integer partyId;
List<String> ratings;
public void updateSubsidiary(List<String> ratings) {
this.ratings = ratings;
// update ratings in db
}
}
public class UpdateSub {
private SubsidiaryService subsidiaryService;
public UpdateSub() {
this.subsidiaryService = new SubsidiaryService();
}
public void processUpdates(List<String> ratings) {
subsidiaryService.updateSubsidiary(ratings);
}
}
// Main.java
public class Main {
public static void main(String[] args) {
UpdateSub UpdateSub = new UpdateSub();
UpdateSub.processUpdates(["AA+", "BB-", "CCC"]);
}
}
As visible here, we are creating an object of SubsidiaryService inside UpdateSub and instantiating it.
Challenges with the above approach?#
-
Tight coupling : Since we create and instantiate the object of SubService manually, it is coupled to the business logic of the UpdateSub itself. Should SubService be made into an interface, we'd need to update the logic inside UpdateSub to instantiate the impl of the interface.
-
Challenge during testing : When writing junits, we don't need to actually create database connections, rather, just mock them. However, in this case, when we call UpdateSub, it'd end up updating the database connection and it won't be possible to mock it
Stage 1 of solution : Field injection#
To cater to the above limitations, we decided to implement dependency injection. But we decided to go with a type called field injection.
As the name suggests, we inject dependencies as a field of the class.
In code, it looked something like this
public class UpdateSub {
@Autowired
private SubsidiaryService subsidiaryService;
public UpdateSub() {
}
public void processUpdates(List<String> ratings) {
subsidiaryService.updateSubsidiary(ratings);
}
}
Just by writing the @Autowired
annotation, we were able to inject the SubService dependency. Now, our junits could mock SubService using @InjectMocks
or @Mock
from Mockito.
Stage 2 : Limitations of @Autowired#
There are a couple of limitations with this approach
- You cannot make the injected service immutable
Since @Autowired
will inject the service after the instantiation of UpdateSub, setting it as final will throw a compile time error. This is a challenge when we want to make sure our injections aren't overridden
- Chances of NPE
Again, owing to the above reason that injection happens after the root class instantiation, we found null pointer exceptions because we were trying to access the method of a service that spring hadn't yet been able to instantiate
- The partial accuracy of inject mocks
In the junits for UpdateSub, if we want to mock UpdateSub, we'd need to mock SubService and pass it along to UpdateSub. We achieved this by @Mock
ing SubService and @InjectMock
ing
the mock into UpdateSub, but that didn't feel like the right approach
Solution - Constructor injection#
We therefore decided to move to Constructor injection
As the name suggests, we inject services into the constructor, rather than as a field.
Here is how it looked like in code :
public class UpdateSub {
SubsidiaryService subsidiaryService;
@Autowired
public UpdateSub(Subsidiaryservice subsidiaryService) {
System.out.println("Update Sub");
}
public void processUpdates(List<String> ratings) {
subsidiaryService.updateSubsidiary(ratings);
}
}
Here, we autowire the constructor and pass the dependency as a param. The advantage of this is that the dependency will be initialized when object of UpdateSub is created, thus solving the null pointer concerns of above
We can even do away with the explicit @Autowired
annotation when there is just one constructor, as is the case above, since Spring handles the initiation during the constructor invocation.
This, considering all factors, seems to us, the most useful and recommended implementation of Dependency injection
Advantages of Dependency Injection#
To summarize, following are the advantages of DI
- Loose Coupling: DI reduces the coupling between classes, making the system more flexible and easier to maintain.
- Easier Testing: Dependencies can be easily mocked or stubbed during unit testing, leading to more isolated and reliable tests.
- Improved Code Readability: DI promotes clean code practices by clearly defining dependencies and their relationships.
- Enhanced Maintainability: Changes in dependencies require minimal changes in the dependent classes, making the system more maintainable.
- Increased Reusability: DI encourages the use of interfaces and abstract classes, enhancing the reusability of components.
Conclusion#
Dependency Injection is a powerful design pattern that improves the modularity, testability, and maintainability of your code.