4 formas de manejar dependencias en NodeJS

La gestión de dependencias es uno de los trabajos importantes que tenemos los programadores a la hora de desarrollar sistemas. La forma en que creamos módulos y los relacionamos los unos con los otros puede ser el factor determinante para que nuestro proyecto sea un éxito o no.

¿Cuanta funcionalidad incluyo en un módulo? ¿Cuándo divido un módulo en partes más pequeñas? ¿Cómo los conecto? ¿Qué módulo depende de quien? Contestar todas estas preguntas suele ser difícil y dependiendo del programador con el que hables y el contexto en el que te encuentres, podrás tomar decisiones de diseño diferentes.

Pero ya no es solo en cómo creamos esos módulos y los conectamos, sino más bien, en cómo hacemos que los módulos sean lo mas independientes posibles, cómo podemos hacer que nuestro módulo pueda ser extraído de la maraña para, sino ya ser reutilizado, por lo menos ser testado de manera aislada del resto.

Cómo podremos manejar las dependencias de un módulo, nos va ayudar en nuestro cometido, por ello, el post de hoy nos hablará de cómo incluir dependencias de 4 formas diferentes que nos podrán ser útiles dependiendo del contexto de cada uno de los módulos.

Veremos que la versatilidad de JavaScript nos va a ayudar a la hora de manejar dependencias.

Antes de empezar…

Tenemos que tener claro un par de conceptos que nos atañen a nivel de diseño y otro par de conceptos que son necesarios que tengamos claros a cómo NodeJS se comporta con la instanciación de módulo.

Cuando diseñes módulos en cualquier tecnología, ten en cuenta…

Cuando empiezas a diseñar módulos, ten en cuenta dos conceptos clave del desarrollo software que son la cohesión y el desacoplamiento.

Todo módulo, para que esté bien diseñado, debería cumplir con el principio de única responsabilidad. Esto quiere decir, que un módulo debería ser responsable de realizar una única acción en la que se encuentre especializado. Definir cual es la única responsabilidad de un módulo suele ser complicado y depende de muchos parámetros del contexto, pero tenerlo en mente puede ayudar a crear mejor software. Si un módulo tiene una única responsabilidad, puede ser más fácil de testear o de sufrir menos fricción cuando se produce un cambio. Pero en fin… el post no habla de esto.

Habla de lo importante que es crear módulos que se encuentren muy cohesionados. Esto quiere decir que los métodos que se encuentran dentro de un módulo tengan una relación interna, ya sea que manipulan un estado común, ya sea que tienen mucho sentido que se encuentren en el mismo módulo porque cumplen con el principio de única responsabilidad. Cuanto más cohesionado se encuentran los miembros de un módulo, mayor reutilización, mantenimiento y evolución podría llegar a tener, en términos generales.

Por otro lado, existe el concepto de acoplamiento, que tiene que ver con el número de dependencias que un módulo tiene con otros módulos. Cuantas más dependencias tiene un módulo más difícil de testear y más expuesto a cambios en el futuro podría estar.

Es por tanto, que es importante que cuando diseñemos y desarrollemos módulos consigamos la mayor cohesión entre miembros y el menor acoplamiento entre módulos posible.

Como, no depender de nada, no es posible, tendremos que crear formas en las que incluir esas dependencias nos generen la menor fricción posible.

Cuando diseñes módulos en NodeJS, ten en cuenta…

NodeJS tiene un buen sistema de módulos que hasta la llegada de ES6 revolucionó la forma en la que podíamos modularizar nuestro código JavaScript. El sistema de módulos de NodeJS se basa en el estándar propuesto por CommonJS y cuenta, de manera muy resumida, de dos palabras reservadas para exponer funcionalidad desde un módulo y para llamar a esa funcionalidad desde otro módulo.

Por ejemplo, si yo quiero exponer algo de un módulo podría hacerlo de esta manera:

// lib/module.js
module.exports = function () {};
module exports = {};

