Patrones JavaScript encontrados en tu API

28580064681_6b328e0973_o.jpg

Foto de Paul Hudson

Cuando miras el código de una de las librerías que usas en tu día a día, aprendes mucho a cómo otros desarrolladores solucionan un problema en particular. Ese conocimiento puede ser usado por ti en el futuro para que, dado un problema parecido, tu puedas adaptar esa solución y te sea útil en tu desarrollo.

Sin darte cuenta, has creado una pieza de conocimiento que puede ser usada siempre que te encuentres con este problema en concreto. Has usado un patrón. Al igual que pasa en lenguajes como Java o C#, en JavaScript es muy común hacer uso de patrones de diseño.

Los patrones de diseño ayudan a crear un repositorio de ‘problema-solución’ que la mayoría de desarrolladores ha experimentado. Este repositorio nos ayuda a crear un lenguaje común entre desarrolladores y nos permite ordenar el conocimiento y nuestras experiencias.

Como no me gusta solo quedarme en el uso de librerías, sino que me parece importante entenderlas, hoy explicaremos 5 patrones de diseño que se suelen encontrar en nuestras librerías de NodeJS favoritas. Adelante:

El patrón Factory

Ninguno de los patrones que vamos a explicar es complejo, sin embargo este se convierte en uno de los más simples que nos vamos a encontrar. Casi se convierte en una buena práctica.

Es recomendado, en JavaScript, no instanciar objetos directamente en nuestra lógica. Veamos como podemos instanciar un objeto en JavaScript:

const client = new Client();

Esta instanciación en sí, no tiene ninguna maldad. No está mal. Sin embargo, envolver esta instanciación dentro de una función, puede resolvernos problemas en el futuro:

function createClient() {
    return new Client();
}

const client = createClient();

Sin darnos cuenta, hemos abstraído la forma en que se crea una entidad cliente. Hemos separado con una simple función el qué del cómo de nuestro código. Dentro de mi lógica de negocio, puede crear todos los clientes que yo necesite, sin importarme cómo se generan estos objetos. Simplemente sé que si ejecuto esa función recibo un objeto de tipo cliente.

Esto es lo que se conoce como patrón Factory. Lo que hacemos es que una función se encarga de la creación de objetos, los fabrica. Lo bueno que tiene esto es que si las especificaciones de cómo se crea un cliente dentro de nuestro negocio cambian, simplemente tendré que ir a nuestra función factoría.

Otro buen uso de las factorías es que podemos jugar con los parámetros para generar objetos más específicos. Imaginemos que tenemos dos tipos de cliente en nuestro sistema. Dependiendo de si el cliente es menor o mayor de edad podríamos querer crear especialidades. Dentro de la aplicación sigue siendo un cliente, pero internamente ese cliente puede comportarse completamente diferente. Podemos modificar nuestra factoría de esta manera:

functión createClient(isSenior) {
    return isSenior ? new SeniorClient() : new JuniorClient();
}

const client = createClient();

Es una buena abstracción que nos cuesta poco y que puede ayudarnos. Veremos que muchos patrones se basan en la factoría para llevarse a cabo.

Dentro del ecosistema de NodeJS, muchas librerías basan la creación de objetos en el uso del patrón factoría. Algunos casos muy famosos son:

Express es una librería que nos devuelve un objeto por medio de una factoría. El propio objeto nativo de NodeJS http.server, es una factoría. Restify también se genera por medio de una factoría. La gran mayoría de librerías relevantes se inicializan por medio de factoría.

El patrón Proxy

Captura de pantalla de 2017-04-11 10-28-35.png

Este patrón nos puede ayudar mucho a proteger nuestro objeto. Imaginemos que queremos validar los parámetros de entrada o de salida de un método o que queremos poner una traza para saber que está ocurriendo. Podemos poner precondiciones o postcondiciones dependiendo de lo ocurrido.

El proxy puede sernos útil también a la hora de generar un objeto de manera perezosa. Podemos delegar el comportamiento al proxy y que este se encargue de la instanciación solo cuando sea necesaria.

Al final un proxy, es una forma de proteger un acceso. Para implementarlo podemos hacerlo por composición. Es decir, dada una nueva funcionalidad, yo compongo el objeto final que necesito.

