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:
-
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.
-
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.
-
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);
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.