Y si yo quiero hacer uso de esa funcionalidad desde otro módulo, lo haría de esta forma:

// app.js
const module = require('./lib/module');

Como el post tampoco va de cómo usar esta funcionalidad, lo dejaremos aquí.

Sí tienes que tener claro que los módulos en NodeJS se instancian casi como si estuviéramos usando el patrón Singleton. Y decimos casi, porque no es del todo así.

Cuando yo instancio un módulo en NodeJS, este lo cachea, de tal forma que si vuelvo a hacer uso de él, la copia que se me devuelve es la cacheada. Este cacheo funciona a nivel de ruta absoluta, por lo que el cacheo nos es útil a nivel de la carpeta en la que me encuentro. Esto se hace así para evitar incongruencias entre módulos que hagan uso de la misma dependencia.

Imaginemos que tenemos la siguiente estructura de ficheros en node_modules y que los dos instancian el módulo que yo anteriormente he creado:

/app.js
/node_modules
    /moduleA
        /node_modules
            /module.js
    /moduleB
        /node_modules
           /module.js

En este caso, cada módulo hará uso de su propia instancia del módulo, si no fuese así, existiría un problema de consistencia interna y sería más difícil tener módulos aislados.

Comentamos esto, porque puede que en el futuro te sea útil y como forma para que entiendas mejor cómo funciona internamente los módulos de NodeJS.

Hardcodeando dependencias

La primera forma en la que podemos manejar dependencias es la rutinaria, la que hacemos todos los días que utilizamos NodeJS y que hemos explicado en el apartado anterior.

Para explicar esta forma de incluir dependencias vamos a hacer un pequeño ejemplo que iremos refactorizando a lo largo de todo el post. La idea del ejemplo es crear un pequeñísimo CRUD que te permita obtener los usuarios de una base de datos y guardar nuevos. Nuestro CRUD va a contar con una estructura en capas típica: Capa de datos > Capa de servicio > Capa de controlador.

Empezaremos desarrollando desde la capa más interna hacia la más externa. Lo primero que haremos será desarrollar nuestra capa de acceso de datos. Usaremos ‘level‘ para guardar y acceder a estos datos y ‘sublevel‘ que es un decorador que añade funcionalidad extra a ‘level’.

Lo hacemos así:

// lib/db.js
const level = require('level');
const sublevel = require('level-sublevel');

module.exports = sublevel(
    level('example-db', { valueEncoding: 'json' })
);

Lo que hacemos es exponer la creación de nuestra base de datos. Si nos detenemos un poco, ya vemos ciertos problemas en este módulo y es que hemos creado un código muy específico.

Como programadores, tenemos que intentar que dentro de nuestro árbol de dependencias, los módulos que se encuentren en la parte más profundas (en las hojas) sean lo más genéricos posibles y que según nos acerquemos a la raíz, los módulos sean más específicos. Esto no se produce porque nosotros queramos, sino porque al estar ya en capas tan superiores, el código  se encuentra ya muy ligado a la lógica de nuestro negocio y esto nos obliga más o menos a que tenga que ser así.

En el código anterior, estamos incluyendo el nombre de la base de datos a fuego. Sin quererlo, hemos hecho que nuestro módulo instancie siempre esa base de datos específica. Por ahora, para el ejemplo nos puede valer.

Lo siguiente, es desarrollar el módulo que hará uso de esta instancia de la base de datos. Lo haremos de la siguiente manera:

// lib/userService.js
const db = require('./db');
const users = db.sublevel('users');

module.exports = {
    findUsers: function () {
        return users.get();
    },
    saveUser: function (user) {
        return users.save(user);
    }
};

Lo que hace el módulo es crear dos métodos: uno para buscar usuarios y otro para crearlos. El módulo depende del módulo de base de datos que hemos creado. Lógicamente tiene que ser así, el problema es que hemos ‘hardcodeado’ el módulo a fuego. Nuestro módulo depende fuertemente de ‘db’, lo que si el día de mañana queremos reutilizar este módulo, nos obligará sí o sí a reutilizar ‘db’ también. Están acoplados. Pero bueno… por ahora nos sirve también :). Sigamos.

