Virguerías en JavaScript II - El patrón Builder

Citando a la Wikipedia, el patrón Builder es un patrón de diseño, usado para permitir la creación de una variedad de objetos complejos desde un objeto fuente.

Se compone de un Director que es el componente encargado de crear y utilizar el objeto Builder, el propio Builder que será el encargado de ir construyendo el objeto deseado, y el Product que no es más que el objeto construido, una vez que se invoca a la función getResult() del Builder.

Las implementaciones más comunes que he visto en C# y Java suelen consistir en una clase que encapsula al producto y expone una interfaz mediante la cual ir dando forma al mismo desde el exterior, impidiendo la modificación directa de dicho producto (ya que si no, no tendría gracia ninguna lo del Builder).

Pasando de las clases

Una cosa que me está gustando de JS es la libertad que tiene a la hora de abordar casos como este en concreto, permitiéndome ignorar el uso de clases e implementar un Builder únicamente con funciones.

Si alguien me preguntase cuál es la ventaja en este caso de usar solo funciones en detrimiento de las clases, diría que me parece que el código queda mucho más limpio y conciso, además de que te ahorra escribir bastante. Además no tengo que implementar clases abstractas o interfaces cada vez que quiera crear un builder nuevo. En suma, me parece que es más flexible (dicha flexibilidad mal usada puede ser peligrosa, ojo aquí).

Volviendo al ejemplo de las pizzas de la Wiki, mi implementación del builder de pizza Hawaiana quedaría tal que así:

const hawaiPizzaBuilder= () => {  
    const pizza = {};

    const builder = () => {
        return pizza;
    };

    builder.buildMasa = () => {
        pizza.masa = 'suave';
        return builder;
    };

    builder.buildSalsa = () => {
        pizza.salsa = 'dulce';
        return builder;
    };

    builder.buildRelleno = () => {
        pizza.relleno = 'chorizo+alcachofas';
        return builder;
    };

    return builder;
};

Como se ve, el builder no es más que una función, que tiene dentro el producto (la pizza) y que acaba devolviendo una función que es la función constructora getResult() del ejemplo en Java de la Wiki. Aquí en verdad me aprovecho del hecho de que, en JS, las funciones son en realidad otro tipo más de objeto al que se le pueden enganchar más funciones. El uso del buider sería algo así:

const buildPizza = hawaiPizzaBuilder();  
buildPizza.buildMasa().buildSalsa().buildRelleno();  
const pizza = buildPizza();  

Me parece una forma super limpia y fácil de crear un builder usando solo funciones.

Dándole una vuelta de tuerca

Personalmente yo no suelo usar los builder como en este ejemplo, y suelo usar builders cuyas funciones de construcción de partes reciben parámetros. Un ejemplo sería un builder para construir criterios de filtrado para un buscador, por ejemplo:

const filterBuilder = () => {  
    const filterCriterias = {};

    const builder = () => {
        return filterCriterias;
    };

    builder.withUser = (userName) => {
        filterCriterias.userName = userName;

        return builder;
    };

    builder.withTelephoneNumber = (telephone) => {
        filterCriterias.telephone = telephone;

        return builder;
    };

    builder.withEmail = (email) => {
        filterCriterias.email = email;

        return builder;
    };

    builder.and = builder;

    return builder;
};

Con este builder puedo construir un objeto filterCriterias que podría usar a la hora de filtrar una lista de registros de usuarios. Como extra he agregado al builder una propiedad and con el único objetivo de tener una API más legible a la hora de usar el builder:

const buildFilter = filterBuilder();  
buildFilter.withUser('Gabriel').and.withEmail('user@gabrielferreiro.com');  
const filter = buildFilter();  

Tipando para más seguridad

Entiendo que esto así tal cual mola (o al menos a mi me mola), pero si no se tiene cuidado esto no deja de ser JavaScript, y uno puede perder el control muy fácilmente, así que si quiero ir sobre seguro y apoyarme al menos en una interfaz o estructura similar a la hora de crear mi builder, puedo servirme de TypeScript para tenerlo todo bien atado y reducir las posibilidades de desastre. Siguiendo con el ejemplo del filtro, este builder puede tiparse muy fácilmente en TypeScript de la siguiente forma:

interface FilterCriterias {  
    userName: string;
    telephone: string;
    email: string;
}

interface FilterBuilder {  
    (): FilterCriterias;
    and: FilterBuilder;
    withUser: (userName: string) => FilterBuilder;
    withTelephone: (telephone: string) => FilterBuilder;
    withEmail: (email: string) => FilterBuilder;
}

En la interfaz para el builder lo primero que choca un poco es la declaración (): FilterCriterias. En realidad lo que significa esta línea para TypeScript es que el objeto que implemente esta interfaz es una función, que al ejecutarse devolverá un objeto FilterCriterias.

const filterBuilder = (): FilterBuilder => {  
    const filterCriterias: FilterCriterias = {
        userName: '',
        telephone: '',
        email: ''
    };

    const builder: any = () => {
        return filterCriterias;
    };

    builder.and = builder;

    builder.withUser = (userName: string) => {
        filterCriterias.userName = userName;

        return builder;
    };

    builder.withTelephone = (telephone: string) => {
        filterCriterias.telephone = telephone;

        return builder;
    };

    builder.withEmail = (email: string) => {
        filterCriterias.email = email;

        return builder;
    };

    return <FilterBuilder>builder;
};

Aquí toca hacer algo de "trampa" para deshabilitar el chequeo de tipos de TypeScript a la hora de construir el builder tipando dicha variable al tipo any para que "cuele". De no hacer esto TypeScript empezará a quejarse porque la propiedad and y los métodos with... no existen en el tipo () => FilterCriterias de la variable builder. A pesar de esta pequeña trampa, nos aseguramos de devolver lo que especifica la firma del método realizando un último cast del builder al tipo correcto.

Concluyendo...

La flexibilidad que da JS a veces resulta en un caos descontrolado pero otras veces te permite tomar otros caminos para problemas cotidianos que resultan en código más simple y corto de escribir que su contrapartida más habitual u ortodoxa.

Gabriel Ferreiro

Read more posts by this author.

Subscribe to Gabriel Ferreiro

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!