7 de enero de 2022

Patrón de Diseño Strategy



Strategy es un patrón de diseño de comportamiento con el ámbito de objeto. Su finalidad es la de poder cambiar el algoritmo o comportamiento de un objeto en tiempo de ejecución. Esto ayuda cuando tenemos una aplicación en la que varían solo ciertas partes de la lógica. Con este patrón podemos aislar estos comportamiento o algoritmos en clases diferentes y elegir entre uno u otro en tiempo de ejecución.

En este tutorial te explicó más a fondo cómo funciona este patrón y te muestro una estrategia de implementación del mismo.

Con este patrón creamos un objeto que se usará como contexto y distintas estrategias de las cuales se podrá seleccionar una u otra dependiendo de ciertas condiciones. Uniremos las estrategias al contexto haciendo uso de la composición.

Imagina que trabajamos en una empresa que se dedica a la exportación de productos. Antes de ser enviados, todos los productos deben seguir la misma serie de pasos:
  1. Tomarlos de los estantes.
  2. Empaquetarlos.
  3. Colocar una etiqueta con el nombre del destinatario.
  4. Llevarlos a la empresa de paquetería para ser enviados.

Todos los productos siguen los mismos 4 pasos; sin embargo, el paso 2 (empaquetarlos) varía dependiendo del tipo de producto y el destino a donde estemos enviando. Por ejemplo, si enviamos artículos frágiles debemos envolverlos en plástico de burbujas; si enviamos revistas o periódicos no es necesario envolverlos; si enviamos artículos que pesen más de 20 kilos debemos usar una caja de madera y cerrarla con clavos en lugar de una caja de cartón cerrada con cinta, etc.

Los pasos anteriores son el contexto de la aplicación de envío de artículos. Las variaciones del paso 2 descritas en el párrafo anterior son los diferentes algoritmos de empaquetamiento que queremos separar en diferentes clases; así, cuando hagamos un envío podemos revisar los artículos que enviaremos y seleccionar el algoritmo de empaquetado más adecuado. Cuando llegue un paquete con artículos diferentes podremos realizar nuevamente la selección del algoritmo y cambiarlo y usar uno diferente.

Cada uno de esos diferentes algoritmos se coloca en una instancia distinta de Strategy; así que es importante mencionar que estos algoritmos deben poder ser intercambiables.

Si estás programando un juego, puedes tener diferentes implementaciones de Strategy dependiendo del nivel de los enemigos: una estrategia para los enemigos agresivos, otra para los normales y otra para los defensivos; por ejemplo.


Objetivo o intención del patrón

Con la explicación anterior podemos ver que el objetivo de Strategy es:
  1. Definir una familia de algoritmos, encapsular cada uno y volverlos intercambiables.
  2. Permitir que el algoritmo cambie (o se seleccione) dependiendo del cliente que lo utilice.


Implementación

Para implementar Strategy debemos realizar los siguientes pasos:
  1. Definir una interface, o clase abstracta, Strategy. Esta interface puede tener varios métodos que serán los que se invoquen para ejecutar el algoritmo.
  2. Crear un conjunto de clases concretas que implementen la interface Strategy. Estas son las que contendrán las diferentes operaciones lógicas que aplicaremos. En el ejemplo de envíos, las clases concretas serían las diferentes formas que tenemos de empaquetar los artículos.
  3. Crear una clase "contexto". Contexto es el nombre formal que se le da a la clase que hace uso de Strategy. En el ejemplo anterior, el contexto sería el módulo o parte de la aplicación que ejecuta los 4 pasos para realizar el envío.
  4. Inyectar o crear la instancia de Strategy adecuada para la operación que queremos realizar. La forma en la que se obtiene esta instancia queda fuera del alcance del patrón, pero podemos usar Factory Method para decidir cuál instancia debemos usar.

