Sitemap
Aditi Tripathi

Aditi Tripathi

Software Enthusiast, AWS

All about Java — Part1(b)

7 min readJun 11, 2023

--

2. Dependency Injection

Press enter or click to view image in full size
Source: Dall-E 2

Dependency Injection (DI) is a software design pattern that allows the separation of dependencies from the classes that use them. It helps manage dependencies by externalizing the creation and injection of dependent objects, rather than having classes create their dependencies directly.

The main goals of DI are to promote loose coupling between classes, improve testability, and enhance maintainability and flexibility of the codebase. By injecting dependencies, classes become easier to test in isolation, as dependencies can be mocked or replaced with alternate implementations during testing.

There are several common ways to achieve Dependency Injection:

1. Constructor Injection:
Constructor injection involves passing dependencies to a class through its constructor. The dependencies are typically defined as parameters, and the class requiring them depends on these interfaces or abstract classes rather than concrete implementations. This is often regarded as the best practice for DI. It promotes immutability, ensures that dependencies are provided at object creation, and makes it clear what dependencies are required for the class to function correctly. It enables better testability and allows for cleaner object initialization. Note: Constructor injection without the @Inject annotation is a manual approach to dependency injection. It requires the responsibility of creating and providing the dependency instance to be handled outside the class, typically in a higher-level component or container. This way, the dependency can be instantiated and passed to the class when it is created. While frameworks can automate the injection process with annotations like @Inject, constructor injection without annotations remains a valid and straightforward method for achieving DI in Java.

Example:

public class MyClass {

private final Dependency dependency;

public MyClass(Dependency dependency) {
this.dependency = dependency;
}
}

2. Setter Injection:
Setter injection involves providing setter methods in a class to inject its dependencies. These setter methods are typically annotated with injection annotations, such as @Inject, to indicate the dependencies that should be injected.

Example:

public class MyClass {
private Dependency dependency;

@Inject
public void setDependency(Dependency dependency) {
this.dependency = dependency;
}
}

3. Method Injection:
Method injection involves injecting dependencies by passing them as parameters to specific methods within a class. These methods can be regular methods or special initialization methods.

Example:

public class MyClass {
private Dependency dependency;

@Inject
public void initialize(Dependency dependency) {
this.dependency = dependency;
}
}

4. Field Injection:
Field injection involves directly injecting dependencies into class fields. This approach is typically discouraged because it can make the code harder to test and maintain. However, some dependency injection frameworks support this method.

Example:

public class MyClass {
@Inject
private Dependency dependency;
}

It’s important to note that while DI patterns promote loose coupling and dependency management, they often require a dependency injection framework or container to handle the actual injection of dependencies. Some popular DI frameworks for Java include Spring Framework, Google Guice, and CDI (Contexts and Dependency Injection).

These frameworks provide mechanisms to configure and manage dependencies, handle object creation, and perform the injection of dependencies automatically based on defined rules and annotations.

Overall, DI is a powerful technique for managing dependencies and improving the design and testability of your code. It allows for greater flexibility, modularity, and maintainability in your applications.

We’ll now compare the benefits of using Guice injection, a popular dependency injection framework, with not using dependency injection at all.

Scenario 1: No Dependency Injection

In this scenario, we have a messaging application with multiple components: MessageService, MessageRepository, and MessageNotifier. Each component relies on the others to function properly. Without dependency injection, we would need to manually instantiate and wire up these dependencies within each component.

public class MessageService {
private MessageRepository messageRepository;
private MessageNotifier messageNotifier;

public MessageService() {
this.messageRepository = new MessageRepository();
this.messageNotifier = new MessageNotifier();
}

// ...
}

public class MessageRepository {
// ...
}

public class MessageNotifier {
// ...
}

The problem with this approach is that it tightly couples the components together. If we later decide to change the implementation of MessageRepository or MessageNotifier, we would need to modify the constructor of MessageService and potentially update other parts of the application that depend on it. This can lead to a lot of manual changes, increasing the risk of introducing bugs and making the codebase harder to maintain and test.

Scenario 2: Using Guice Injection

Get Aditi Tripathi’s stories in your inbox

Join Medium for free to get updates from this writer.

Now, let’s see how Guice injection helps address these issues. Guice allows us to define the dependencies between components using annotations and automatically manages the instantiation and wiring of these dependencies.

public class MessageService {
private MessageRepository messageRepository;
private MessageNotifier messageNotifier;

@Inject
public MessageService(MessageRepository messageRepository, MessageNotifier messageNotifier) {
this.messageRepository = messageRepository;
this.messageNotifier = messageNotifier;
}

// ...
}

public class MessageRepository {
// ...
}

public class MessageNotifier {
// ...
}

By using Guice, we can declare the dependencies of MessageService using the @Inject annotation. Guice will then automatically resolve and provide the necessary instances of MessageRepository and MessageNotifier when creating an instance of MessageService.

