Mixins en Java y Java8 !Sí, es posible!

1
9382

Mixins en Java y Java8 !Sí, es posible!

Creación: 20–02–2015

Índice de contenidos

1. Introducción
2. Entorno
3. Java Mixin, la implementación manual
4. Java Mixin, en runtime, gracias a un proxy dinámico
4.1. Ejemplo de uso de la librería java-mixins
4.2. La clave de la librería java-mixins
5. Java Mixin con Java 8
6. Conclusiones
7. Sobre el autor

1. Introducción

Un Mixin es una forma de incluir métodos de una clase en otra, sin que exista relación de herencia entre ellas. En cierto sentido se puede ver como una especie de “herencia” múltiple, pero sin existir relación de especialización entre las clases.

De esta definición la parte más importante es la de que no existe relación de herencia, ya que, si bien el lenguaje Java no soporta directamente los mixins, es precisamente esta falta de relación de herencia la que nos va a permitir implementarlos (si fuera necesaria una relación de herencia entonces sí que sería totalmente imposible implementarlo en Java, ya que Java sólo permite herencia simple).

Antes de seguir avanzando vamos a definir un poco más que es un mixin, comparándolo con una interfaz y un trait:

  • Interfaz – sólo tiene definición de métodos
  • Trait – tiene definición de métodos + implementación de los mismos
  • Mixin – tiene definición de métodos + implementación + estado

Aquí vemos otro aspecto importante de los mixins, y es que pueden tener estado. Es decir, las clases que vamos a usar para componer el mixin pueden tener atributos que serán “añadidos” al mixin.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15’’ (2.3 GHz Intel i7, 16GB 1600 Mhz DDR3, 500GB Flash Storage).

  • NVIDIA GeForce G7 750M

  • Sistema Operativo: Mac OS X Lion 10.10.2

  • Java Virtual Machine (JVM) 7 y 8

3. Java Mixin, la implementación manual

Si no existe relación de herencia podemos ver un mixin como una especie de componsición y delegación:

Java Mixin manual
Java Mixin manual

Vemos como la clase Mixture es la mezcla de extender la clase Original e implementar la interfaz Mixin, delegando la implmentación y ejecución de los métodos de esta interfaz en la clase MixinDelegate. MixinDelegate es la clase que estamos inclullendo en la Original a modo de mixin.

En código puede quedar algo similar a:

class Original {
    private int foo = 42;
    public int getFoo() { return foo; }
}

interface Mixin {
    void print();
}

class MixinDelegate implements Mixin {
    private final Object original;
    MixinDelegate(Object original) { this.original = original; }

    @Override
    public void print() {
        System.out.println(original.getFoo());
    }
}

class Mixture extends Original implements Mixin {
    private final Mixin mixin = new MixinDelegate(this);

    @Override
    public void print() {
        mixin.print();
    }
}

class Main {
    public static final main(String[] args) {
        Mixture mixture = new Mixture();
        mixture.getFoo();
        mixture.print();
    }
}

En este ejemplo cabe destacar como en las líneas 32-33 estamos usando tanto los métodos de Original como los de Mixin, así que podemos decir que hemos conseguido el efecto que deseábamos.

Esta implementación tiene dos grandes problemas:

  1. Si la interfaz Mixin tiene muchos métodos, vamos a tener mucho código duplicado en la clase Mixture (como el de las líneas 23 a 26), además de que lo vamos a tener que copiar en cada nueva clase mezcla que queramos hacer.

  2. Para cada mezcla tenemos que crear una nueva clase (como la clase Mixture) que tiene que conocer los mixins que le queremos aplicar al original (líneas 21 y 25). Es decir, hay demasiado acoplamiento entre el mixin y la clase original donde se están incluyendo los métodos.

 4. Java Mixin, en runtime, gracias a un proxy dinámico

He creado una pequeña librería que, utilizando sólo los mecanismos que proporciona Java, nos permite realizar mixtures en runtime y sin perder el tipado fuerte, corrigiendo, además, los dos problemas que comentábamos en el apartado anterior. Es decir no tenemos que escribir código repetitivo y sobre todo, la clase original y el mixin no se conocen de nada.

