Dependency Injection Made Easy: Simple Examples to Understand Key Concepts

Friday, 31 Mar 2023 6 minutes read

What Is Dependency Injection (DI)

Dependency Injection (DI) is a design pattern used in software engineering. It aims to increase modularity and reduce coupling between software components. In this pattern, the responsibility of creating and managing objects is delegated to a separate component called the container. The container creates and manages the objects and injects them into the components that require them.

What Do We Achieve With DI

The primary goal of DI is to make software components independent of each other, thus making the code more maintainable and easier to test. Few benefits of DI can be:

  • Reduced coupling: With DI, the dependencies of a class are decoupled from the class itself. This makes it easier to modify, test, and reuse the code.
  • Improved testability: DI makes it easier to test individual components in isolation. By injecting mock objects or test doubles, you can test the behavior of a component without worrying about its dependencies.
  • Easier to maintain: DI makes it easier to change the behavior of a component without affecting the rest of the system. This makes it easier to maintain and update your code over time.
  • Increased modularity: DI promotes modularity by breaking down complex systems into smaller, more manageable components.

Types Of Dependency Injection

There are three main types of dependency injection:

  1. Constructor Injection: In this type of dependency injection, the dependencies are passed to a class through its constructor. The dependencies are then stored in private fields within the class and used as needed. Constructor injection is a simple and effective way to provide dependencies to a class, and it helps to ensure that the class has all the dependencies it needs to function properly.

  2. Setter Injection: In setter injection, the dependencies are set using setter methods. This type of dependency injection is less common than constructor injection, but it can be useful in cases where you want to provide a default value for a dependency or where you want to change the value of a dependency at runtime.

  3. Interface Injection: In interface injection, a separate interface is used to provide the dependencies to a class. The class implements the interface and uses its methods to get the dependencies it needs. This type of dependency injection is more complex than the other two types and is less commonly used.

Lets assume we have two classes. Car and Engine

Engine.java

public class Engine {
    private String type;

    public Engine(String type) {
        this.type = type;
    }

    public void start() {
        System.out.println("Starting " + type + " engine...");
    }
}
 

Car.java

public class Car {
    private Engine engine;

    // Constructor Injection
    public Car(Engine engine) {
        this.engine = engine;
    }

    // Setter Injection
    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    // Interface Injection
    public void setEngine(EngineInterface engine) {
        this.engine = engine.getEngine();
    }

    public void start() {
        System.out.println("Starting car...");
        engine.start();
    }
}

The Car class has a dependency on the Engine class. Let's see how each type of dependency injection would look for the Car class.

Constructor Injection: In this approach, the Engine dependency is passed to the Car class through its constructor.

Engine engine = new Engine("Gasoline");
Car car = new Car(engine); 
Setter Injection: In this approach, the Engine dependency is set using a setter method. 
Engine engine = new Engine("Diesel"); 
Car car = new Car();
car.setEngine(engine);

Interface Injection: In this approach, an interface EngineInterface is used to provide the Engine dependency to the Car class.

public interface EngineInterface {
    public Engine getEngine();
}

public class GasolineEngine implements EngineInterface {
    public Engine getEngine() {
        return new Engine("Gasoline");
    }
}

EngineInterface engineInterface = new GasolineEngine();
Car car = new Car();
car.setEngine(engineInterface);

Interface injection is a good choice when the dependency is polymorphic. In the above example the engine could also be an ElectricEngine

How About Not Using DI Here

In the above example we used Car and Engine class and applyed DI. Lets see how would the Class look without DI

public class Car {
    private Engine engine;

    public Car(Engine engine) {
        this.engine = new Engine();
    }

    // Other methods
}

Here, Car class is responsible for creating its dependency Engine. This makes the Car tightly coupled with its dependency. This will eventually lead into complexity, eg. if there is any change in the Engine constructor the Car should also get updated for that.