Dependency Injection (DI) is one of those concepts that has a really intimidating name but once you get to know it, it is really quite easy. The core concept behind DI is to support ‘Program to an interface, not an implementation’ whereby objects with dependencies should be passed a reference to their dependencies rather than be responsible for constructing them on their own. By doing this, you implement a more flexible design that enhances testability and extensibility in your code.
The main ways to implement Dependency Injection are:
Constructor injection is where you pass references to the already instantiated dependencies directly into the constructor of the new object upon creation. This is the most common form of DI and is what you are most likely to use. Constructor injection is used to create objects that have dependencies so that the receiving class doesn’t need to know about the implementation or construction details of its dependencies. This facilitates loose coupling.
To use the classic car example:
const smallEngine = { start() {} };
class Car {
constructor(smallEngine) {
this.engine = smallEngine;
this.start = this.start.bind(this);
}
start() {
this.engine.start();
}
}
const car = new Car(engine);
car.start();
Setter injection is typically used when constructor injection isn’t preferable because an object’s dependencies can’t be resolved at build time, you want to be able to change at runtime, or the developer doesn’t want to couple object construction to the object’s constructor. A use case for this is when implementing the strategy pattern where you may want to encapsulate actions into their own objects and determine which action to use at runtime.
To extend the example above, the alternative using setter injection is as follows:
const stockEngine = { start() {} };
const v8Engine = { start() {} };
class Car {
constructor() {
this.start = this.start.bind(this);
}
setEngine(engine) {
this.engine = engine;
}
start() {
this.engine.start();
}
}
const car = new Car();
// create car with original engine
car.setEngine(stockEngine);
car.start();
// now, the engine can be changed at runtime
car.setEngine(v8Engine);
car.start();
Method injection is where the dependency to be used is directly passed into the method being called by the caller. This is useful in situations where binding the dependency to the callee would create a circular dependency situation, such as a service => repository => factory => model => repository.
An example is if you wanted your car to provide a report of its accident history, it may not be optimal to store the entire history of your car on your car object (and have to get it from the database on each call) Instead, if and when you need that method to be called, you can pass a history provider as a parameter into a method, leaving your car decoupled from the history provider implementation.
class Car {
constructor() {
this.getHistory = this.getHistory.bind(this);
}
getHistory(historyProvider) {
return historyProvider.getCarHistory(this._id);
}
}
const car = new Car();
const carFax = {
getCarHistory() {},
};
const instaVIN = {
getCarHistory() {},
};
const carFaxHistory = car.getHistory(carFax);
const instaVINHistory = car.getHistory(instaVIN);
Thank you for reading! To be the first notified when I publish a new article, sign up for my mailing list!