Los Mixins en la Programación Orientada a Objetos moderna (ES6)

12 Dic 2016

Introducción

En el artículo anterior hablamos sobre las nuevas clases en ECMAScript 2015 (aka ES6): una revisión moderna del enfoque prototípico del lenguaje pensada para una Programación Orientada a Objetos más tradicional.

En esta ocasión, vamos a estudiar un patrón de diseño estrechamente relacionando con este paradigma que, si bien no podemos catalogar como moderno, sí que podemos verlo como una reinvención que se beneficia de la nueva sintaxis disponible. Se trata de los Mixins, un sistema estructurado de composición de objetos a partir del cual estos pueden ir sumando funcionalidades a través de una cadena de herencia jerárquica.

En este artículo analizaremos la teoría tras los mixins, las formas de aplicación modernas a partir de las nuevas estructuras del lenguaje, sus limitaciones y varios ejemplos ilustrativos.

¡Vamos con ello!

Qué son exactamente los mixins

El término ‘mixin’ se ha vuelto muy popular en los últimos años dentro de la terminología front end debido a que varios lenguajes los aplican de uno u otro modo. Los encontramos por ejemplo cuando trabajamos con preprocesadores CSS como LESS o SASS, en bibliotecas y diversos frameworks Javascript (bien como parte de su oferta de herramientas, bien para renegar de ellos) y, más directamente relacionado con lo que ahora nos ocupa, en la teoría puntera de arquitectura de sistemas web como serían los Web Components.

Y aunque están presentes en todos estos ámbitos, su definición original está estrechamente ligada con la Programación Orientada a Objetos, habiéndose extendido a partir de ella al resto de sistemas mencionados.

En los lenguajes de programación orientada a objetos, un mixin es una clase que ofrece cierta funcionalidad para ser heredada por una subclase, pero que no está ideada para ser autónoma.

Wikipedia, Mixin

En una terminología más purista, diríamos que los mixins son subclases abstractas que aplicamos sobre diferentes superclases para crear familias relacionadas de clases modificadas.

Hacemos una pausa rápida para refrescar la terminología que acabamos de emplear:

Superclase y Subclase

Los conceptos de superclase y subclase no existen como tipos de objetos concretos sino que se utilizan para definir la relación que existe entre dos clases dadas:

  • Una ‘subclase’ es toda aquella clase que hereda (extiende) de otra; también podemos referirnos a ella como ‘clase hija’.
  • Una ‘superclase’ es aquella clase a partir de la cual heredan (extienden) otras, siendo así también llamada como ‘clase madre’.

Aplicando a estas definiciones la flexibilidad de los objetos, encontramos que cualquier ‘clase hija’, o subclase, puede ser a su vez la ‘clase madre’, o superclase, de otras.

Esquema de composición

Visto lo anterior, podemos hablar de los mixins como fábricas (factorías) de subclases que están parametrizadas por la superclase. De este modo, se crea una cadena de herencia donde encontramos, en este orden, la superclase (la madre), el mixin (la fábrica), y la subclase (la hija).

Ejemplos

A veces, la teoría puede resultar más confusa de lo que supone la práctica, por lo que vamos a ver algunos ejemplos rápidos muy sencillos para ilustrar el concepto.

Siguiendo el ejemplo de otras fuentes, vamos a recurrir en una primera aproximación al lenguaje Dart para explicar estos conceptos: su sintaxis, además de agradable, es muy similar a la de Javascript.

Tomemos dos clases: una Foo que será nuestra superclase o ‘clase padre’ y otra Bar para extenderla:

// Superclass
class Foo { }
 
// Subclass
class Bar extends Foo {}

En un gráfico esquemático, expresaríamos la anterior relación del siguiente modo:

superclass-subclass

Esquema donde Bar extiende de Foo

NOTA: Para la realización de los gráficos, se ha utilizado el servicio online gratuito Draw.io

Introduzcamos ahora un mixin entre ambas:

// Mixin
class M {}
 
// Subclass of Foo-with-M
class Bar extends Foo with M {}

En este fragmento, tenemos a tres actores:

  • Foo como superclase, o ‘clase padre’.
  • M como mixin
  • Bar como subclase, no de Foo, sino de la combinación de M con Foo (a la que llamamos Foo-with-M)

El último punto es clave para comprender el concepto: Foo no es la superclase de Bar, sino que en su lugar, lo es el mixin (la combinación) de Foo-with-M. En un esquema, la relación quedaría así:

super-mixin-sub

Esquema donde Bar extiende a la combinación Foo-with-M

Este gráfico se corresponde, efectivamente, con la definición que dimos anteriormente y donde podemos observar cómo el mixin se introduce jerárquicamente entre la superclase original (Foo) y la subclase (Bar).

