Dependency Injection in Spring
Before we talk about dependency injection, we need to understand the concept of Inversion of Control (IoC).
What Is Inversion of Control?
Inversion of Control (IoC) is a software engineering principle where control of objects or parts of a program is transferred to a container or framework. It enables frameworks to control program flow, calling custom code, and is commonly used in object-oriented programming.
IoC decouples task execution from implementation, facilitates switching between implementations, enhances program modularity, simplifies testing by isolating components or mocking dependencies, and enables communication through contracts. IoC can be achieved through mechanisms like the Strategy pattern, Service Locator pattern, Factory pattern, and Dependency Injection (DI).
What is Dependency Injection?
Dependency Injection (DI) is a pattern used to implement Inversion of Control (IoC) by delegating the responsibility of setting an object's dependencies to an external assembler. Objects are connected or "injected" into each other by an external entity rather than the objects themselves. Dependencies refer to objects required by a class to perform its operations, while injection is the process of providing these dependencies to an object. DI facilitates IoC by shifting the responsibility of object creation and dependency injection to the framework (e.g., Spring) rather than the class itself. It can be implemented using constructor-based injection, setter-based injection, or field-based injection.
Benefits of Dependency Injection (DI)
Dependency Injection (DI) offers several benefits that contribute to the improvement of software systems. It enables loose coupling, enhances modularity, and facilitates testing. Here's why DI is advantageous:
In summary, dependency injection aligns with sound software design principles such as encapsulation, separation of concerns, and single responsibility. This results in more maintainable, flexible, and testable codebases.
The IoC Container in Spring
In Spring, the Inversion of Control (IoC) container is encapsulated by the ApplicationContext interface. This interface is tasked with configuring, instantiating, and managing the lifecycle of all objects (known as beans) within the application.
As mentioned earlier DI in Spring can be achieved through fields, setters, or constructors.
Constructor-Based Dependency Injection
In constructor-based injection, the dependencies needed for a class are passed as arguments to its constructor. The container then invokes this constructor, providing the necessary dependencies as arguments, with each argument representing a specific dependency.
@Component
class Pie {
private Flavor flavor;
Pie(Flavor flavor) {
this.flavor = flavor;
}
...
}
Before Spring 4.3, the @Autowired annotation was required for constructor injection. However, in newer versions, this annotation is optional if the class has only one constructor. In the provided example of the Pie class, because there is only one constructor, the @Autowired annotation is not necessary. Consider the below example with two constructors:
@Component
class Burger {
private Ingredient ingredients;
private Bun bunType;
Burger(Ingredient ingredients) {
this.ingredients = ingredients;
}
@Autowired
Burger(Ingredient ingredients, Bun bunType) {
this.ingredients = ingredients;
this.bunType = bunType;
}
...
}
When a class has multiple constructors, it's necessary to explicitly add the @Autowired annotation to one of them. This annotation informs Spring about which constructor to utilize for injecting dependencies.
Setter Injection
Setter-based Dependency Injection is akin to constructor-based Dependency Injection, but instead of passing dependencies through the constructor, they are set using setter methods after the object is instantiated.
In setter-based injection, dependencies are provided as parameters to the class fields, and their values are assigned using setter methods. These setter methods need to be annotated with @Autowired.
During setter-based DI, the container instantiates the bean by invoking a no-argument constructor or a no-argument static factory method, and then proceeds to call the setter methods of the class to set the dependencies.
The Car class requires an object of type Engine. The Engine object is provided as an argument in the setter method of that property:
@Component
class Car {
private Engine engine;
@Autowired
void setEngine(Engine engine) {
this.engine = engine;
}
Engine getEngine() {
return engine;
}
...
}
Field Injection
This approach, known as field-based Dependency Injection (DI), involves annotating the desired class fields with @Autowired to indicate the dependencies that should be injected.
领英推荐
In this example, we let Spring inject the Engine dependency via field injection:
@Component
class Car {
@Autowired
private Engine engine;
void setEngine(Engine engine) {
this.engine = engine;
}
Engine getEngine() {
return engine;
}
...
}
While field-based dependency injection offers simplicity and cleanliness, it's not always the best choice. It can lead to performance issues due to its reliance on reflection, which is slower compared to constructor or setter injection.
Additionally, it's easy to violate the Single Responsibility Principle by adding too many dependencies, causing the class to appear to perform more functions than necessary.
Now, the million-dollar question: which type of dependency injection should I use?
First, let's see what the Spring team recommends in their documentation:
This article will now strongly encourage you to use Constructor Injection.
So, why Should I Use Constructor Injection?
Having explored the various injection methods, let's now delve into the benefits of utilizing constructor injection.
All necessary dependencies are accessible during initialization
When we create an object, we call a constructor. If this constructor expects all the necessary dependencies as parameters, then we can be sure that the class will never be instantiated without its injected dependencies.
The IoC container ensures that all arguments provided in the constructor are available before passing them to the constructor. This helps to avoid the unwanted NullPointerException.
Constructor injection is extremely useful because we don't need to write separate business logic everywhere to check if all the necessary dependencies are loaded, thus simplifying the code complexity.
Identifying potential code smells
Constructor injection aids in identifying potential code smells, particularly when the constructor requires a large number of arguments. This could indicate that the class has too many responsibilities, prompting the need for refactoring to ensure proper separation of concerns.
Enhancing Unit Testing with Constructor Injection
Constructor injection streamlines unit testing by necessitating valid objects for all dependencies, effectively avoiding incomplete object creation.
Mocking libraries like Mockito facilitate the creation of mock objects that can be passed into constructors. While mocks can also be passed via setters, there's a risk of forgetting to invoke the setter when adding a new dependency, potentially leading to NullPointerExceptions in tests.
With constructor injection, test cases are only executed when all dependencies are present, ensuring comprehensive testing without incomplete objects.
Immutability
Constructor injection promotes the creation of immutable objects, as the constructor's signature serves as the sole method for object instantiation. Once a bean is created, its dependencies cannot be altered. In contrast, setter injection allows dependency injection after object creation, resulting in mutable objects. Mutable objects can pose challenges in multi-threaded environments, potentially leading to thread safety issues, and may be harder to debug due to their mutability.
Conclusion
In conclusion, constructor injection offers clear benefits. It ensures all dependencies are available during initialization, simplifies code complexity, aids in identifying code smells, enhances unit testing by requiring valid dependencies, and promotes immutability, crucial for thread safety and debugging. Overall, constructor injection is a powerful technique for building robust, testable, and maintainable software systems.
References