La librería Java Mixin la podés encontrar en el GitHub de de Autentia.

El siguiente diagrama representa el ejemplo que vamos a utilizar:

Java Mixin con Proxy dinámico

Igual que en el apartado anterior vemos como un mixin se compone de una interfaz y una clase que implementa los métodos de esta y sobre la que, la clase original, delegará la ejecución. Así en el ejemplo podemos identificar dos mixins:

  • Entity, formado por la interfaz Entity y la clase delegado EntityDelegate.
  • MixtureInspector, formado por la interfaz MixtureInspector y la clase delegado MixtureInspectorDelegate.

En el diagrama vemos como al final, el Proxy dinámico poweredPerson cumplirá las interfaces de Person, Entity y MixtureInspector, por lo que sobre este objeto podremos llamar a cualquier método de estas interfaces.

4.1. Ejemplo de uso de la librería java-mixins

Pero tranquilos porque aunque parece complicado, la librería que he preparado va a hacer que el uso sea muy sencillo. Veámoslo con un ejemplo de uso:

final Person originalPerson = new SimplePerson("John", "Doe");

final Person person = new MixerBuilder(Person.class)
        .include(new Mixin(Entity.class, EntityDelegate.class))
        .include(new Mixin(MixtureInspector.class, MixtureInspectorDelegate.class))
        .build()
        .mixWith(originalPerson);

En este código podemos hacer los siguietnes comentarios:

  • línea 1 – creamos el objeto donde queremos aplicar los mixins. Es una creación normal, de hecho la podríamos haber hecho directamente en la línea 7, pero he preferido separarla en una variable local para que quede más claro que se trata del objeto original.
  • línea 3 – creamos el MixerBuilder indicando el tipo de los mixtures que devolverá el Mixer.
  • línea 4 y 5 – creamos los Mixin y los incluimos en el MixinBuilder. Aquí queda claro como cada mixin es una pareja de interfaz más una clase delegada que implementa los métodos.
  • línea 6 – construimos el Mixer.
  • línea 7 – le decimos al Mixer sobre que instancia queremos aplicar los mixins.

Ya que hemos separado la construcción del Mixer de la construcción de las mixtures (mezcla del objeto original más los mixins), podemos cachear fácilmente el Mixer para reutilizarlo en la construcción de muchos mixtures. Veamos un ejemplo:

// Builds the Factory just one time
final Mixer mixer = new MixerBuilder(PoweredPerson.class)
        .include(new Mixin(Entity.class, EntityDelegate.class))
        .include(new Mixin(MixtureInspector.class, MixtureInspectorDelegate.class))
        .build();

// Builds all the mixtures that you want!
final PoweredPerson poweredPerson1 = mixer.mixWith(new SimplePerson("John", "Doe"));
final PoweredPerson poweredPerson2 = mixer.mixWith(new SimplePerson("Jane", "Doe"));
final PoweredPerson poweredPerson3 = mixer.mixWith(new SimplePerson("Joe", "Public"));

En los tests de la librería se pueden encontrar más ejemplos de uso.

4.2. La clave de la librería java-mixins

Os animo a que echéis un vistazo a todo el código, pero podríamos decir que el quid de la cuestión está en el método privado createProxy de la clase Mixer:

private Object createProxy(final Object original, final Map<Class<?>, Object> delegatesByInterfaceType) {
    return proxyClass.createProxy(new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object objectToCall = delegatesByInterfaceType.get(method.getDeclaringClass());
            if (objectToCall == null) {
                objectToCall = original;
            }

            return method.invoke(objectToCall, args);
        }
    });

}

Aquí vemos como se está creando un Proxy dinámico cuyo InvocationHandler lo que hace es:

  • línea 5 – buscar en un mapa el delegado que corresponde con el tipo donde está definido el método que se está ejecutando.
  • línea 7 – si no encuentra un delegaod supone que el método pertenece al objeto original.
  • línea 10 – ejecuta el método (bien en el delegado o en el objeto original).

 5. Java Mixin con Java 8