Múltiples Mixins

La flexibilidad del sistema permite aplicar varios mixins sobre una misma subclase. Éstos se van añadiendo a la cadena jerárquica siguiendo un estricto orden de izquierda a derecha:

class Bar extends Foo with M1, M2, M3 {}

El siguiente esquema representa la cadena resultante:

multiple

Esquema donde Bar aplica múltiples mixins de forma secuencial

Creando Mixins en Javascript

Vista ya la teoría y ejemplos de sintaxis en Dart, pasemos a su implementación en Javascript.

NOTA: Aunque esta estructura puede conseguirse a través de un Javascript más tradicional, en este artículo nos centraremos en el estándar ES6 y las clases que ya hemos analizado en su correspondiente artículo.

La vida secreta de las clases en Javascript

La sintaxis de clases en ES6 esconde varios trucos que, más allá de resultar anecdóticos, abren la puerta a composiciones de una flexibilidad extraordinaria. Precisamente vamos a explotar estas funcionalidades para el caso que nos ocupa:

  • Además de como instrucciones, las clases pueden ser utilizadas como expresiones. Gracias a esto, es posible crear una nueva clase cada vez que dicha expresión es evaluada.
  • La cláusula extends permite también actuar sobre expresiones, lo cual habilita que una clase extienda de otra creada en tiempo de ejecución mediante la funcionalidad anterior.

Veamos cómo utilizar todo lo anterior para re construir el sistema que habíamos esbozado con Dart:

// Superclass
class Foo {}
 
// Define a mixin
const M = Superclass => class extends Superclass {};
 
// Creating a subclass from mixin
 
class Bar extends M( Foo ) {}

Analicemos las partes:

La superclase

En este caso, la superclase, como se ha explicado con anterioridad, no corresponde a un tipo especial de datos: únicamente se trata de una ‘clase madre’ (o clase base normal) que utilizaremos para extender a partir de ella.

El mixin

En este caso, estamos creando una clase en tiempo de ejecución a partir de una expresión:

const M = Superclass => class extends Superclass {};

Nuestra constante M (siguiendo la sintaxis de las Funciones Flecha) recibe un parámetro (una clase) para devolver inmediatamente una nueva clase que extiende a la indicada.

En esencia, esta función está actuando como una factoría o fábrica de subclases a partir de otra dada.

La subclase

La subclase que definimos ahora está haciendo uso de la segunda funcionalidad extra que comentamos arriba: está extendiendo una expresión, la cual no es otra que nuestra anterior factoría de clases.

class Bar extends M( Foo ) {}

El resultado es que estamos creando una nueva clase en tiempo de ejecución a través de la factoría M, la cual extiende a su vez de otra (Foo) que enviamos como parámetro.

Finalmente, obtenemos una cadena prototípica donde se cumple el esquema con el que iniciamos el apartado teórico: superclase, mixin y subclase.

Mixins múltiples

Sabiendo que podemos extender a partir de factorías, y que éstas son funciones, nada nos impide encadenar declaraciones para obtener mixins múltiples:

class Bar extends M1( M2( M3( Foo ) ) ) {}

En este caso, con cada expresión, se crea una nueva clase que extiende dinámicamente de la anterior, consiguiéndose así la cadena completa.

Ejemplo completo

No es el objetivo de este artículo el enumerar las posibilidades compositivas de este patrón de diseño, sino que la intención es mostrar cómo implementarlo de forma efectiva.

Para ilustrar en un solo código lo ya visto, recurriremos a un esquema similar al que podría utilizar una base de datos tipo Wikipedia para catalogar a los artistas.

Partiendo de una clase Person, y un par de mixins con funcionalidades básicas, crearemos mediante composición una subclase Artist que herede todo lo anterior de forma conjunta:

// Superclass
class Person {
    constructor ( params = {} ) {
      ( {
          name: this._name = 'Unknown',
          lastName: this._lastName = 'Unknown',
          birthDate: this._birthDate = '1970/01/01'
      } = params );
    }

    get fullName () {
        return `${ this._name } ${ this._lastName }`;
    }
}

// Mixins
const Storage = Superclass => class extends Superclass {
    save ( database = 'Unknown' ) {
        return `Saving data into database: ${ database }`;
    }
};

const Validation = Superclass => class extends Superclass {
    validate ( schema = {} ) {
        return 'Validating Schema...';
    }
};

// Subclass
class Artist extends Storage( Validation( Person ) ) {
    constructor ( params = {} ) {
        super( params );

        ( {
            movement: this._movement = 'Unknown',
            knownFor: this._knownFor = [],
            notableWorks: this._notableWorks = []
        } = params );

    }

    get fullName () {
        return `${ this._name } ${ this._lastName }, (${ this._birthDate })`;
    }