function createProxy(subject) {
    const proto = Object.getPrototypeOf(subject);

    function Proxy (subject) {
        this.subject = subject; 
    }
    Proxy.prototype = Object.create(proto);

    Proxy.prototype.doAction1 = function () {
        return this.subject.doAction1();
    }
     
    Proxy.prototype.doAction2 = function () {
        return this.subject.doAction2
            .apply(this.subject, arguments);
    }

    return new Proxy(subject);
}

Lo que hacemos es crear una factoría que internamente crea una función constructora, se incluye el prototipo del sujeto a expandir y se va modificando los métodos que necesitemos interceptar. Los métodos que no necesitemos interceptar los delegamos directamente. Por último, devolvemos una instancia de esta nuevo objeto.

Si queremos simplificarlo, podemos hacer uso de un closure también:

function createProxy(subject) {
    return {
        doAction1: function () {
             return subject.doAction1();
        },
        doAction2: function () {
             return subject.doAction2.apply(subject, arguments);
        }
    };
}

La otra forma es por medio lo que se conoce como monkey patch. Es la técnica que nos permite sobre escribir métodos de un objeto sin perder el funcionamiento por defecto. Es otra forma de extender el comportamiento.

function createProxy(subject) {
    const doAction1Orig = subject.doAction1;
    
    subject.doAction1 = function () {
         return doAction1Orig.apply(this, arguments);
    }

    return subject;
}

Lo que hacemos es cachear el comportamiento por defecto y encapsularlo con la nueva funcionalidad. Es un método menos recomendado porque estamos modificando el objeto original.

El modelo en composición se encarga de crear una nueva copia, lo que puede que nos sea útil. Lo malo de la composición es que tendremos que ir cambiando el comportamiento de todos los métodos aunque no nos interese su extensión o su protección.

Una librería muy conocida que implementa el patrón Proxy es Mongoose. Este ODM de MongoDB para NodeJS permite interceptar los métodos de creación o guardado de objetos en base de datos, por ejemplo para incluir precondiciones y postcondiciones.

El patrón Decorator

Captura de pantalla de 2017-04-11 10-29-29.png

El patrón decorator es muy parecido al patrón proxy. Lo que intenta hacer este patrón es añadir nueva funcionalidad a un objeto o componente determinado. El proxy simplemente encapsula y protege la funcionalidad por defecto.

Su implementación por medio de composición es bastante parecida a la anterior:

function decorate(component) {
    const proto = Object.getPrototypeOf(component);

    function Decorator (component) {
        this.component = component; 
    }
    Decorator.prototype = Object.create(proto);

    Decorator.prototype.doNewAction = function () {
        return 'Hello World';
    }

    Decorator.prototype.doAction1 = function () {
        return this.component.doAction1
            .apply(this.component, arguments);
    }

    return new Decorator(component);
}

El sistema por extensión es bastante simple. Lo que hacemos es incluir un nuevo método al objeto:

function decorate(component) {
    component.doNewAction = function () {
         return 'Hello World';
    }
    
    return component;
}

Tenemos los mismos problemas que con el patrón proxy. Si decidimos hacerlo por composición, tenemos que manipular cada uno de los métodos del objeto a decorar. Si lo hacemos por extensión, estamos modificando siempre el mismo prototipo todo el rato y puede que no necesitemos eso en muchos casos.

Hay un decorador muy famoso que es lodash-plus. Se encarga de incluir nueva funcionalidad a nuestra librería funcional favorita Lodash. Todos los plugins que usamos en jQuery, son extensiones que se hacen por medio del patrón Decorator también.

El patrón Adapter

Captura de pantalla de 2017-04-11 10-30-52.png

El patrón adapter explica muy bien la naturaleza del mismo. Necesitamos crear una interfaz estandarizada para que siempre que realicemos esa acción se comporte con esa firma. Puede que usemos varias librerías o código externo que no se adapten a nuestras necesidades y que este padrón nos ayude a esta adaptación.

Se trata de un patrón encargado de homogeneizar interfaces. Nos permite desacoplar plugins y librerías haciendo solo uso de la API de la misma en zonas muy localizadas. Si el día de mañana cambia la API, mi aplicación no se ve afectada.