Lo siguiente es desarrollar el controlador. Esta capa tiene la responsabilidad de jugar con la petición y la respuesta HTTP. También cuenta con dos métodos y llamará a la capa de servicio para realizar la acción:

// lib/userController.js
const userService = require('./lib/userService');

module.exports = {
    findUsers: function (req, res, next) {
        userService.findUsers()
            .then(findUsersOk)
            .catch(handleError);
    },
    saveUser: function (req, res, next) {
        userService.saveUser(req.body)
            .then(saveUserOk)
            .catch(handleError);
    }
}

Nos ocurre lo mismo que el caso anterior, estamos muy acoplados a ‘userService’ y por tanto a ‘db’, hacer pruebas de esto se complica porque no es posible aislar la funcionalidad de ‘userController’ sin sufrir las consecuencias.

Lo siguiente es hacer uso de todo en el módulo inicial de la aplicación. Hemos usado express para ello:

// app.js
const express = require('express');
const app = express();
const bodyParser = require('bodyParser');
const userController = require('./lib/userController');

app.use(bodyParser.json());

app.get('/users', userController.findUsers);
app.post('/users', userController.saveUser);

app.listen(3000, function () {
    console.log('Servidor funcionando correctamente');
});

module.exports = app;

No hay mucho más que comentar. No contamos con módulos genéricos, unos dependen de los otros, las responsabilidades están bien definidas, pero el acoplamiento rompe con necesidades futuras, necesitamos desacoplarlo un poco.

Inyectando dependencias

Y una de las maneras puede ser por medio de la inyección de dependencias. La idea de esto trata en, en vez de llamar directamente al módulo por medio de un ‘require’, de delegar esta instanciación a módulos superiores. En vez de su uso a fuego, las dependencias son pasados como parámetros de la función.

Por ejemplo, veamos ahora el módulo de la base de datos. Teníamos el problema de que el módulo estaba muy acoplado con el nombre de la base de datos:

// lib/db.js
const level = require('level');
const sublevel = require('level-sublevel');

module.exports = function (dbName) {
    return sublevel(
        level(dbName, { valueEncoding: 'json' })
    );
}

El refactor lo que hace es exportar ahora una función cuyo parámetros espera el nombre de la base de datos. El módulo ahora es reutilizables porque podemos indicar el nombre de base de datos que queramos. La tecnología de la que depende la base de datos, si las dejamos internas porque eso si nos aísla bien. Sigamos.

En el módulo servicio lo hacemos también con sus dependencias, en vez de llamar directamente al modulo de la base de datos, exportamos una función que espera ese modulo como parámetro:

// lib/userService.js
module.exports = function (db) {
    const users = db.sublevel('users');

    return {
        findUsers: function () {
            return users.get();
        },
        saveUser: function (user) {
            return users.save(user);
        }
    };
}

Nos pasa lo mismo, el módulo es mucho más genérico, menos acoplado. Si yo quisiera, podría incluir cualquier base de datos que cumpla con esta interfaz y seguiría funcionando.

Con el controlador nos pasa lo mismo:

// lib/userController.js
module.exports = function (userService) {
    return {
        findUsers: function (req, res, next) {
            userService.findUsers()
                .then(findUsersOk)
                .catch(handleError);
        },
        saveUser: function (req, res, next) {
            userService.saveUser(req.body)
                .then(saveUserOk)
                .catch(handleError);
        }
    };
}

El lío bien en el módulo de aplicación. Lo que aquí hacemos es ir instanciando los módulos e inyectando las dependencias tal y como las necesitamos y en un orden concreto:

// app.js
const express = require('express');
const app = express();
const bodyParser = require('bodyParser');

const db = require('./lib/db')('example-db');
const userService = require('./lib/userService')(db);
const userController = require('./lib/userController')(userService);