    get notableWorks () {
        return `Notable Works: ${ this._notableWorks.join( ', ' ) }`;
    }
}

// Creating an instance of Artist
let data = {
    name: 'Leonardo',
    lastName: 'da Vinci',
    birthDate: '15/04/1452',
    movement: 'High Renaissance',
    knownFor: [ 'Art', 'science' ],
    notableWorks: [ 'Mona Lisa', 'The Vitruvian Man', 'Lady with an Ermine' ]
};

let p1 = new Artist( data );
p1.fullName; // Leonardo da Vinci, (15/04/1452)
p1.save( 'ArtistsDB' ); // Saving data into database: ArtistsDB
p1.validate(); // Validating Schema...
p1.notableWorks; // Notable Works: Mona Lisa, The Vitruvian Man, Lady with an Ermine

NOTA: Todo el código anterior es editable, por lo que se invita al lector a jugar con los datos y métodos para observar los cambios en caliente.

Herencia

Como consecuencia de la arquitectura anterior, podemos observar que los métodos de la subclase, o clase hija, sobreescriben los declarados previamente tanto en la superclase como en los mixins de los que extiende.

Esto, conforme al paradigma POO, otorga una gran flexibilidad compositiva. Tomemos el siguiente ejemplo:

// Superclass
class Foo {
    log () {
        console.info( 'Logging from Foo' );
    }
}

// Mixin M1
const M1 = Superclass => class extends Superclass {
    log () {
        console.info( 'Logging from M1' );
    }
};

// Mixin M2
const M2 = Superclass => class extends Superclass {
    log () {
        console.info( 'Logging from M2' );
  }
};

// Subclass
class Bar extends M1( M2( Foo ) ) {
    log () {
        console.info( 'Logging from Bar' );
    }
}

// Instance
let myInstance = new Bar( Foo );
myInstance.log(); // Logging from Bar

Aquí podemos comprobar cómo aunque la superclase y mixins poseen métodos homónimos (con el mismo nombre), es el de la subclase el que se impone sobreescribiendo al resto.

Si este comportamiento no es el esperado, recurrimos al siguiente apartado: la composición.

Composición

En la Programación Orientada a Objetos, el concepto de composición es una relación a través de la cual un objeto puede ser utilizado dentro de otro objeto.

La composición (también conocida como relación asociativa) es un tipo de relación que se establece entre dos objetos que tienen comunicación persistente. Se utiliza para expresar que un par de objetos tienen una relación de dependencia para llevar a cabo su función, de modo que uno de los objetos involucrados está compuesto por el otro.

Wikipedia, Objeto (programación)

Para nuestra arquitectura, la composición viene a subrayar que, independientemente de cómo se organicen internamente los objetos (las clases y sus mixins), el sistema puede invocar a cada uno de sus métodos desde la subclase (su instancia).

En un escenario donde componemos nuestras clases a partir de mixins independientes, cada uno de éstos no tiene porqué saber nada sobre los métodos de los demás. Acabamos de verlo en el ejemplo anterior: cada mixin posee un método log que es sobreescrito tras cada relación.

Para preservar la funcionalidad, podemos utilizar la instrucción super la cual protege y habilita las super llamadas a través de la cadena jerárquica:

// Superclass
class Foo {
    log() {
        console.info( 'Logging from Foo' );
    }
}

// Mixin M1
const M1 = Superclass => class extends Superclass {
    log() {
        console.info( 'Logging from M1' );

        super.log && super.log();
    }
};

// Mixin M2
const M2 = Superclass => class extends Superclass {
    log() {
        console.info( 'Logging from M2' );

        super.log && super.log();
    }
};

// Subclass
class Bar extends M1( M2( Foo ) ) {
    log() {
        console.info( 'Logging from Bar' );

        super.log();
    }
}

// Instance
let myInstance = new Bar();
myInstance.log();

// Logging from Bar
// Logging from M1
// Logging from M2
// Logging from Foo

La clave aquí está en el uso del concepto de super llamada aplicado tras cada acción:

console.info( 'Logging from XX' );
 
super.log && super.log();

NOTA: La super llamada se debe aplicar tras al final de la propia lógica del método para preservar así el orden compositivo. Del mismo modo, es siempre aconsejable aplicarla mediante un condicional como en el fragmento anterior (en este caso mediante una evaluación perezosa). La estructura sería equivalente a:

if ( super.log ) {
    super.log();
}

Como resultado, podemos tener así componentes (mixins) completamente independientes entre sí que nos permitan construir sistemas basados en la composición y agregación de elementos reutilizables.

Importante: las reglas del super y this

