Key Takeaways
• Understand the importance of dependency injection and why it supports modular code design.
• Implement dependency injection with three different techniques.
• Review the benefits and drawbacks to each dependency injection technique.
What is dependency injection?
Dependency injection is a technique used to decouple classes that depend upon each other. Class dependency arises when once class uses the functionality (method) of another class 'as a service'.
Classes are deemed to have a hard dependency if their invocation creates a 'new' instance of another class.
Dependency injection falls under the broader design of 'inversion of control'. Inversion of control enforces the fifth principle of SOLID whereby a client's implementation is transferred from the class itself to a framework.
In the example below, the client, Student
has a hard dependency on the SchoolService
. The SchoolService is used to return a List of classes the student would have within the learn() method.
public class Student{
SchoolService schoolService;
public Student(){
schoolService = new ArtSchool();
}
public List<String> learn(){
return schoolService.getClasses();
}
}
The Student Object will create the ArtSchool at compile time, however a problem may arise if the we want the Student to instead attend the LiteratureSchool. The class will need to change its dependency implementation accordingly:
public Student(){
schoolService = new LiteratureSchool();
}
Why use dependency injection?
By using dependency injection, the application will delegate the responsibility of creating Objects away from the individual classes, and into a single framework.
The task execution is decoupled from implementation and a further degree of modularity is achieved.
Further benefits can be seen during unit-testing where components can be mocked and passed with dependency injection for more concise and light-weight tests.
Implementing dependency injection
Dependency injection design can be implemented by refactoring the Student class to reference a pre-existing instance of the SchoolService.
Dependency injection can be achieved in one of many way, including, but not limited to:
• Constructor injection: using the client constructor to take in the service as an argument.
• Spring constructor injection: using a Spring bean to inject the bean as a dependency.
• Spring setter injection: using a setter method within a Spring bean to instantiate the dependency.
Constructor dependency injection
A constructor parameter can take a dependency as an argument to be used by the class. Rather than the Student class defining the type of SchoolService it depends upon, the responsibility is delegated to the class that instantiates the Student instance:
public class Student{
private SchoolService schoolService;
public Student(SchoolService schoolServiceArgument){
this.schoolService = schoolServiceArgument;
}
}
From the main method, the SchoolService can be passed in as an argument:
public class Main{
public static void main(String[] args){
SchoolService myService = new ArtSchool();
Student myStudent = new Student(myService);
myStudent.learn();
}
}
Spring Constructor dependency injection
The Spring framework offers annotations that can inject dependencies into the class.
Before using Spring for dependency injection, it is important to ensure the annotation-driven injection is enabled by creating a configuration class:
@Configuration
public class Config {
@Bean
public SchoolService schoolService() {
return new ArtSchool();
}
@Bean
public Student student() {
return new Student(schoolService());
}
}
The @Configuration annotation indicates that the class declares bean definitions. The class is therefore processed by the Spring IoC container at runtime to create the beans.
The Bean schoolService
returns a new instance of ArtSchool
.
In the Student bean, the SchoolService is injected with the bean definition of ArtSchool. The Spring framework is now responsible for the schoolService injection, and not the Student class itself.
public class Student {
private SchoolService schoolService;
public Student(SchoolService schoolService) {
this.schoolService = schoolService;
}
...
The @Autowired annotation
The @Autowired annotation is used in Spring to implicitly inject dependencies into a class. It is not necessary to use the @Autowired annotation if the Student class only defines a single constructor. If more than one constructor is defined, @Autowired must be used to instruct the Spring container to inject the dependency into the class.
Spring setter dependency injection
Rather than using the constructor of the Student class to instantiate the schoolService, Spring bean definitions can call a setter method when defining the bean. In the Configuration class, the Student bean is updated to create a Student instance, to call the setter method, and to return the Student.
@Bean
public Student student() {
Student student = new Student();
student.setSchoolService(schoolService());
return student;
}
The Student class must now define the new no-argument constructor. If the Bean for the Student class was not defined, the dependency injection will no longer work with two constructors. Instead, the @Autowired annotation is necessary to ensure the schoolService is injected with the Bean when a Student is instantiated:
public class Student {
@Autowired
private SchoolService schoolService;
public Student(SchoolService schoolService) {
this.schoolService = schoolService;
}
public Student() {
}
Choosing a dependency injection technique
The constructor and setter dependency injection achieve the same objective in slightly different ways. The setter method can be seen as more readable against the constructor injection. Setter injections can also resolve a potential ObjectCurrentlyInCreationException where a circular dependency between two Objects arise.
The constructor injection, however, ensures that the Object is not created without the dependency injected. It may still be possible to create a Student class with an incomplete dependency if relying upon the setter injection. The constructor is also not liable to being overridden by child classes, while the setter method can be, introducing an implementation security concern.
Conclusion
Dependency injection is technique used to enable Objects and the services they depend upon to be created as separate entities.
This blog has reviewed three of the most common ways of implementing dependency injection, however there are other annotations such as @Resource and @Inject that can achieve similar outcomes and are worth exploring in Spring.
When using test-driven-development, or any kind of unit test, dependency injection can greatly reduce the weight and complexity of unit tests through mocking frameworks.
As well as improving code readability and reducing boilerplate code, dependency injection shifts the implementation of an application away from individual classes, and into a modular framework.
The code sample from this blog can be found on GitHub here.