Java 8 introduce una nueva construcción en el lenguaje que permite especificar una implementación por defecto para métodos de una interfaz.

interface MessagePrinter {
    default printMessage() {
        System.out.println("Este es el mensaje por defecto");
    }
}

Según Oracle el principal objetivo de estas implementaciones por defecto es la de mantener la compatibilidad hacia atrás al añadir nuevos métodos a una interfaz ampliamente usada (como hace el propio Java con todo el API de colecciones para soportar Lambdas y Streams). De manera que al añadir estos nuevos métodos no se “rompa” todo el código que ya está escrito (antes de Java 8 si añadimos un método a una interfaz existente, todas las clases que usan esa interfaz dejarán de compilar).

Nosotros vamos a aprovechar esta capacidad para hacer una implementación de mixin. Esta implementación tendrá el mismo problema de alto acoplamiento que presentaba en el primer punto de este tutorial, cuando hacíamos los mixins a mano; pero por lo menos no tendremos el problema de la duplicación de código.

De hecho realmente lo que estamos implementando, tal como cuenta el artículo Java 8: Now You Have Mixins?, es el Virutal Field Pattern.

El ejemplo que vamos a usar es:

Java 8 Mixin
Java 8 Mixin

El código sería:

class SimplePerson {
    ...
}

interface Entity {
    void save();
}

interface EntityFlavour extends Entity {
    Entity getEntity();

    @Override
    default void save() { getEntity().save(); }
}

class EntityDelegate implements Entity {
    private final Object original;
    EntityDelegate(Object original) { this.original = original; }

    @Override
    public void save() {
        ...
    }
}

class PoweredPerson extends SimplePerson implements EntityFlavour {
    private final EntityDelegate mixin = new MixinDelegate(this);

    @Override
    public Entity getEntity() {
        return mixin;
    }
}

class Main {
    public static final main(String[] args) {
        PoweredPerson mixture = new PoweredPerson();
        mixture.getFoo();
        mixture.save();
    }
}

Aquí el quid de la cuestión está en la interfaz EntityFlavour (líneas 9 a 14) Esta interfaz añade el método getEntity(), que obliga a quien lo implemente a proporcionar un Entity, y luego sobreescribe todos los métodos de la interfaz Entity proporcionando una implementación por defecto en función de ese método. Es decir proporciona una implementación por defecto que delega la ejecución del método sobre la instancia devuelta por getEntity().

Gracias a la interfaz EntityFlavour y sus implementaciones por defecto, ya no tenemos que repetir código; basta con que las clases donde queremos aplicar el mixin implementen esta interfaz (líneas 26 a 32).

6. Conclusiones

Este tutorial no pretende ser más que un ejercicio teórico, y no se si la librería java-mixins llegará muy lejos o tendrá utilidad real. Pero sí me parece interesante el estudio de como, mediante el uso de patrones, podemos implementar característica que nuestro lenguaje no soporta de forma nativa.

Os animo a que reviséis el código del proyecto aunque sólo sea como ejercicio para repasar como funciona un Proxy dinámico en Java.

Nota: Todas los diagramas UML han sido generados con yUML.

7. Sobre el autor

Alejandro Pérez García, Ingeniero en Informática (especialidad de Ingeniería del Software) y Certified ScrumMaster

Socio fundador de Autentia (Desarrollo de software, Consultoría, Formación)

mailto:alejandropg@autentia.com

Autentia Real Business Solutions S.L. – “Soporte a Desarrollo”

http://www.autentia.com

Alejandro es socio fundador de Autentia y nuestro experto en Java EE, Linux y optimización de aplicaciones empresariales. Ingeniero en Informática y Certified ScrumMaster. Seguir @alejandropgarci Si te gusta lo que ves, puedes contratarle para darte ayuda con soporte experto, impartir cursos presenciales en tu empresa o para que realicemos tus proyectos como factoría (Madrid). Puedes encontrarme en Autentia: Ofrecemos servicios de soporte a desarrollo, factoría y formación.

1 COMENTARIO

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad