How to optimize module encapsulation in Node.js
Martin Tovmassian
Posted on September 27, 2017
Standard encapsulation in Node.js
Module export
Any expression declared within a Node.js module can be exported and become available throughout the application. The export mechanism relies on the use of the keyword exports
to which we assigned a variable name and an expression. For example, if within my oscar.js
module I want to export the sayMyName()
and sayMyAge()
functions I proceed this way:
// oscar.js
exports.sayMyName = function() {
let name = 'Oscar';
console.log(`My name is ${name}`);
}
exports.sayMyAge = function() {
let birthDate = '1990-09-19';
let age = Date.now() - new Date(birthDate) / 31556952000;
console.log(`I am ${age} years old`);
}
This mechanism is very useful insofar as it makes it possible to finely manage access to functions and variables. In fact, every expressions that are not preceded by the exports
keyword remain privates. The exports
keyword refers to an object that contains expressions that need to be exported. Rather than adding expressions one by one, this object can be directly manipulated through the module.exports
keyword. Using this keyword we can refactor oscar.js
this way:
// oscar.js
module.exports = {
sayMyName: function() {
let name = 'Oscar';
console.log(`My name is ${name}`);
},
sayMyAge: function() {
let birthDate = '1990-09-19';
let age = Date.now() - new Date(birthDate) / 31556952000;
console.log(`I am ${age} years old`);
}
};
Module import
The import mechanism relies on the use of the require
function with the relative path of the module we want to import as argument. Once called, this function returns the module.exports
object and then it is possible to access by key the expressions it contains. For example, if within my index.js
module I want to import the oscar.js
module and call the sayMyName()
and sayMyAge()
functions I proceed this way:
// index.js
let oscar = require('./oscar');
oscar.sayMyName();
oscar.sayMyAge();
Limitations of standard encapsulation
Let's imagine that my sayMyName()
and my sayMyAge()
functions now require a client in oder to read name
and birthDate
values into a database. And this client is instantiated as a singleton in the index.js
module. If I keep the standard encapsulation I need to rewrite my modules this way:
// oscar.js
module.exports = {
sayMyName: function(clientDb) {
let name = clientDb.getOscarName();
console.log(`My name is ${name}`);
},
sayMyAge: function(clientDb) {
let birthDate = clientDb.getOscarBirthDate()
let age = Date.now() - new Date(birthDate) / 31556952000;
console.log(`I am ${age} years old`);
}
}
// index.js
let clientDb = require('./clientDb');
let oscar = require('./oscar');
oscar.sayMyName(clientDb);
oscar.sayMyAge(clientDb);
Although this encapsulation is viable and does not encounter any functional limit, it suffers at this point of a loss of optimization since the injection of the database client is not mutualized and must be repeated each time an imported function is called. And this loss of optimization is amplified as soon as we implement private expressions that need to use external parameters as well. To have an illustration let's update the function sayMyAge()
in the oscar.js
module so that now the variable age
is the result of a private function named calculateAge()
.
// oscar.js
function calculateAge(clientDb) {
let birthDate = clientDb.getOscarBirthDate()
return Date.now() - new Date(birthDate) / 31556952000;
}
module.exports = {
sayMyName: function(clientDb) {
let name = clientDb.getOscarName();
console.log(`My name is ${name}`);
},
sayMyAge: function(clientDb) {
let age = calculateAge(clientDb);
console.log(`I am ${age} years old`);
}
}
In this case, it is the calculateAge()
function that requires access to the database and no longer the sayMyAge()
function. Since the calculateAge()
function is private I am now forced to pass the clientDb
parameter to the sayMyAge()
public function just in purpose of making it transit to the calculateAge()
function. Regarding factoring and mutualization of components, this solution is far from the most optimal.
Optimized encapsulation
To counter the limitations of standard encapsulation it is possible to implement this design pattern:
// Design Pattern
module.exports = function(sharedParameter) {
function privateFunction() {}
function publicFunctionA() {}
function publicFunctionB() {}
return {
publicFunctionA: publicFunctionA,
publicFunctionB: publicFunctionB
};
};
Here module.exports
no longer returns an object but a global function. And it is within it that the expressions of our module are declared. The global function then returns an object in which are mapped the functions that we want to make public and export. In this way the mutualization is no longer an issue since parameters can be passed as argument to the global function and become accessible to every expression wether private or public.
If I apply this design pattern to my example, my two modules now look like this:
// oscar.js
module.exports = function(clientDb) {
function sayMyName() {
let name = clientDb.getOscarName();
console.log(`My name is ${name}`);
}
function calculateAge() {
let birthDate = clientDb.getOscarBirthDate()
return Date.now() - new Date(birthDate) / 31556952000;
}
function sayMyAge() {
let age = calculateAge();
console.log(`I am ${age} years old`);
}
return {
sayMyName: sayMyName,
sayMyAge: sayMyAge
};
};
// index.js
let clientDb = require('./clientDb');
let oscar = require('./oscar')(clientDb);
oscar.sayMyName();
oscar.sayMyAge();
Posted on September 27, 2017
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 19, 2024