Al definir una subclase, es necesario recordar que tenemos que cumplir dos reglas:

  • En el constructor de una clase hija, es obligatorio llamar a super antes de utilizar this.
  • Los constructores de clases en ES6 DEBEN llamar a super siempre que sean subclases, o deben devolver de forma explícita algún objeto que reemplace a aquel que no ha sido inicializado.

Mejorando la sintaxis

Basándonos en los experimentos de Justin Fagnani y el Dr. Rauschmayer, podemos mejorar la sintaxis anterior buscando un sistema más fluido y funcional.

La idea es construir una API más semántica para trabajar con clases y mixins tal y como lo hacen otros lenguajes de programación de forma nativa.

const mix = Superclass => new MixinBuilder( Superclass );
 
class MixinBuilder {
    constructor( superclass ) {
        this.superclass = superclass;
    }
 
    with ( ...mixins ) {
        return mixins.reduce( ( c, mixin ) => mixin( c ), this.superclass );
    }
}

El código anterior nos permite reescribir ahora nuestra clase derivada así:

class Bar extends mix( Foo ).with( M1, M2 ) {
    /* ... */
}

Limitaciones

No todo es perfecto y simple. Esta arquitectura de mixins tiene algunas limitaciones importantes que exigen de malabares más complejos para salvarlas.

Caché de mixins

El propósito de los mixins es el de aplicarlos sobre diferentes superclases por lo que no deberían requerir de una jerarquía de herencia específica. Sin embargo, es posible (y habitual) que varias subclases necesiten de los mismos conjuntos de superclases y mixins:

class Bar1 extends M1( M2( M3( Foo ) ) ) {}
class Bar2 extends M1( M2( M3( Foo ) ) ) {}

Debido a nuestro sistema, nos encontramos aquí con que para cada subclase, se crean dos prototipos idénticos de Foo-with-M1-with-M2-with-M3. Ilustrar esto requiere de un gráfico complejo y redundante:

multiple-no-cache

Cadenas prototípicas duplicadas (sin caché) para cada subclase

Implementar una caché excede el objetivo de este artículo y queda pendiente para un futuro análisis.

Instanceof

Otro inconveniente es que, lamentablemente, la instrucción instanceof no resulta útil cuando trabajamos con estas arquitecturas. Cabría esperar que una subclase, creada a partir de una superclase y un mixin, reflejara ser una instancia de éste último:

class Bar extends M1( Foo ) {}
 
let myInstance = new Bar();
 
myInstance instanceof Foo // true
myInstance instanceof M1 // Error 'prototype' property of M1 is not an object

A día de hoy, no hay forma de conseguir una fórmula consistente que devuelva la instancia correcta de una subclase dentro de su cadena prototípica.

Conclusión

Los mixins son una herramienta muy poderosa en la Programación Orientada a Objetos. En el ecosistema web actual, estos componentes permiten estructurar arquitecturas basadas en un modelo de composición donde las funcionalidades se comparten entre cada uno de los miembros de una manera eficiente y transparente.

Si bien en el artículo anterior hablábamos de cómo la POO había recobrado vigencia en los últimos años gracias a los avances sintácticos del lenguaje, con esta nueva introducción esperamos proporcionar más herramientas con las que afrontar los desafíos actuales: elementos, componentes, mixins, composición y herencia; términos todos ellos reactualizados que debemos conocer y manejar en nuestro día a día.

Más:

{2} Comentarios.

  1. Ed

    Estábamos debatiendo con un compañero la diferencia entre usar mixins vs object.assign. ¿Cual crees que seria el mejor ejemplo para decidir cual implementar?

    • Carlos Benítez

      La pregunta es interesante, pero la cuestión aquí es que se están mezclando sintaxis:

      object.assign sirve para copiar las propiedades enumerables de un objeto dentro de otro. En la práctica, eso querría decir que, efectivamente, podríamos coger uno que funcione como clase (un patrón módulo tradicional por ejemplo) y copiar sus métodos en un segundo objeto para crear así una suerte de mixin.

      El problema, es que en ES6, una clase (class) no es un objeto y, en consecuencia, no tiene propiedades enumerables que podamos clonar/copiar con object.assign. Si utilizamos esta nomenclatura, o este azúcar sintáctico, tenemos que utilizar sus propias reglas: en este caso, heredar/extender mediante la instrucción extend.

      Los beneficios de este último sistema frente al primero más tradicional es que podemos utilizar constructores en los mixins y super llamadas (tantos en estos, como en las subclases). Con un object.assign, no tendríamos composición ya que, en caso de colisión con los nombres de las propiedades, éstas se sobreescriben unas a otras. Es ahí donde super hace la magia.

      Por lo tanto, el beneficio de utilizar clases es su flexibilidad nativa, mientras que los objetos tradicionales, requieren de mucho código ad-hoc para lograr comportamientos similares.
      Saludos!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *