Dependency Injection
in Spring

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:

  1. Decoupling: DI separates the dependencies between classes by allowing external sources to provide dependencies. This reduces tight coupling, promoting modular and independent components.
  2. Flexibility: DI simplifies the modification or replacement of dependencies without altering the dependent class. By injecting dependencies externally, it facilitates switching implementations or configurations, making the system more adaptable.
  3. Testability: DI facilitates unit testing by allowing the injection of mock or stub dependencies. This isolates components during testing, ensuring they function correctly without reliance on actual dependencies.
  4. Reusability: By decoupling dependencies from classes, DI enables their reuse across multiple components. This promotes code reuse and eliminates the need for duplicating or tightly coupling dependencies.
  5. Modular Development: DI encourages modular development by advocating for the separation of concerns. Developers can independently develop and test components, wiring their dependencies later, leading to improved code organization and maintainability.

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:

Spring 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

  1. Why You Should Use Constructor Injection in Spring
  2. How does dependency injection work in Spring?
  3. Intro to Inversion of Control and Dependency Injection with Spring
  4. Spring documentation


要查看或添加评论,请登录

社区洞察

其他会员也浏览了