Dependency Injection Design Pattern in Java

Dependency Injection Design Pattern in Java

In this article, I am going to discuss Dependency Injection Design Pattern in Java with Examples. Please read our previous article where we discussed Intercepting Filter Design Pattern in Java. In this article, we will explore the Dependency Injection Design Pattern in Java, its advantages, disadvantages, and practical applications in software development.

What is Dependency Injection Design Pattern?

In software development, managing dependencies between components is crucial for building flexible, maintainable, and testable code. The Dependency Injection design pattern provides a systematic approach to handling dependencies by externalizing their creation and management.

The Dependency Injection (DI) design pattern is a technique that focuses on separating the creation and management of dependencies from the classes that use them. It enables components to be loosely coupled by having their dependencies injected from external sources rather than creating them internally.

The Dependency Injection Design Pattern involves 3 types of classes:

  1. Client Class: The Client Class (dependent class) is a class that depends on the Service Class. That means the Client Class wants to use the Services (Methods) of the Service Class.
  2. Service Class: The Service Class (dependency) is a class that provides the actual services to the client class.
  3. Injector Class: The Injector Class is a class that injects the Service Class object into the Client Class.

For a better understanding, please have a look at the following diagram.

What is Dependency Injection Design Pattern?

As you can see above in the above diagram, the Injector Class creates an object of the Service Class and injects that object into the Client Class. And then the Client Class uses the Injected Object of the Service Class to call the Methods of the Service Class. So, in this way, the Dependency Injection Design Pattern separates the responsibility of creating an object of the service class out of the Client Class.

Different Types of Dependency Injection in Java:

The Injector Class injects the Dependency Object into the Client Class in three different ways. They are as follows.

  1. Constructor Injection: When the Injector Injects the Dependency Object (i.e. Service Object) into the Client Class through the Client Class Constructor, then it is called Constructor Dependency Injection.
  2. Setter Injection: When the Injector Injects the Dependency Object (i.e. Service Object) into the Client Class through the public setter property of the Client Class, then it is called Setter Dependency Injection.
  3. Method Injection: When the Injector Injects the Dependency Object (i.e. Service Object) into the Client Class through a public Method of the Client Class, then it is called Method Dependency Injection. 

Constructor Injection

Constructor dependency injection is a design pattern used in object-oriented programming where the dependencies of a class are provided through its constructor. Instead of the class creating or managing its dependencies internally, they are passed into the class from the outside. Here are a few key points regarding constructor dependency injection:

  • Decoupling: Constructor dependency injection helps decouple classes by removing the responsibility of creating or obtaining dependencies from within the class. This separation improves code maintainability and testability.
  • Explicit dependencies: By explicitly specifying dependencies in the constructor, it becomes clear what other components or services a class relies on to function correctly. This clarity makes code easier to understand and reduces the chances of hidden or implicit dependencies.
  • Testability: Constructor injection facilitates unit testing by allowing dependencies to be easily mocked or replaced with test doubles. This enables isolated testing of the class under consideration, as the test can provide mock or stub objects to simulate different scenarios.
  • Inversion of Control (IoC): Constructor dependency injection is one of the ways to achieve Inversion of Control. Rather than the class itself being responsible for obtaining its dependencies, the responsibility is inverted to an external entity (often a container or framework) that manages the creation and wiring of objects.
  • Flexibility and extensibility: Constructor injection allows for flexible and extensible code. New dependencies can be introduced by modifying the constructor signature, and the class can be easily adapted to work with different implementations of its dependencies by providing alternative objects during instantiation.
  • Dependency graph: Constructor injection makes the dependencies of a class more explicit and visible, which helps in understanding the overall dependency graph of an application. This knowledge can be useful for managing and organizing dependencies effectively.
  • Single Responsibility Principle (SRP): Constructor injection promotes the SRP by focusing on the single responsibility of a class and delegating the responsibility of dependency management to external entities. This separation of concerns improves code modularity and maintainability.
  • Constructor overloading: With constructor injection, it is possible to provide multiple constructors with different sets of dependencies. This allows clients to choose the appropriate constructor based on their requirements, enabling flexibility in object instantiation.

In Java, constructor dependency injection can be implemented as follows:

public class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void saveUser(User user) {
        userRepository.save(user);
    }
}

In the example above, the UserService class has a dependency on the UserRepository class, which is passed through the constructor. The UserService class has a single constructor that takes an instance of UserRepository as a parameter.

The constructor assigns the UserRepository instance (which is passed into the constructor) to the userRepository field of the UserService class. This way, the UserService can utilize the UserRepository for saving user data.

By using Constructor Dependency Injection, the dependency is clearly expressed through the constructor, and the class can’t be instantiated without providing the required dependency. It promotes loose coupling and simplifies testing as the dependencies can be easily mocked or substituted during unit tests.

