Patrón de diseño de estrategia en Java 8

1. Introducción

En este artículo, veremos cómo podemos implementar el patrón de diseño de estrategia en Java 8.

Primero, daremos una descripción general del patrón y explicaremos cómo se ha implementado tradicionalmente en versiones anteriores de Java.

A continuación, probaremos el patrón nuevamente, solo que esta vez con lambdas de Java 8, reduciendo la verbosidad de nuestro código.

2. Patrón de estrategia

Esencialmente, el patrón de estrategia nos permite cambiar el comportamiento de un algoritmo en tiempo de ejecución.

Por lo general, comenzaríamos con una interfaz que se usa para aplicar un algoritmo y luego lo implementamos varias veces para cada algoritmo posible.

Digamos que tenemos el requisito de aplicar diferentes tipos de descuentos a una compra, según sea Navidad, Semana Santa o Año Nuevo. Primero, creemos una interfaz de Descontador que será implementada por cada una de nuestras estrategias:

public interface Discounter { BigDecimal applyDiscount(BigDecimal amount); } 

Entonces digamos que queremos aplicar un 50% de descuento en Semana Santa y un 10% de descuento en Navidad. Implementemos nuestra interfaz para cada una de estas estrategias:

public static class EasterDiscounter implements Discounter { @Override public BigDecimal applyDiscount(final BigDecimal amount) { return amount.multiply(BigDecimal.valueOf(0.5)); } } public static class ChristmasDiscounter implements Discounter { @Override public BigDecimal applyDiscount(final BigDecimal amount) { return amount.multiply(BigDecimal.valueOf(0.9)); } } 

Finalmente, probemos una estrategia en una prueba:

Discounter easterDiscounter = new EasterDiscounter(); BigDecimal discountedValue = easterDiscounter .applyDiscount(BigDecimal.valueOf(100)); assertThat(discountedValue) .isEqualByComparingTo(BigDecimal.valueOf(50));

Esto funciona bastante bien, pero el problema es que puede ser un poco molesto tener que crear una clase concreta para cada estrategia. La alternativa sería utilizar tipos internos anónimos, pero eso sigue siendo bastante detallado y no mucho más práctico que la solución anterior:

Discounter easterDiscounter = new Discounter() { @Override public BigDecimal applyDiscount(final BigDecimal amount) { return amount.multiply(BigDecimal.valueOf(0.5)); } }; 

3. Aprovechamiento de Java 8

Desde que se lanzó Java 8, la introducción de lambdas ha hecho que los tipos internos anónimos sean más o menos redundantes. Eso significa que crear estrategias en línea es ahora mucho más limpio y fácil.

Además, el estilo declarativo de la programación funcional nos permite implementar patrones que antes no eran posibles.

3.1. Reducir la verbosidad del código

Intentemos crear un EasterDiscounter en línea , solo que esta vez usando una expresión lambda:

Discounter easterDiscounter = amount -> amount.multiply(BigDecimal.valueOf(0.5)); 

Como podemos ver, nuestro código ahora es mucho más limpio y más fácil de mantener, logrando lo mismo que antes pero en una sola línea. Esencialmente, una lambda puede verse como un reemplazo de un tipo interno anónimo .

Esta ventaja se hace más evidente cuando queremos declarar aún más Descuentos en línea:

List discounters = newArrayList( amount -> amount.multiply(BigDecimal.valueOf(0.9)), amount -> amount.multiply(BigDecimal.valueOf(0.8)), amount -> amount.multiply(BigDecimal.valueOf(0.5)) );

Cuando queremos definir muchos Descuentos, podemos declararlos estáticamente todos en un solo lugar. Java 8 incluso nos permite definir métodos estáticos en interfaces si queremos.

Entonces, en lugar de elegir entre clases concretas o tipos internos anónimos, intentemos crear lambdas en una sola clase:

public interface Discounter { BigDecimal applyDiscount(BigDecimal amount); static Discounter christmasDiscounter() { return amount -> amount.multiply(BigDecimal.valueOf(0.9)); } static Discounter newYearDiscounter() { return amount -> amount.multiply(BigDecimal.valueOf(0.8)); } static Discounter easterDiscounter() { return amount -> amount.multiply(BigDecimal.valueOf(0.5)); } } 

Como podemos ver, estamos logrando mucho en un código que no es mucho.

3.2. Composición de la función de aprovechamiento

Modifiquemos nuestra interfaz de Descontador para que amplíe la interfaz UnaryOperator y luego agreguemos un método combine () :

public interface Discounter extends UnaryOperator { default Discounter combine(Discounter after) { return value -> after.apply(this.apply(value)); } }

Esencialmente, estamos refactorizando nuestro Discounter y aprovechando el hecho de que aplicar un descuento es una función que convierte una instancia de BigDecimal en otra instancia de BigDecimal , lo que nos permite acceder a métodos predefinidos . Como UnaryOperator viene con un método apply () , podemos simplemente reemplazar applyDiscount con él.

El método combine () es solo una abstracción sobre la aplicación de un Descontador a los resultados de esto. Utiliza la función apply () incorporada para lograr esto.

Ahora, intentemos aplicar varios Descuentos de forma acumulativa a una cantidad. Haremos esto usando el funcional reduce () y nuestro combine ():

Discounter combinedDiscounter = discounters .stream() .reduce(v -> v, Discounter::combine); combinedDiscounter.apply(...);

Preste especial atención al primer argumento de reducción . Cuando no se proporcionan descuentos, debemos devolver el valor sin cambios. Esto se puede lograr proporcionando una función de identidad como el descontador predeterminado.

Esta es una alternativa útil y menos detallada para realizar una iteración estándar. Si consideramos los métodos que estamos obteniendo para la composición funcional, también nos brinda mucha más funcionalidad de forma gratuita.

4. Conclusión

En este artículo, explicamos el patrón de estrategia y también demostramos cómo podemos usar expresiones lambda para implementarlo de una manera menos detallada.

La implementación de estos ejemplos se puede encontrar en GitHub. Este es un proyecto basado en Maven, por lo que debería ser fácil de ejecutar como está.