Podemos crear una pequeña API para hacer llamadas AJAX como la siguiente. Es una librería tiene el método get y el método post. Internamente quiero hacer uso de librerías que ya existen como $.ajax o la API nativa de ES6 fetch.

De la siguiente manera, podríamos abstraerla y conseguir un sistema homogéneo.

function createAjaxAdapter(library) {
    const optionsDefault = {
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        }
    };

    return {
        get: (url, options) => library(url, Object.assign(optionsDefault, options, { method: 'GET' })),
        post: (url, options, payload) => library(url, Object.assign(optionsDefault, options, { method: 'POST', body: JSON.stringify(payload) }))
    };
}

const ajax1 = createAjaxAdapter(fetch);
ajax1.get('https://api.github.com/gists')
    .then(response => response.ok 
        ? response.json() 
        : Promise.reject({ status: response.status }))
    .then(console.log);

const ajax2 = createAjaxAdapter($.ajax);
ajax2.get('https://api.github.com/gists')
 .then(console.log);

Como ejemplo de librería que hace uso de Adapters tenemos LevelUP. Esta librería es un adaptador de LevelDB una base de datos clave/valor creada por Google y que cuenta con muchas implementaciones para adaptar diferentes partes de su API. Aquí puede encontrarlos.

El patrón Strategy

Captura de pantalla de 2017-04-11 10-31-50.png

Puede darse la situación en la que tengamos que realizar ciertas acciones que dependiendo de un contexto tenga que ejecutarse de una manera o de otra. Es el típico caso en el que el código se empieza a llenarnos de if-else para realizar diferentes acciones.

El patrón estrategia nos permite ejecutar unas acciones determinadas dependiendo de un contexto dado.

El patrón es bastante simple:

class Context {
    constructor(strategy) {
        this.strategy = strategy;
    }

    doAction() {
        this.strategy.doActionStrategy();
    }
}

const strategy1 = {
    doActionStrategy: function () {
        console.log('Hello World');
    }
};

const strategy2 = {
    doActionStrategy: function () {
        console.log('Bye World');
    }
};

const obj1 = new Context(strategy1);
const obj2 = new Context(strategy2);

obj1.doAction(); // 'Hellor World'
obj2.doAction(); // 'Bye World'

Dado un contexto donde puedo ejecutar acciones, yo puedo ejecutar lo que yo necesite dependiendo de los cambios en la aplicación.

Un caso muy simple puede ser el de homogeneizar la forma en que llamas al storage. Puede que necesitemos guardar datos en local o en sesión. Usando el patrón Strategy se nos puede reducir el código y conseguir una API bastante simple que solo cambia dependiendo de si queremos guardado local o no:

function getStorage(isLocal) {
    class Storage {
        constructor(strategy) {
            this.strategy = strategy;
        }

        set(key, value) {
            this.strategy.setItem(key, value);
        }

        get(key) {
            return this.strategy.getItem(key);
        }
    }

    return isLocal 
        ? new Storage(localStorage) 
        : new Storage(sessionStorage);
}

const store1 = getStorage(true);
store1.set('Hello', 'World');
store1.get('Hello');

const store2 = getStorage(false);
store2.set('Hello', 'World');
store2.get('Hello');

Una de las librerías más míticas que hacen uso del patrón Strategy es la librería Passport. Esta librería permite indicar la estrategia a seguir para autenticar a un usuario en una API o un sistema.

El patrón que usa la librería hace que incluir nuevas estrategias sea bastante cómodo y la abstracción de las acciones muy mantenible. Aquí puedes ver todas las estrategias que Passport puede usar.

Conclusión

Nos hemos salido un poco de todos los estudios y análisis funcionales que hemos ido haciendo en el blog para centrarnos en patrones de POO. Como vemos, estos patrones en JavaScript son más simples de implementar y su flexibilidad hace que se necesite menos fontanería debido a su naturaleza débilmente tipada.

Habrá gente que estos patrones no les aporte nada en un contexto como JavaScript y habrá a otros que su propia familiaridad les hará dar el paso definitivo hacia un lenguaje como este.

Sea como fuera, conocer estos patrones no es por gusto y capricho, si no que ayuda a entender mejor el ecosistema tan variado en el que nos movemos y nos ayuda a comprender muchas de las decisiones de las librerías que usamos, por tanto es algo necesario y que nos puede ayudar en el futuro.

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