El paso 4 es donde vemos la utilidad de este patrón. Como mencioné al inicio del tutorial: el objeto Contexto tiene una relación de composición con el objeto Strategy que usará. Esto quiere decir que tiene una variable del tipo de la interface Strategy y esto es lo que nos permite usar una instancia u otra. Esto quedará mucho más claro cuando veamos el ejemplo.

Diagrama



Como puedes ver en el diagrama anterior este patrón tiene solamente 3 componentes:
  • Strategy. Define la interface común a todos los algoritmos soportados.
  • Estrategia concreta. Las distintas clases que implementan la interface Strategy.
  • Contexto. Objeto que contiene la referencia a Strategy. El contexto es invocado por los clientes y delega la funcionalidad correspondiente al objeto Strategy.

📌 Algoritmo es el nombre que se le da a este patrón a esos bloques de código “intercambiable” que se aplicarán dependiendo de la instancia de Strategy que se vaya a utilizar.

¿Dónde se usa este patrón? Strategy se usa mucho en desarrollo web. En particular en javax.servlet.http.HttpServlet, el método service() y los métodos doXXX() reciben un HttpServletRequest y HttpServletResponse y la clase que extiende de HttpServlet debe hacer uso de ellos para procesar la respuesta y enviar la respuesta de vuelta al cliente.

También se usa mucho en comparaciones y ordenamientos usando java.util.Comparator#compare(), de hecho este será le mismo uso que nosotros le daremos.


Ejemplo

Para los ejemplos uso Java 17 y Gradle. Hago uso de lambdas en cierta parte del código, por lo que debe funcionar en cualquier versión superior a la 8. También, uso Lombok para evitar escribir código repetitivo de forma innecesaria.