app.use(bodyParser.json());

app.get('/users', userController.findUsers);
app.post('/users', userController.saveUser);

app.listen(3000, function () {
    console.log('Servidor funcionando correctamente');
});

module.exports = app;

La inyección de dependencias funciona muy bien, nos desacopla y nos permite reutilizar y testear nuestro módulos de manera independiente. Ahora es mucho más fácil que yo ‘mockee‘ un módulo que no me interesa, cumpliendo con la interfaz necesaria.

El problema de hacer la inyección de dependencias de esta manera, es la gestión y configuración que estoy llevando a cabo en ‘app.js’. El ejemplo es sencillo, pero imagina una aplicación con más dependencias, se puede hacer inmanejable porque yo tengo que ir instanciando todo en un orden determinado.

Además, podemos sufrir dependencias circulares que pueden hacer la inyección compleja, por lo tanto, es un buen sistema de gestionar dependencias, pero no es algo definitivo, quizá para algo pequeño nos pueda servir, pero en cuanto nuestro sistema crezca, la gestión manual nos complicará las cosas.

Creando un Service Locator

Como vemos, la idea de inyectar dependencias nos gusta, pero la forma de gestionarlas tan manualmente nos impide trabajar cómodos. Sería bueno contar con una librería que nos permitiese hacer esta gestión de una forma menos manual, de no tener que preocuparme del orden en que instancio las dependencias.

Esto lo podemos conseguir creando un servicio llamado ‘Service Locator’. La gestión de dependencias por parte de NodeJS no deja de ser un ‘Service Locator’. ¿Para qué hacer entonces esto? Para tener un mayor control de lo que inyectamos en cada módulo. Aunque NodeJS lo hace muy bien, en muchos casos nos interesará no delegar tanta responsabilidad y crear pequeños servicios que se encarguen de esto.

Para crear este servicio usamos el siguiente código. Es bastante simple, veamos:

// lib/serviceLocator.js

module.exports = function () {
    const dependencies = {};
    const factories = {};
    const serviceLocator = {
        factory,
        register,
        get
    };

    function factory(name, factory) {
         factories[name] = factory;
    }

    function register(name, dependency) {
        dependencies[name] = dependency;
    }

    function get(name) {
        if (!dependencies[name]) {
            const factory = factories[name];
            dependencies[name] = factory && factory(serviceLocator);
            
            if (!dependencies[name]) {
                throw new Error('No existe este módulo en el SL');
            }
        }
        return dependencies[name];
    }

    return serviceLocator;
}

Lo que hacemos es crear una pequeña librería encargada de cachear dependencias y factorías. Las dependencias son aquellas funciones u objetos que no tienen otras dependencias y las factorías son aquellos módulos que tienen dependencias internas y van a necesitar hacer uso del propio ‘Service Locator’.

La función ‘get’ se encarga de devolver un módulo que se encuentre registrado como dependencia. Si no estuviese todavía registrado, lo buscaremos en la factoría y le pasaremos el propio ‘Service Locator’. para que internamente pueda ser usado. Si no existiese, lanzaríamos una excepción, pues el módulo que necesitamos no ha sido registrado.

Vale, pues teniendo esto, lo siguiente es refactorizar los módulos para usen nuestro servicio. Empecemos por el servicio de datos:

// lib/db.js
const level = require('level');
const sublevel = require('level-sublevel');

module.exports = function (serviceLocator) {
    const dbName = serviceLocator.get('dbName');

    return sublevel(
        level(dbName, { valueEncoding: 'json' })
    );
}

Lo que hemos hecho en este caso es inyectar como parámetro el ‘serviceLocator’, internamente hacemos uso del módulo que necesitamos.

Hacemos lo mismo tanto con ‘userService’ como con ‘userController’:

// lib/userService.js
module.exports = function (serviceLocator) {
    const db = serviceLocator.get('db');
    const users = db.sublevel('users');

    return {
        findUsers: function () {
            return users.get();
        },
        saveUser: function (user) {
            return users.save(user);
        }
    };
}