The benefits of using Guice injection in this scenario are:

  1. Loose Coupling: With Guice, the dependencies between components are declared explicitly. The components themselves don’t need to know how to create their dependencies, leading to loose coupling. This makes it easier to replace or modify individual components without affecting the entire system.
  2. Dependency Resolution: Guice takes care of resolving and injecting the required dependencies automatically. You don’t have to manually instantiate and wire up the dependencies, reducing boilerplate code and making the codebase cleaner and more maintainable.
  3. Testability: Guice injection simplifies unit testing. Since dependencies can be easily mocked or replaced, you can test each component in isolation without the need for complex setup or relying on real implementations of other components.

Overall, using Guice injection (or any dependency injection framework) provides better modularity, maintainability, and testability compared to manually managing dependencies. It promotes loose coupling and allows for more flexible and extensible code.

Factory Classes alongside Dependency Injection

Adding factory classes can further enhance the benefits of using Guice injection. Factory classes provide a way to create instances of complex objects with customized configurations or dynamic behavior. Let’s extend our previous example by introducing factory classes.

public class MessageService {
private MessageRepository messageRepository;
private MessageNotifier messageNotifier;

@Inject
public MessageService(MessageRepository messageRepository, MessageNotifier messageNotifier) {
this.messageRepository = messageRepository;
this.messageNotifier = messageNotifier;
}

// ...
}

public class MessageRepository {
// ...
}

public class MessageNotifier {
// ...
}

public interface MessageRepositoryFactory {
MessageRepository create();
}

public interface MessageNotifierFactory {
MessageNotifier create();
}

In this updated example, we have added two factory interfaces: MessageRepositoryFactory and MessageNotifierFactory. These factories define methods to create instances of MessageRepository and MessageNotifier, respectively. We can now leverage Guice to inject these factory instances into the MessageService class.

public class MessageService {
private MessageRepository messageRepository;
private MessageNotifier messageNotifier;

@Inject
public MessageService(
MessageRepositoryFactory messageRepositoryFactory,
MessageNotifierFactory messageNotifierFactory) {
this.messageRepository = messageRepositoryFactory.create();
this.messageNotifier = messageNotifierFactory.create();
}

// ...
}

By injecting the factory instances, we can dynamically create the necessary components with customized configurations or behavior. For example, the factory classes might implement logic to create different types of repositories or notifiers based on runtime conditions.

Using factory classes in conjunction with Guice injection offers several benefits:

  1. Flexibility and Customization: The factory classes provide a way to create instances of components with specific configurations or behavior. This allows for flexibility in adapting the components to different scenarios or runtime conditions.
  2. Encapsulation: The creation logic for complex components is encapsulated within the factory classes. This keeps the creation logic separate from the components themselves, promoting better separation of concerns and making the codebase more modular and maintainable.
  3. Testability: With factory classes, it becomes easier to mock or stub the creation logic during unit testing. You can create mock factory instances that return predefined objects, allowing you to test different scenarios without relying on the real implementations.

By combining Guice injection with factory classes, you can achieve even greater decoupling, modularity, and configurability in your application. The factories provide a level of indirection, allowing for dynamic component creation, which can be particularly useful in scenarios where different configurations or behaviors are required.

Popular DI Frameworks:

There are several popular Dependency Injection (DI) frameworks available for Java, each with its own strengths and areas of focus. Some of the most widely used DI frameworks include:

  1. Spring Framework: The Spring Framework is a comprehensive framework that provides various modules and features, including dependency injection. It offers both XML-based and annotation-based configuration options. Spring’s DI framework, often referred to as Spring DI or Spring IoC (Inversion of Control), is widely adopted in enterprise-level Java applications.
  2. Google Guice: Guice is a lightweight DI framework developed by Google. It focuses on simplicity and ease of use. Guice uses Java annotations for dependency configuration and injection. It offers automatic injection and supports both constructor and field injection. Guice is known for its simplicity and clean API.
  3. CDI (Contexts and Dependency Injection): CDI is a part of the Java EE (Enterprise Edition) platform and provides a set of powerful features for managing dependencies and contexts in Java applications. It supports both types of injection: field and constructor injection. CDI uses annotations for dependency configuration and integrates well with other Java EE technologies.
  4. Dagger: Dagger is a statically typed DI framework developed by Google. It is designed for performance and code generation. Dagger generates optimized code during the compilation process to eliminate the use of reflection and improve application startup time. It supports annotation-based configuration and primarily focuses on compile-time validation.
  5. HK2 (Hundred Kilobyte Kernel): HK2 is a lightweight DI framework developed by Oracle. It is part of the GlassFish application server and the Eclipse Jersey project. HK2 offers a flexible and extensible dependency injection system. It supports both programmatic and annotation-based configuration and can be used standalone or within a Java EE environment.
  6. Weld: Weld is the reference implementation of the CDI specification. It is an open-source DI framework that provides a comprehensive set of features for managing dependencies and contexts in Java applications. Weld integrates well with Java EE and offers advanced capabilities such as event handling, interceptors, and decorators.

These are just a few of the most popular DI frameworks available for Java. Each framework has its own unique features, benefits, and learning curves. The choice of DI framework depends on factors such as project requirements, development style, and personal preferences.

--

--

Aditi Tripathi
Aditi Tripathi
0 followers

Software Enthusiast, AWS