What is Dependency Injection? A Beginner’s Guide for Java Developers

Shambhavi Shandilya
4 min readMay 21, 2024

--

In object-oriented programming, we often deal with classes that rely on other classes to function. These “dependencies” can create tightly coupled code, making it difficult to test and maintain. Dependency injection (DI) is a design pattern that helps us decouple these dependencies, leading to cleaner, more flexible, and easier-to-test code.

Imagine you’re building a car. The engine is a critical dependency for the car to function. Traditionally, the car class might be responsible for creating its own engine. But with dependency injection, some other entity (like a factory or another company) would provide the engine for the car. This allows developers to:

Use different engine types: You can easily swap the engine with an electric motor or a more powerful model without modifying the car class.

Easier testing: You can provide a mock engine for unit testing the car’s functionality without needing a real engine.

Improved maintainability: The code becomes more modular and easier to understand, as the car class doesn’t need to concern itself with engine creation details.

The “car” might need different dependent engine objects

These benefits align with the SOLID principles of object-oriented design, specifically the Dependency Inversion Principle (DIP).

High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.

To understand DI, we shall take an example of the Car class and implement it in different ways using DI.

public class Car {
private Engine engine;
public Car() {
engine = new Engine(); // Creates its own dependency
}
public void start() {
engine.start();
}
}
The “Car” class creates its own dependency (without Dependency Injection)

Three Types of Dependency Injection

There are three main native ways to inject dependencies into a class:

1. Constructor Injection

  1. The class receives its dependencies as arguments in its constructor.
  2. This approach is explicit and promotes clear dependencies.
public class Car {
private final Engine engine;

public Car(Engine engine) {
// The constructor is providing the dependency object
this.engine = engine;
}

public void start() {
engine.start();
}
}
Dependency is injected into the “Car” class from the constructor

2. Setter Injection

  1. Dependencies are injected using setter methods.
  2. It offers more flexibility for dynamic configuration but can be less clear.
public class Car {
private Engine engine;

public void setEngine(Engine engine) {
// A setter function is providing the dependency
this.engine = engine;
}

public void start() {
engine.start();
}
}
Dependency is injected into the “Car” class from an explicit setter method

3. Interface Injection

  1. Classes depend on interfaces, allowing for different implementations of the functionality.
  2. Promotes loose coupling and encourages code reusability.
public interface Engine {
void start();
}
public class Car {
private final Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.start();
}
}
public class GasEngine implements Engine {
@Override
public void start() {
System.out.println(“Starting gas engine…”);
}
}
public class ElectricEngine implements Engine {
@Override
public void start() {
System.out.println(“Starting electric engine…”);
}
}
Dependency is injected into the “Car” class from an interface

Unit Testing Made Easy with DI

Dependency injection simplifies unit testing by allowing you to isolate individual components easily. You can provide mock objects or stubs for dependencies, enabling you to test the specific functionality of the class without relying on external systems or complex setups.

For example, to unit test the Car class’s start method, you could create a mock Engine implementation that simulates the starting behaviour. This lets you focus on testing the car’s logic without needing a real engine.

DI can help create mock objects for testing purposes

Conclusion

Dependency injection is a powerful tool that can improve your code’s flexibility, testability, and maintainability. While it can introduce some overhead for simpler projects, it often pays off for larger codebases with complex dependencies.

Disadvantages to Consider

Increased complexity: DI can add some initial complexity, especially for beginners.
Boilerplate code: Constructor or setter injection can lead to repetitive code for setting dependencies.

Apart from the traditional methods, external libraries can perform this process. Some popular ones are Guice, Dagger, Spring, etc.

Despite these potential downsides, the benefits of DI often outweigh the drawbacks. Adopting DI principles creates a foundation for building robust, well-structured, and easily maintainable software applications.

--

--