DEV Community

Martin Tovmassian
Martin Tovmassian

Posted on • Edited on

How to optimize module encapsulation in Node.js

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`);
}
Enter fullscreen mode Exit fullscreen mode

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`);
    }

};
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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`);
    }

}
Enter fullscreen mode Exit fullscreen mode
// index.js
let clientDb = require('./clientDb');
let oscar = require('./oscar');

oscar.sayMyName(clientDb);
oscar.sayMyAge(clientDb);
Enter fullscreen mode Exit fullscreen mode

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`);
    }

}
Enter fullscreen mode Exit fullscreen mode

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

    };

};
Enter fullscreen mode Exit fullscreen mode

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

    };

};
Enter fullscreen mode Exit fullscreen mode
// index.js
let clientDb = require('./clientDb');
let oscar = require('./oscar')(clientDb);

oscar.sayMyName();
oscar.sayMyAge();
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
cherif_b profile image
Cherif Bouchelaghem

In the Optimized encapsulation first example, for performance reasons, I guess functions could be defined outside the exported function and they still be available to use inside factory function.

// a-module.js

function privateFunction() {}

 function publicFunctionA() {}

 function publicFunctionB() {}


module.exports = function(sharedParameter) {

    return {

        publicFunctionA: publicFunctionA,
        publicFunctionB: publicFunctionB

    };

};

Great article BTW.