To use the UserService, you would need to instantiate it and provide the required UserRepository dependency:

UserRepository userRepository = new UserRepository(); // or use a framework to create an instance
UserService userService = new UserService(userRepository);

User user = new User(“John Doe”, “john.doe@example.com”);
userService.saveUser(user);

In the example above, we first create an instance of UserRepository. Then, we create an instance of UserService, passing the UserRepository instance through the constructor. Finally, we can use the userService object to save a user.

Setter Injection

Setter dependency injection is an alternative approach to constructor dependency injection in object-oriented programming. Instead of passing dependencies through the constructor, dependencies are set using setter methods. Here are a few key points regarding setter dependency injection:

  • Flexible configuration: Setter injection allows for a more flexible configuration of dependencies since they can be set or changed at any time during the object’s lifecycle. This can be useful when dealing with optional or dynamic dependencies.
  • Gradual object construction: With setter injection, it is possible to create an object with default dependencies and then gradually set or update the dependencies as needed. This can be beneficial when dealing with complex object graphs or when some dependencies are not immediately available during object creation.
  • Optional dependencies: Setter injection is suitable for optional dependencies that may or may not be required by the class. By providing setter methods, these dependencies can be injected if and when needed, allowing for more lenient usage of the class.
  • Testability: Like constructor injection, setter injection facilitates unit testing by allowing dependencies to be easily mocked or replaced with test doubles. Test scenarios can be created by setting different dependencies through the setter methods, enabling more granular testing of the class.
  • Readability and maintainability: Setter injection can make the code more readable, as the dependencies are explicitly set using descriptive method names. Additionally, modifying dependencies is straightforward since it involves calling the appropriate setter method.
  • Inversion of Control (IoC): Setter injection is also a form of IoC, as the responsibility for providing dependencies is shifted to an external entity. However, the control flow of the object’s construction is not as clear as with constructor injection, as dependencies can be set at different times during the object’s lifecycle.
  • Potential for inconsistent object state: With setter injection, there is a possibility of leaving an object in an inconsistent state if required dependencies are not set. It is crucial to ensure that all necessary dependencies are appropriately set before using the object to avoid runtime errors or unexpected behavior.
  • Maintenance challenges: Setter injection can lead to code that is more difficult to maintain, as it requires tracking and managing dependencies separately from the object’s construction. Changes to dependencies or their relationships may need to be reflected in multiple setter calls across different parts of the codebase.

In Java, setter dependency injection can be implemented as follows:

public class UserService {
    private UserRepository userRepository;

    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void saveUser(User user) {
        userRepository.save(user);
    }
}

In the example above, the UserService class has a dependency on the UserRepository class, which is injected through a setter method. The UserService class provides a setter method setUserRepository() to set the UserRepository dependency.

The setUserRepository() method assigns the passed UserRepository instance to the userRepository field of the UserService class. This way, the UserService can utilize the UserRepository for saving user data.

By using Setter Dependency Injection, the dependency can be set or changed at any time after the UserService object is created. It provides flexibility in configuring the dependencies and allows for optional or dynamic dependencies.

To use the UserService with Setter Dependency Injection, you would need to create an instance of UserService and then set the required UserRepository dependency using the setter method:

UserRepository userRepository = new UserRepository(); // or use a framework to create an instance
UserService userService = new UserService();
userService.setUserRepository(userRepository);

User user = new User(“John Doe”, “john.doe@example.com”);
userService.saveUser(user);

In the example above, we first create an instance of UserRepository. Then, we create an instance of UserService and set the UserRepository dependency using the setUserRepository() method. Finally, we can use the userService object to save a user.

Method Injection

Method dependency injection is a variation of dependency injection where the dependencies of a class are provided through specific methods rather than through the constructor or setters. Here are a few key points regarding method dependency injection:

  • Granular control: Method dependency injection allows for granular control over injecting dependencies. Instead of injecting all dependencies at once during object creation, dependencies can be injected on a per-method basis. This provides flexibility in managing and modifying dependencies throughout the object’s lifecycle.
  • Selective injection: With method injection, dependencies can be injected selectively, based on specific method calls or scenarios where they are required. This is particularly useful when certain dependencies are optional or needed only for specific operations within the class.
  • Reduced coupling: Method dependency injection helps reduce coupling between classes by explicitly defining the dependencies required for specific methods. This promotes the Single Responsibility Principle (SRP) and makes the code more modular and maintainable.
  • Clear intent: By injecting dependencies through methods, the intent of the dependencies becomes clearer, as it is tied directly to the methods that require them. This improves code readability and makes it easier to understand the purpose and requirements of each method.
  • Dynamic dependencies: Method injection enables the injection of dynamic dependencies, where the specific dependency instance can change over time or based on different conditions. This allows for more flexibility and adaptability in the behavior of the class.
  • Testability: Similar to a constructor and setter injection, method dependency injection supports easier unit testing by allowing dependencies to be mocked or replaced with test doubles. Test scenarios can be created by providing different dependency instances during method calls, facilitating thorough testing of individual methods.
  • Dependency visibility: Method injection makes the dependencies of a class more visible and explicit. By explicitly stating the dependencies required by each method, it becomes easier to understand the dependencies and their relationships within the class.
  • Potential for increased complexity: Method dependency injection can introduce increased complexity, especially if multiple methods require different sets of dependencies. Care should be taken to avoid method explosion and maintain clarity and simplicity in the code.

