Dependency injection
Dependency injection is an object-oriented design pattern that allows for the decoupling of dependencies between objects making code more maintainable, testable and modular.
The basic idea is to resist having any class (C) depend on the specific implementation details of any other class to which it sustains a dependency (D). If we can abstract the implementation of D, this makes it easier to swap-out and change C’s dependencies later on without having to re-write C as a result of changes to D.
This is where interfaces become very helpful because they are schematic representations of the main methods in a given class (basically their names, params and return value). As long as this is kept consistent in D when changes are made, you avoid conflicts when changes or refactorings are made to D.
Example
In the example to follow we will have two classes:
ConsoleLogger
- This will simply log something and be a dependency for the class below
UserService
- This will depend on
ConsoleLogger
- This will depend on
First we define an interface for the dependency:
interface ILogger {
log(message: string): void;
}
So this is a class that has a single method log
which receives a message
string as an argument and returns a side-effect.
Now we’ll implement this class:
class ConsoleLogger implements ILogger {
log(message: string): void {
console.log(message);
}
}
Now we create a class that depends on it:
class UserService {
private logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
}
createUser(username: string): void {
// ... (some logic to create a user)
this.logger.log(`User created: ${username}`);
}
}
We can see that the constructor references the Logger
interface. Thus we inject the dependency when instantiating UserService
:
// First implement our earlier class that matches the `Logger` in its shape:
const logger: ILogger = new ConsoleLogger();
// Then pass it as a dependency:
const userService: UserService = new UserService(logger);
userService.createUser("John Doe");