// lib/userController.js
module.exports = function (serviceLocator) {
    const userService = serviceLocator.get('userService');

    return {
        findUsers: function (req, res, next) {
            userService.findUsers()
                .then(findUsersOk)
                .catch(handleError);
        },
        saveUser: function (req, res, next) {
            userService.saveUser(req.body)
                .then(saveUserOk)
                .catch(handleError);
        }
    };
}

Lo que nos queda por hacer es registrar todos lo módulos en el ‘serviceLocator’ en nuestra ‘app.js’:

// app.js
const express = require('express');
const app = express();
const bodyParser = require('bodyParser');
const serviceLocator = require('./lib/serviceLocator')();

serviceLocator.register('dbName', 'example-db');
serviceLocator.factory('db', require('./lib/db'));
serviceLocator.factory('userService', require('./lib/userService'));
serviceLocator.factory('userController', require('./lib/userController'));

app.use(bodyParser.json());

const userController = serviceLocator.get('userController');

app.get('/users', userController.findUsers);
app.post('/users', userController.saveUser);

app.listen(3000, function () {
    console.log('Servidor funcionando correctamente');
});

module.exports = app;

Lo que hacemos es registrar todos aquellos módulos que se van a hacer uso en nuestro aplicación. Cuando sepamos que nuestro módulo no va a tener dependencias, haremos uso del método ‘register’ para registrarlo. Cuando sepamos que un módulo contendrá otras dependencias, es decir que hará un uso interno de ‘serviceLocator’, lo registraremos con el método ‘factory’.

De esta forma, solo nos quedará obtener el módulo con el que se inicializa todo y nuestro sistema volverá a funcionar.

Entre las ventajas que tiene este método, nos encontramos con un desacoplamiento de módulos y con la facilidad de no tener que pasar las dependencias en un orden concreto. El ‘serviceLocator’ se encargará de obtener el módulo que se precise en cada momento.

Por el contrario, este sistema hace difícil ver el árbol de dependencias de nuestro sistema, pues perdemos esa trazabilidad. Además, los módulos se encuentran algo infectados por tener que contar con ese ‘serviceLocator’ como dependencia.

Hemos conseguido un buen sistema, pero no algo definitivo, nos hace falta un sistema algo menos intrusivo, pero que nos permita registrar módulo en cualquier momento, sin tener que contar con un orden concreto.

Creando un contenedor de inyección dependencias

Para conseguir esto, la mejor solución es encontrar algo intermedio entre la inyección de dependencias y el ‘Service Locator’, algo que permita registrar módulos, pero que inyecte estos módulos como parámetros. Como veremos, hay varias formas para conseguir esto, pero lo importante es saber que el módulo se inyectará de manera automática solo con saber el nombre del parámetro.

Esto se puede conseguir creando un contenedor de inyección de dependencias. Este contenedor es una librería muy parecida a la creada en el ‘Service Locator’, pero con una nueva funcionalidad que permita saber que módulo tener que inyectar de manera automática.

La inyección de dependencias automática es una forma que se puso muy de moda en JavaScript gracias a la funcionalidad con la que cuenta AngularJS. La gente de Google añadió un inyector de dependencias automático. Dependiendo del nombre de los parámetros que indicasemos, el framework sabía que módulo debía inyectar como parámetro.

Como vimos hace mucho tiempo en El Abismo, había varias formas de inyectar estos módulos para evitar ciertos problemas a la hora de la ‘minificación’ de un módulo, pero en este caso, vamos a solucionar la forma de inyección automática.

Lo primero que tenemos que hacer es modificar ligeramente el ‘Service Locator’ creado anteriormente:

// lib/containter-di.js
const argsList = require('args-list');

