Design Patterns - Strategy

TLDR: El Strategy es un patrón que te permite definir familias de algoritmos, poniéndolas en clases separadas, y haciendo que sus objetos sean intercambiables


El patrón de diseño Strategy te sugiere que tomes una clase que hace algo en especifico en varias formas y extraigas todas las variaciones de los algoritmos en clases separadas, llamadas strategies, para poder intercambiarlas en tiempo de ejecución.

¿Porque necesitamos el Strategy?

Imagina que estás trabajando en un software para el procesamiento de texto. La aplicación está centrada en poderle dar a los usuarios la posibilidad de exportar la información procesada y almacenada en múltiples formatos.

Inicialmente, una de las características de esta aplicación es exportar la información en el formato Markdown, así que por defecto y la primera versión de la aplicación exportaba la información en este formato. Lucia algo así como lo siguiente:

// Markdown
//  * Hola
//  * Mundo

Después de un tiempo, se dieron cuenta que ese formato se les quedó insuficiente y requirieron al programador agregar un nuevo formato, este era HTML y la aplicación se popularizo tanto que la gente estaba solicitando muchas características a la aplicación.

Desde un punto de vista general, la aplicación estaba siendo muy utilizada y se estaba convirtiendo en un éxito, pero siempre que se quería incluir una nueva funcionalidad a la aplicación era un dolor de cabeza. Siempre que se le quería incluir un nuevo formato al procesador de texto, corregir un bug o refactorizar esa clase era una tarea compleja y se estaba volviendo inmanejable.

Aquí es donde entra el patrón Strategy, en el que tomas una clase que hace algo especifico en muchas maneras diferentes y extraes algo de esos algoritmos en separadas clases llamadas strategies.

La idea es encontrar el esqueleto del algoritmo y separarlos en componentes de alto nivel, y en la clase original, guardar la referencia a las estrategias.

Hay dos patrones de comportamiento para solucionar esta necesidad, este y el Template Method. La diferencia con ese es que el Template usa la herencia como forma de solucionar el problema y este usa la composición.

En definición, ¿que es el patrón Strategy?

Un Strategy es un patrón que define una familia de algoritmos, que encapsula cada uno y los hace intercambiables. Este patrón le permite al algoritmo variar independientemente del cliente que lo esté usando.

El problema en un ejemplo

Imaginemos que estemos trabajando en un software que quiere construir un procesador de texto, y se requiere exportar una lista en varios formatos. Ya sabemos que tenemos esos requerimientos, pero a medida que el software vaya teniendo éxito, nos irán solicitando requerimientos adicionales.

Por el momento vamos a soportar dos formatos, el primero es HTML y el segundo es Markdown. El resultado debería ser algo así como lo siguiente:

// HTML
// <ul>
//    <li>Hola</li>
//    <li>Mundo</li>
// </ul>

// Markdown
//  * Hola
//  * Mundo

El implementar estos dos formatos y con la proyección que se tiene de formatos adicionales causará que el código crezca con el paso del tiempo, y cualquier modificación o extensión podrá volverse un dolor de cabeza. Por tal razón vamos a implementar una estrategia para poder hacer que el código de TextProcessor sea dinámico, que al día de mañana podamos agregar algún otro formato y no tengamos pensar dos veces que podríamos dañar algo existente.

Para poder realizar algo así, tenemos que analizar el algoritmo que sea más complejo y proponga diferentes cambios. Que tenga varios condicionales y definir una interfaz común para todas las variantes del algoritmo.

Por ejemplo, para generar el texto de HTML, debemos de tener unos tags de inicio <ul> y de cierre </ul>. También necesitamos algo que retorne la lista con sus respectivos tags <li>...</li>. Para lo siguiente vamos a crear nuestra clase donde iniciaremos la implementación:

let OutputFormat = Object.freeze({
  markdown: 0,
  html: 1,
});

class TextProcessor {
  constructor(ouputFormat) {
    this.buffer = [];
    this.setOutputFormat(outputFormat);
  }

  setOutputFormat(format) {
    switch (format) {
      case OutputFormat.markdown:
        this.listStrategy = new MarkdownListStrategy();
        break;
      case OutputFormat.html:
        this.listStrategy = new HtmlListStrategy();
        break;
    }
  }
}

Aquí podemos ver varias cosas, creamos un enumerador llamado OutputFormat que es un objeto Object.freeze en el cual definimos los formatos que vamos a admitir. Después, definimos el atributo buffer que contendrá la lista de string que vamos a transformar y pasaremos en el constructor el formato que vamos a utilizar. Adicionalmente, creamos la función setOutputFormat(format) que nos permitirá cambiar de un formato a otro. En esta función se podrá seleccionar la Strategy requerida y se asignará a la variable listStrategy.

Las Strategies tendrán una clase base, que debería ser abstract y las strategies que crearemos MarkdownListStrategy y HtmlListStrategy deberán adoptar. En este ejemplo, nuestra clase base tendrá 3 métodos, start(), addListItem() y end(). Esto debido a que para el formato HTML se requiere un tag de inicio y fin. Cabe resaltar que el número de métodos que contendrá la clase base varia de acuerdo con la abstracción de los algoritmos que incluiremos en nuestras strategies.

class ListStrategy {
  start(buffer) {}
  end(buffer) {}
  addListItem(buffer, item) {}
}