In Java, method dependency injection can be implemented as follows:

public class UserService {
    private UserRepository userRepository;

    public void saveUser(User user, UserRepository userRepository) {
        this.userRepository = userRepository;
        userRepository.save(user);
    }
}

In the example above, the UserService class has a dependency on the UserRepository class, and Method Dependency Injection is used to inject the dependency.

The saveUser() method is provided in the UserService class to set the UserRepository dependency. External entities can call this method and pass the UserRepository instance to provide the required dependency.

The saveUser () method assigns the passed UserRepository instance to the userRepository field of the UserService class. This allows the UserService to utilize the UserRepository for saving user data.

To use the UserService with Method Dependency Injection, you would create an instance of UserService and then call the saveUser() method to inject the UserRepository dependency:

UserRepository userRepository = new UserRepository();
UserService userService = new UserService();

User user = new User(“John Doe”, “john.doe@example.com”);
userService.saveUser(user, userRepository);

In the example above, we first create an instance of UserRepository. Then, we create an instance of UserService and an instance of User. Then we call the saveUser() method and provide the User and UserRepository to it. The user object is then saved in the particular repository.

Method Dependency Injection provides flexibility in setting dependencies at different points in the object’s lifecycle and allows for optional or dynamic dependencies. However, it’s important to ensure that the required dependencies are set before using the methods that rely on them.

Components of Dependency Injection Design Pattern

The Dependency Injection Design Pattern consists of the following key components:

  • Dependency: A dependency is an object or service that is required by a class or component to perform its tasks. Dependencies can be other classes, interfaces, data sources, configuration settings, or external services.
  • Dependency Injection Container: The DI container is responsible for managing the creation, configuration, and lifetime of dependencies. It acts as a centralized registry where dependencies can be registered and resolved.
  • Dependency Injection Types: There are three main types of dependency injection: constructor injection, setter injection, and method injection. These types determine how dependencies are provided to the dependent classes.
Example to Understand Dependency Injection Design Pattern in Java

To illustrate the Dependency Injection pattern in a real-world context, let’s consider a scenario where a web application requires a method to send a message to a user. We can use Dependency Injection to inject the messaging dependency. By using Dependency Injection, we achieve loose coupling between the application and the messaging method.

Additionally, Dependency Injection provides flexibility. We can easily switch types of messages, such as email, without modifying the application code. This decoupling of dependencies improves code maintainability and promotes modularity and extensibility. Overall, the Dependency Injection pattern simplifies the management of dependencies and promotes loose coupling between components. The UML Diagram of this example is given below using Dependency Injection Design Pattern.

Example to Understand Dependency Injection Design Pattern in Java

Implementing Dependency Injection Design Pattern in Java

Step 1: Create a new directory to store all the class files of this project.

Step 2: Open VS Code and create a new project, called DependencyInjection.

Step 3: In the project, create a new file called Message.java. Add the following code to the file:

public interface Message
{
    public void sendMessage (String receiver, String msg);
}

This is the interface that will be implemented by other classes.

Step 4: In the project, create two new files called SMS.java and Email.java. Both of these classes implement the Message interface. Add the following code to the files:

SMS.java
public class SMS implements Message
{
    @Override
    public void sendMessage(String receiver, String msg)
    {
        System.out.print("SMS sent to " + receiver + ": ");
        System.out.println(msg);
    }   
}
Email.java
public class Email implements Message
{
    @Override
    public void sendMessage(String receiver, String msg)
    {
        System.out.print("Email sent to " + receiver + ": ");
        System.out.println(msg);
    }   
}

Step 5: In the project, create a new file called Client.java. Add the following code to the file:

public interface Client
{
    public void processMsgs(String receiver, String msg);    
}

This is the interface that will be implemented by other classes.

Step 6: In the project, create a new file called ConcreteClient.java. This class implements the Client interface. Add the following code to the file:

public class ConcreteClient implements Client
{
    private Message m;

    public ConcreteClient(Message m)
    {this.m = m;}

    @Override
    public void processMsgs(String receiver, String msg)
    {m.sendMessage(receiver, msg);}
}

Step 7: In the project, create a new file called Injector.java. Add the following code to the file:

