I'm going introduce you in a problem that maybe you've been through and some point in your node.js career.
Usually i split my business logic from anything else in my code (let's name it as a service), been my business layer responsible to trigger the resources that are required to make some action. Sometimes, one item in this business layer needs to use another one in the same layer.
Example:
CustomerService requires UserService to create the login credentials
and in another moment, UserService will call CustomerService to validate Customer profile.
Failure scenario:
|--/services/CustomerService.js
const UserService = require('./UserService')
class CustomerService{
create() {
UserService.create();
console.log('Create Customer');
}
get() {
return {
name: 'test'
}
}
}
module.exports = new CustomerService;
|--/services/UserService.js
const CustomerService = require('./CustomerService')
class UserService {
create() {
console.log('Create user');
}
get() {
let customer = CustomerService.get();
console.log({customer});
}
}
module.exports = new UserService;
|--/index.js
const CustomerService = require('./services/CustomerService');
const UserService = require('./services/UserService');
CustomerService.create();
UserService.get();
So, after implementing this code and run node index.js in your terminal, you gonna get the following error:
You may be thinking: WTF??? But this method does exist!!!!
Yes, that was my reaction. You're getting this error due the circular dependency, which happens when you create a dependecy between two modules, it means that we're importing and using UserService inside CustomerService and vice-versa.
How can we solve this? One possible solution.
We're going to load our modules in a centralized file called index.js, so after this, we're going to import only the index.js file and specify the Objects that we need to use.
Hands on:
1 - Create a index.js file inside the services folder (Attention: it's our crucial code snippet):
|--/services/index.js
const fs = require('fs');
const path = require('path');
const basename = path.basename(__filename);
const services = {};
// here we're going to read all files inside _services_ folder.
fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) &&
(file !== basename) &&
(file.slice(-3) === '.js') &&
(file.slice(-8) !== '.test.js') &&
(file !== 'Service.js')
}).map(file => {
// we're are going to iterate over the files name array that we got, import them and build an object with it
const service = require(path.join(__dirname,file));
services[service.constructor.name] = service;
})
// this functionality inject all modules inside each service, this way, if you want to call some other service, you just call it through the _this.service.ServiceClassName_.
Object.keys(services).forEach(serviceName => {
if(services[serviceName].associate) {
services[serviceName].associate(services);
}
})
module.exports = services;
2 - Let's create a Parent class that gonna be inherited by each service that we create.
|--/services/Service.js
class Service {
associate(services) {
this.services = services;
}
}
module.exports = Service;
3 - Let's rewrite ou code to check how it's gonna be.
|--/services/CustomerService.js
const Service = require('./Service')
class CustomerService extends Service{
create() {
this.services.UserService.create();
console.log('Create Customer');
}
get() {
return {
name: 'test'
}
}
}
module.exports = new CustomerService;
|--/services/UserService.js
// now you only import the Service.js
const Service = require('./Service.js')
class UserService extends Service{
create() {
console.log('Create user');
}
get() {
// now we call the service the we want this way
let customer = this.services.CustomerService.get();
console.log({customer});
}
}
module.exports = new UserService;
4 - now., let's see how we're going to call the services outside the services folder.
// now we only import the index.js (when you don't set the name of the file that you're intending to import, automatically it imports index.js)
const {CustomerService, UserService} = require('./services/')
// and we call this normally
CustomerService.create();
UserService.get();
PROS:
- Now it's easier to load another service, without getting any circular dependecy error.
- if you need to use a service inside other service, you just have to call the property this.service.NameOfTheService.
CONS:
- It's not trackeable anymore by your IDE or Code Editor, as the import handler is inside our code and it's not straight in the module that you want to use anymore.
- A small loss of performance due the loading of all modules, although some of them are not used.
If you think that there's something confusing, or impacting the understanding, or that i can improve, please i'm going to appreciate your feedback.
See you guys and thanks a lot
Top comments (3)
you forget the get() method atcustomerservice...
Good one there.
But, I would rather prefer using events in Node.js.
Hey man! Thank you for your feedback.
How would you implement events in this situation?