Aclaro que los métodos están vacíos, porque cuando se heredan, no es necesario proveer una definición propia del método. Por ejemplo: para Markdown, no es necesario implementar los métodos start y end, pero para HTML si, y así sucesivamente. Ahora construyamos las Strategy requeridas con los algoritmos para cada formato.

class MarkdownListStrategy extends ListStrategy {
  addListItem(buffer, item) {
    buffer.push(`  * ${item}`);
  }
}

class HtmlListStrategy extends ListStrategy {
  start(buffer) {
    buffer.push(`<ul>`);
  }

  end(buffer) {
    buffer.push(`</ul>`);
  }

  addListItem(buffer, item) {
    buffer.push(`  <li>${item}</li>`);
  }
}

Ahora que tenemos las Strategies listas, vamos a usarlas. Para esto vamos a crear un nuevo método en la clase TextProcessor llamado appendList que obtendrá los items y aplicaremos la strategy actual.

class TextProcessor {
  constructor(ouputFormat) {
    this.buffer = [];

    // ...
  }

  setOutputFormat(format) {
    //...
  }

  appendList(items) {
    this.listStrategy.start(this.buffer);

    for (let item of items) this.listStrategy.addListItem(this.buffer, item);

    this.listStrategy.end(this.buffer);
  }

  // Métodos de útilidad.
  clear() {
    this.buffer = [];
  }

  toString() {
    return this.buffer.join("\n");
  }
}

Adicionalmente, hemos agregado dos métodos, uno para limpiar la lista de items llamada clear y toString sobrescrita para imprimir el resultado en string. Ahora usemos estas clases en la siguiente implementación:

let tp = new TextProcessor(OutputFormat.markdown);
tp.appendList([`factory`, `bridge`, `façade`]);
console.log(tp.toString());
// * factory
// * bridge
// * façade

tp.clear();
// Cambiamos a otro formato
tp.setOutputFormat(OutputFormat.html);
tp.appendList(["composite", "decorator", "state"]);
console.log(tp.toString());
// <ul>
//   <li>composite</li>
//   <li>decorator</li>
//   <li>state</li>
// </ul>

Como se puede ver en el anterior ejemplo, la primera salida de consola está con el formato markdown. Después, limpiamos la lista y cambiamos de formato a HTML y podremos ver la segunda salida de consola en formato HTML.

Entonces la idea principal con strategy es poder definir un algoritmo general, como lo es el método appendList, que es el algoritmo esqueleto que necesitamos utilizar. Y las implementaciones de cada formato son definidas en cada una de las strategies MarkdownListStrategy y HtmlListStrategy previamente creadas.

Este algoritmo no sabe que strategy será utilizada y eso es lo importante de definir una estructura de alto nivel, como hicimos en la interfaz ListStrategy y solamente sobrescribir los métodos que necesitamos en las strategies en especifico.

Código completo del ejemplo: https://bit.ly/3dVIck7

Estructura del patrón Strategy

En este patrón encontramos varios componentes que interactuaran entre si.

  1. El Context, que en el ejemplo es la clase TextProcessor es el encargado de almacenar la referencia a las strategies y comunicarse con este objeto por medio de la interfaz ListStrategy.
  2. La Strategy, que en el ejemplo es la interfaz ListStrategy, es común ante todas las estrategias. Y declara los métodos que usará el Context para ejecutar la estrategia.
  3. Los Concrete Strategies, que en el ejemplo son las clases MarkdownListStrategy y HtmlListStrategy, y son clases cuyo padre es la interfaz ListStrategy. Estas son las que provee la implementación para los diferentes algoritmos que el Context usa.
  4. El Context llama al método requerido de la strategy que ha sido asociada y siempre que se necesite, se podrá correr el algoritmo. El context no sabe nada de los detalles de la strategy que está ejecutando ni como funciona el algoritmo que está ejecutando.
  5. El Cliente crea una instancia del contexto seleccionado por medio de el enumerador el formato que requiere para su ejecución, internamente se seleccionará el strategy a utilizar. Si se requiere cambiar el strategy, el context expone el método setOutputFormat que permite realizar ese cambio en tiempo de ejecución.

Conclusiones

  • El patrón de diseño Strategy te ayuda a organizar tu código cuando existen diferentes variaciones de un algoritmo con un objeto y tengas la posibilidad de cambiar de un algoritmo a otro en tiempo de ejecución.
  • Este patrón de diseño te ayuda a definir un algoritmo de alto nivel, bastante general, para poder definir una interfaz de la que esperas que cada strategy siga. Esto permite aislar la lógica de negocios de la clase a los detalles de la implementación de el algoritmo, siento este código no tan relevante de tener en el contexto de la lógica.
  • Este patrón es muy similar al template method, a diferencia que el acercamiento de ese patrón es por medio de la herencia.

Ejercicio

Se requiere que se construya un software para administrar diferentes sistemas de transportes en una misma plataforma.

  • Bicicleta
  • Carro
  • Bus

Se requiere crear una solución aplicando el patrón Strategy, que le permitan a un usuario transportarse desde una misma aplicación y obtener el cobro final.

Estructura Inicial

Solución esperada

Recursos adicionales y bibliografia

2018-1.7 Alexander Shvets - Dive into design Patterns

Erich Gamma, Richard Helm, Ralph Johnson, John M. Vlissides - Design Patterns Elements of Reusable Object-Oriented Software (1994)

The State Pattern - Addy Osmani - Learning JavaScript Design Patterns (2012)

https://www.dofactory.com/javascript/facade-design-pattern

https://loredanacirstea.github.io/es6-design-patterns/#observer