module.exports = function () {
    const dependencies = {};
    const factories = {};
    const di = {
        factory,
        register,
        inject,
        get
    };

    function factory(name, factory) {
         factories[name] = factory;
    }

    function register(name, dependency) {
        dependencies[name] = dependency;
    }

    function inject(factory) {
        const args = argList(factory)
            .map(dependency => di.get(dependency));

        return factory.apply(null, args);
    }

    function get(name) {
        if (!dependencies[name]) {
            const factory = factories[name];
            dependencies[name] = factory && di.inject(factory);
            
            if (!dependencies[name]) {
                throw new Error('No existe este módulo en el CDI');
            }
        }
        return dependencies[name];
    }

    return di;
}

Lo único que hemos cambio es el método ‘get’, que ahora llama a ‘inject’ para que nos inyecte las dependencias de dicho módulo, y el método ‘inject’. Este método lo que hace es obtener el nombre de los parámetros de una función con la librería ‘args-list’ e ir obteniendo su dependencia y ejecutarlo con ellas.

De esta forma, el código del resto de módulos vuelve a ser como el del segundo caso de inyección manual de dependencias. Recordemos como era:

// lib/db.js
const level = require('level');
const sublevel = require('level-sublevel');

module.exports = function (dbName) {
    return sublevel(
        level(dbName, { valueEncoding: 'json' })
    );
}

// lib/userService.js
module.exports = function (db) {
    const users = db.sublevel('users');

    return {
        findUsers: function () {
            return users.get();
        },
        saveUser: function (user) {
            return users.save(user);
        }
    };
}

// lib/userController.js
module.exports = function (userService) {
    return {
        findUsers: function (req, res, next) {
            userService.findUsers()
                .then(findUsersOk)
                .catch(handleError);
        },
        saveUser: function (req, res, next) {
            userService.saveUser(req.body)
                .then(saveUserOk)
                .catch(handleError);
        }
    };
}

El ‘app.js’ cambia poco también. Simplemente llamamos a nuestra nueva librería y registramos todo:

// app.js
const express = require('express');
const app = express();
const bodyParser = require('bodyParser');
const di = require('./lib/container-di')();

di.register('dbName', 'example-db');
di.factory('db', require('./lib/db'));
di.factory('userService', require('./lib/userService'));
di.factory('userController', require('./lib/userController'));

app.use(bodyParser.json());

const userController = diContainer.get('userController');

app.get('/users', userController.findUsers);
app.post('/users', userController.saveUser);

app.listen(3000, function () {
    console.log('Servidor funcionando correctamente');
});

module.exports = app;

Hemos ganado mucho ya que conseguimos módulos desacoplados y sin contaminación de herramientas propias. Nuestros módulos no saben nada de cómo se inyectan las dependencias. Además, ahora puedo ir registrando dependencias en nuestro contenedor sin precisar un orden.

Sigo perdiendo en cuanto a visibilidad del árbol de dependencias. Conseguir ese trazado es difícil con este caso, pero esa legibilidad, se compensa con lo modularizable y reutilizable que es ahora mi sistema.

Es recomendable usar esta forma en tus proyectos. No hace falta que desarrolles una librería de DI si no lo deseas, el ejemplo era una forma de enseñar cómo funcionan estas herramientas. En la comunidad existen muchas opciones que están más que probadas, así que no dudes en hacer uso de ellas.

Conclusión

Cada una de las formas que hemos ido implementando pueden ayudar en conseguir un mejor código. Cada una de las formas mejoran las cualidades de la anterior, sin embargo, hay que tener muy en cuenta que dependiendo del contexto, unas opciones serán más útiles que otras.

El conocer esta forma de aislar componentes, es la primera fase para aprender a crear librerías en JavaScript agnósticas a la plataforma o plugins que puedan ejecutarse de muchas maneras diferentes.

Conocer estas técnicas nos hace entender cómo funciona AngularJS, cómo funcionan los inyectores de dependencias por lo que, aunque tu proyecto sea simple y sigas haciéndolo con el ‘harcodeo’ como toda la vida, piensa que este conocimiento te puede ser útil para el futuro cuando tus aplicativos crezcan.

Nos leemos 🙂

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s