Para validar el correcto funcionamiento de la aplicación usaré JUnit y AssertJ. Agrega las siguientes dependencias en el archivo build.gradle (si usas Maven, coloca las dependencias correspondientes en el archivo pom.xml).
dependencies {
    compileOnly 'org.projectlombok:lombok:1.18.22'
    annotationProcessor 'org.projectlombok:lombok:1.18.22'
 
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
    testImplementation 'org.assertj:assertj-core:3.21.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}


Ahora sí, al código.

El problema que resolveremos es el siguiente: Crearemos una aplicación que se encargue de ordenar una serie de cadenas. Dependiendo de la cantidad de cadenas que tengamos usaremos un algoritmo de ordenamiento diferente, usando la siguiente lógica:
  1. Si tenemos menos de 5 cadenas usaremos el algoritmo BubbleSort.
  2. Si tenemos entre 5 y 10 cadenas usaremos el algoritmo Quicksort.
  3. Si tenemos más de 10 cadenas usaremos el algoritmo ContingSort.

¿Por qué esas implementaciones? En realidad creo que el por qué no es importante para este tutorial, ya que la intención es mostrar como usar el patrón de diseño; pero, me estoy basando en la complejidad (Big-O) de cada uno de los algoritmos tomando la información de este sitio: https://www.bigocheatsheet.com 📌 Las implementaciones de los algoritmos de ordenamiento los tomo del siguiente repositorio en GitHub (muy recomendable): TheAlgorithms/Java
Incluyo el código fuente de los algoritmos en mi proyecto de GitHub, pero dejo el código original con los créditos a los autores originales, y las ligas al código fuente original a lo largo del tutorial.

Las clases anteriores, convenientemente, ya hacen uso del patrón Strategy. Todas implementan una interface común, SortAlgorithm, esta interface representa a la interface Strategy del diagrama del patrón.

SortAlgorithm tiene dos métodos, ambos regresan un objeto de tipo Comparable, el cual usamos para ordenar la lista usando las funcionalidades nativas de Java.

El primer método recibe un arreglo de objetos y el segundo una lista. Yo respeto esta misma interface, la cual se ve de la siguiente forma (sin los comentarios del código fuente original):
/** 
* @author Podshivalov Nikita (https://github.com/nikitap492)
*/
public interface SortAlgorithm {

    <T extends Comparable<T>> T[] sort(T[] unsorted);

    default <T extends Comparable<T>> List<T> sort(List<T> unsorted) {
        return Arrays.asList(sort(unsorted.toArray((T[]) new Comparable[unsorted.size()])));
    }
}

Como puedes ver, el segundo método es un método default que hace uso del primero, así que en realidad en las clases que implementen la interface solo estamos obligados a implementar un método, el que recibe un arreglo.

Cada una de las implementaciones que usaremos son las estrategias concretas. Como el mostrar el código de las mismas no aporta nada al tutorial, y ocupan algo de espacio, lo omitiré, pero dejo nuevamente las ligas al código fuente original por si quieres revisarlo:
  • BubbleSort, por Varun Upadhyay (https://github.com/varunu28)
  • QuickSort, por Varun Upadhyay (https://github.com/varunu28)
  • CountingSort, por Youssef Ali (https://github.com/youssefAli11997)
Lo siguiente es crear la clase Contexto. A esta clase la llamaré OrdenadorCadenas. Esta clase tendrá una referencia a SortAlgorithm, de la siguiente forma:
public class OrdenadorCadenas {
    private SortAlgorithm sortAlgorithm;
}

Además, tendrá solo un método, ordena, que recibirá un arreglo de cadenas desordenado y regresará un nuevo arreglo ordenado. La tarea de ordenamiento será delegada a la instancia de SortAlgorithm, de la siguiente forma:
public String[] ordena(String[] arregloDesordenado) {

    String[] arregloOrdenado = sortAlgorithm.sort(arregloDesordenado);

    return arregloOrdenado;
}

El último paso consiste en obtener la instancia apropiada de SortAlgorithm de acuerdo con las reglas que establecimos anteriormente. Para esto haré uso de una SimpleFactory. No explico los detalles de la misma ya que tengo un tutorial completo donde hablo de este patrón de diseño. ¿Por qué una SimpleFactory y no una FactoryMethod? Bueno, explico las diferencias en este tutorial: Diferencias: Simple Factory vs. Factory Method vs. Abstrac Factory .

public class SortAlgorithmSimpleFactory {

    private static final int ELEMENTOS_BUBBLE_SORT = 5;
    private static final int ELEMENTOS_QUICK_SORT = 10;

    public static SortAlgorithm getSortingAlgorithm(int numeroDeElementos) {

        SortAlgorithm sortAlgorithm;

        if (numeroDeElementos < ELEMENTOS_BUBBLE_SORT) {
            sortAlgorithm = new BubbleSort();
        } else if (numeroDeElementos > ELEMENTOS_BUBBLE_SORT && numeroDeElementos <= ELEMENTOS_QUICK_SORT) {
            sortAlgorithm = new QuickSort();
        } else {
            sortAlgorithm = new CountingSort();
        }

        return sortAlgorithm;
    }
}

📌 Esta es solo una forma de obtener la instancia de SortAlgorithm, pero esta instancia puede obtenerse de muchas otras formas. Usa la que sea más conveniente para el problema que estás tratando de resolver.

La clase OrdenadorCadenas queda de la siguiente forma:
public class OrdenadorCadenas {
    private SortAlgorithm sortAlgorithm;

    public String[] ordena(String[] arregloDesordenado) {

        sortAlgorithm = SortAlgorithmSimpleFactory.getSortingAlgorithm(arregloDesordenado.length);

        String[] arregloOrdenado = sortAlgorithm.sort(arregloDesordenado);

        return arregloOrdenado;
    }
}

Ahora solo queda comprobar que todo funciona correctamente. Para eso haré uso de algunas pruebas unitarias. Usaré tres formas diferentes para validar que los arreglos están correctamente ordenados.

En la primera prueba creamos un arreglo de 3 cadenas, las ordenamos usando una instancia de OrdenadorCadena y luego validamos que el arreglo regresado contenga los elementos en las posiciones esperadas:
@Test
public void testCadenasOrdenadas_cuandoArregloConTresCadenas() {
    String[] arregloDesordenado = {"Hola", "Estoy", "Desordenado"};

    String[] arregloOrdenado = ordenadorCadenas.ordena(arregloDesordenado);

    assertThat(arregloOrdenado).contains("Desordenado", atIndex(0));
    assertThat(arregloOrdenado).contains("Estoy", atIndex(1));
    assertThat(arregloOrdenado).contains("Hola", atIndex(2));
}

Para la segunda prueba usamos 6 cadenas y también validamos que el resultado obtenido sean las mismas cadenas en el orden apropiado:
@Test
public void testCadenasOrdenadas_cuandoArregloConSeisCadenas() {
    String[] arregloDesordenado = {"Ahora", "Tenemos", "Mas", "Elementos", "Para", "Ordenar"};

    String[] arregloOrdenado = ordenadorCadenas.ordena(arregloDesordenado);

    assertThat(arregloOrdenado).containsExactly("Ahora", "Elementos", "Mas", "Ordenar", "Para", "Tenemos");
}

Finalmente, realizaremos la prueba con 15 cadenas:
@Test
public void testCadenasOrdenadas_cuandoArregloCon15Cadenas() {
    String[] arregloDesordenado = {"Ahora", "Si", "Tenemos", "Muchas", "Cadenas", "Para", "Validar", "El", "Algoritmo", "De", "Ordenamiento", "Seleccionado", "En", "Nuestra", "Aplicacion"};

    String[] arregloOrdenado = ordenadorCadenas.ordena(arregloDesordenado);

    String[] arregloEsperado = {"Ahora", "Algoritmo", "Aplicacion", "Cadenas", "De", "El", "En", "Muchas", "Nuestra", "Ordenamiento", "Para", "Seleccionado", "Si",  "Tenemos", "Validar"};

    assertThat(arregloOrdenado).isEqualTo(arregloEsperado);
}

Y listo, con esto podemos comprobar que la aplicación funciona correctamente.

El diagrama de nuestra implementación es el siguiente:




Ventajas y Desventajas

  • ➕Es un patrón muy simple de utilizar, eficiente y flexible al hacer uso de la composición por sobre la herencia.
  • ➕Funciona muy bien cuando tenemos muchos comportamientos distintos y de los cuales tenemos diferentes variantes del mismo algoritmo.
  • ➕Ayuda a evitar el exponer estos algoritmos complejos al cliente que hará uso de ellos.
  • ➕Ayuda a eliminar el uso de las sentencias condicionales para poder aplicar el algoritmo deseado.
  • ➖El cliente (o el componente encargado de crear las distintas instancias de Strategy) debe estar al tanto de todas estas instancias (o hacer uso de un patrón más adecuado para la creación de las mismas). Esto significa que debe entender todas las estrategias para saber cuál es la más apropiada para ser usada.


Otros Nombres

  • Policy Pattern


Conclusión

Como viste en este tutorial, Strategy es un patrón muy útil, bastante utilizado y con una implementación realmente simple; pienso que de hecho es el más simple de todos los patrones de diseño. También ampliamente utilizado en librerías y frameworks por su conveniencia al momento de estructurar una aplicación.

Además se mezcla muy bien con otros patrones de diseño como FactoryMethod y TemplateMethod.

Como nota adicional, en el repositorio dejo, además del código, el archivo original en draw.io con los esquemas de los patrones.

Espero que este tutorial les sea de utilidad. Si tienen alguna duda, sugerencia, comentario o aclaración, pueden dejarla en la sección de comentarios o enviar un correo a programadorjavablog@gmail.com (pueden agregarme al chat de gmail). También puedes seguir JavaTutoriales en las siguientes redes sociales: Saludos y gracias.

Descarga los archivos de este tutorial desde mi repositorio en GitHub:
Entradas relacionadas: