What is Dependency Inversion?
The Dependency Inversion Principle (DIP) is one of the five SOLID principles. The principle says High-level modules (classes) should not depend on low-level modules. Both should depend on abstractions.
Let's start with an example that does not use this principle. I created a UserService class that is in charge of creating new users.
class UsersService {
public createUser(data: User) {
// ...
}
}
However, we need to add logs as well. Luckily, the is a LoggerService that does the logging for the whole application. Let's apply it:
class UsersService {
private logger;
public createUser(data: User) {
logger = new LoggerService();
loggger.log('Started creating users...');
// ...
}
}
The logger is in place and it does the job, but, there are a few problems:
1. Difficult to maintain
What if the implementation of LoggerService changes? For example, new methods are added, existing ones are changed, and there is a parameter added to the constructor of a LoggerService. If that happens, we'd need to go to all places where Logger Service is used and make the necessary changes:
logger = new LoggerService('parameter');
2. Hard to swap dependencies
What if sometime in the future we need to swap the existing LoggerService that logs in the console with another that logs in the database? Or if we have a Payment class that uses PayPal and we need to swap it with another class that uses Stripe?
In either case, a huge refactor is unavoidable.
3. Hard to Test
Because the UserService is directly calling the logger, it means that when testing the service, we'd make actual logs to the console/database and we don't want that. Imagine if this was an Email Service and it sent emails to customers while running tests on each build. It'd be very expensive for us and we'd also spam the customers.
Enter Dependency Inversion. Instead of invoking LoggerService directly, the UserService will call its interface. This pattern solves all previously mentioned problems, as we're not bound to work with actual classes but rather their abstractions.
The abstraction will have the methods available on the LoggerService and in case the underlying implementation or framework changes, it won't matter.
Implementing Dependency Inversion in Nest.js
Now we're going to apply this principle in Nest.js.
Quick Note
Nest.js however does not allow using interfaces as providers because injection tokens only work strings, symbols, and classes (as explained here). However, it does work with Abstract classes which we'll use here.
App overview
📁 src
|__ 📁 models
|_____ loggerService.class.ts
|__ 📁 services
|_____ 📁 ConsoleLogger
|_______ consolelogger.service.ts
|_____ 📁 FileLogger
|_______ filelogger.service.ts
|__ app.module.ts
|__ app.controller.ts
Generate abstract class
// ./models/loggerservice.class.ts
export default abstract class LoggerService {
abstract log(data: string): void;
}
Generate Two Services
-
ConsoleLoggerService
- Implementation of LoggerService that logs to the console -
FileLoggerService
- Implementation of LoggerService that logs to the file
Each service extends the abstract LoggerService
class and implements the log()
method.
// ./services/ConsoleLogger/consolelogger.service.ts
import { Injectable } from '@nestjs/common';
import LoggerService from '../../models/loggerservice.class';
@Injectable()
export class ConsoleLoggerService extends LoggerService {
log(data: string): void {
console.log(data);
}
}
// ./services/FileLogger/filelogger.service.ts
import { Injectable } from '@nestjs/common';
import LoggerService from '../../models/loggerservice.class';
// Node.js built-in File-System API
import { createWriteStream } from 'fs';
@Injectable()
export class FileLoggerService extends LoggerService {
log(data: string): string {
const stream = createWriteStream('<your-file>.log', { flags: 'a' });
// 'a' means append to the existing file
stream
.end(data) // insert data into file
.on('error', () => console.log('Unable to log to file!'));
}
}
Setup Appmodule
In AppModule we'll inject our services using the abstract class as a provider token.
import LoggerService from './models/loggerservice.class';
import { ConsoleLoggerService } from './services/ConsoleLogger/consolelogger.service';
import { FileLoggerService } from './services/FileLogger/filelogger.service';
@Module({
imports: [],
controllers: [AppController],
providers: [
AppService,
{
provide: LoggerService, // our custom injection token
useClass: /* either implementation of LoggerService */
},
],
})
export class AppModule {}
Setup AppController
import { Controller, Get, Inject } from '@nestjs/common';
import LoggerService from './models/loggerservice.class';
@Controller()
export class AppController {
constructor(private readonly logger: LoggerService) {}
@Get()
getHello(): string {
this.logger.log('Greeting sent to the user');
return 'Hello World!';
}
}
The beauty here is that the consumer has no clue what logger service is used.
Let's give it a whirl!
Set a ConsoleLoggerService inside the AppModule and run the app:
providers: [
AppService,
{
provide: LoggerService, // our custom injection token
useClass: ConsoleLoggerService
},
],
npm run start:dev
curl localhost:3000
The console output should be visible in the terminal.
Now to the same for the FileLoggerService. Important note - remember to create your log file first, e.g. access.log.
providers: [
AppService,
{
provide: LoggerService, // our custom injection token
useClass: FileLoggerService
},
],
curl localhost:3000
The information is logged into the file as expected.
And that's it!
If you'd like to learn more about logging in Node.js, be sure to check out my blog on Automated Logging in Express.js. For everything else awesome, follow me on Dev.to and on Twitter to stay up to date with my content updates.
Top comments (4)
What? In fact, you don't have to use
@Inject()
in your example, becauseLoggerService
is a class that can be associated with the appropriate dependency.@Inject()
makes sense to use if your token is not a class.You're right.
I'll fix that right away. Thanks for pointing it out.
NestJS objectively has an architecture that is poorly suited for modularity.
Perhaps... I think it's in the sweet spot between Express.js and .NET Web API regarding OOP.