public interface Injector
{
    public Client getClient();
}

This is the interface that will be implemented by other classes.

Step 8: In the project, create two new files called SMSInjector.java and EmailInjector.java. Both of these classes implement the Message interface. Add the following code to the files:

SMSInjector.java
public class SMSInjector implements Injector
{
    @Override
    public Client getClient()
    {return new ConcreteClient(new SMS());}
}
EmailInjector.java
public class EmailInjector implements Injector
{
    @Override
    public Client getClient()
    {return new ConcreteClient(new Email());}
}

Step 9: In the project, create a new file called DependencyInjectionPatternDemo.java. This class contains the main() function. Add the following code to DependencyInjectionPatternDemo.java:

public class DependencyInjectorPatternDemo
{
    public static void main(String[] args)
    {
        String msg = "Hi!, How are you?",
        email = "person@email.com", ph = "1234567890";
        
        //sms
        new SMSInjector().getClient().processMsgs(ph, msg);

        //email
        new EmailInjector().getClient().processMsgs(email, msg);
    }    
}

Step 10: Compile and execute the application. Ensure compilation is successful. Verify that the program works as expected.

Implementing Dependency Injection Design Pattern in Java

Congratulations! You now know how to implement Dependency Injection Design Pattern in Java!

UML Diagram of Dependency Injection Design Pattern:

Now, let us see the Dependency Injection Design Pattern UML Diagram Components with our Example so that you can easily understand the UML Diagram.

UML Diagram of Dependency Injection Design Pattern

The classes can be described as follows:

  1. Message & ConcreteMessage: This represents the message interface and its implementation. There is one implementation per type of message.
  2. Injector & ConcreteInjector: This represents the injector interface and its implementation. Usually, there is one implementation per type of message.
  3. Client & ConcreteClient: This represents the client interface and implementation.
  4. DriverClass: This class contains the main() function and is responsible for the simulation of the program.
Advantages of Dependency Injection Design Pattern in Java:

The followings are the advantages of using the Dependency Injection Design Pattern in Java:

  • Loose Coupling: The primary advantage of the Dependency Injection pattern is achieving loose coupling between components. By externalizing the creation and management of dependencies, classes are not responsible for instantiating their dependencies directly. This loose coupling enhances modularity, flexibility, and maintainability.
  • Testability: Dependency Injection greatly improves the testability of code. By injecting dependencies, it becomes easier to substitute real dependencies with mock objects or test doubles during unit testing. This allows for isolated testing of individual components, leading to more robust and reliable test suites.
  • Reusability: The Dependency Injection pattern promotes the reusability of components. Dependencies can be injected from external sources, making it possible to reuse a component in different contexts or applications without modifying its implementation.
  • Configurability: With Dependency Injection, dependencies can be easily configured and customized. Different implementations of the same interface or class can be injected based on specific requirements or runtime conditions. This flexibility allows for runtime configuration changes without modifying the core logic of the components.
  • Separation of Concerns: Dependency Injection enhances the separation of concerns by removing the responsibility of creating and managing dependencies from the dependent classes. This separation allows classes to focus on their primary tasks, promoting better code organization and readability.
Disadvantages of Dependency Injection Design Pattern in Java:

The followings are the disadvantages of using the Dependency Injection Design Pattern in Java:

  • Learning Curve: Adopting the Dependency Injection pattern may have a learning curve for developers who are new to the concept. Understanding the different types of dependency injection and how to configure the DI container requires additional knowledge and familiarity with the pattern.
  • Increased Complexity: Implementing Dependency Injection can introduce additional complexity to the codebase. The need to configure and manage dependencies through a DI container can add layers of abstraction and configuration files, making the codebase more intricate and harder to understand.
  • Runtime Errors: Since dependencies are resolved at runtime, there is a risk of encountering errors related to unresolved or misconfigured dependencies. These errors may not be caught at compile-time and can lead to runtime issues that are harder to diagnose and debug.
  • Dependency on the DI Container: The Dependency Injection pattern introduces a dependency on the DI container or framework being used. This can limit the portability of the code and make it harder to switch to a different DI container or framework in the future.
Why Do We Need the Dependency Injection Design Pattern in Java?

The Dependency Injection Design Pattern in Java allows us to develop Loosely Coupled Software Components. In other words, we can say that Dependency Injection Design Pattern is used to reduce the Tight Coupling between the Software Components. As a result, we can easily manage future changes and other complexities in our application. In this case, if we change one component, then it will not impact the other components.

In the next article, I am going to discuss the Repository Design Pattern in Java with Examples. Here, in this article, I try to explain Dependency Injection Design Pattern in Java with Examples. I hope you understood the need for and use of the Dependency Injection Design Pattern in Java.

Leave a Reply

Your email address will not be published. Required fields are marked *