Codificando el Comportamiento Dinámico con el Patrón de la Estrategia

Cómo aprovechar el polimorfismo en tiempo de ejecución

Foto de Linus Nylund en Unsplash

Uno de los beneficios del diseño orientado a objetos es la capacidad de los objetos de compartir algunos comportamientos mientras que simultáneamente difieren en otros. Típicamente, esto se logra a través de la herencia – cuando muchas subclases heredan propiedades de una clase de padres, pero pueden opcionalmente anular ciertos comportamientos según sea necesario. Este es un patrón de diseño muy útil y común; sin embargo, hay situaciones en las que el polimorfismo por herencia es inapropiado. Considere, por ejemplo, cuando sólo necesita un único comportamiento para cambiar, pero desea que un objeto permanezca igual. O, cuando se desea que un objeto cambie su comportamiento en tiempo de ejecución basado en algún factor externo (pero impredecible). En tales casos, es probable que un esquema de herencia cause un código innecesariamente inflado que es difícil de mantener (particularmente a medida que aumenta el número de subtipos). Sin embargo, hay un mejor enfoque: el patrón de estrategia .

Polimorfismo a través de estrategias

El patrón de estrategia , también conocido como patrón de política , es un patrón de diseño de comportamiento que permite a un objeto ejecutar algún algoritmo (estrategia) basado en el contexto externo proporcionado en tiempo de ejecución. Este patrón es particularmente útil cuando se tiene un objeto que necesita ser capaz de ejecutar un solo comportamiento de diferentes maneras en diferentes momentos. Utilizando el patrón de estrategia, se puede definir un conjunto de algoritmos que se pueden proporcionar dinámicamente a un objeto determinado si/cuando se necesitan. Este patrón tiene una serie de beneficios, incluyendo: encapsulación de algoritmos particulares en sus propias clases; aislamiento del conocimiento sobre cómo se implementan los algoritmos; y, código que es más flexible, móvil y mantenible. Hasta el último punto, puedes notar que estos son los mismos atributos que resultan del código que sigue el Principio Abierto/Cerrado (Open/Closed Principle, OCP) y de hecho, el patrón de estrategia es una excelente manera de escribir código que sea compatible con OCP.

Al implementar el patrón de estrategia, se necesitan tres elementos principales:

  1. Un cliente , que sabe de la existencia de alguna estrategia abstracta pero no sabe qué hace esa estrategia o cómo lo hace.
  2. Un conjunto de estrategias que el cliente puede utilizar si/cuando se le proporciona una de ellas. Estos pueden venir en forma de funciones de primera clase, objetos, clases u otra estructura de datos.
  3. Contexto opcional que el cliente puede proporcionar a su estrategia actual para utilizar en la ejecución.

La forma clásica de implementar estrategias es con interfaces. En este caso, un cliente tiene un puntero interno a alguna interfaz de estrategia abstracta, que luego se apunta a una implementación de estrategia concreta a través de la inyección de dependencias (es decir, durante la construcción o con un ajustador en tiempo de ejecución). A partir de entonces, el cliente puede utilizar la estrategia proporcionada para llevar a cabo algún trabajo, todo ello sin saber (o preocuparse) de lo que la estrategia realmente hace.

Aunque las interfaces son la forma clásica de implementar el patrón de estrategia, se puede lograr un efecto similar en lenguajes que no tienen interfaces. Lo importante es que el cliente sea consciente de alguna estrategia abstracta y sea capaz de ejecutar esa estrategia sin conocer su funcionamiento interno.

Polimorfismo por herencia pura

Antes de analizar cómo usar el patrón de estrategia, veamos algunos ejemplos que utilizan otros enfoques del polimorfismo. Considere el siguiente fragmento, que utiliza la herencia pura para definir los diferentes tipos de corredores en una carrera.

Aquí tenemos una clase para padres de Runner y tres subclases que heredan de ella: Jogger; Sprinter; y, Maratonista. Cada subclase anula el método de ejecución de la clase padre con su propia implementación. Posteriormente, cuando instanciamos a los corredores de cada tipo y los pasamos a un nuevo objeto de la carrera, podemos ver que cada uno utiliza su propio comportamiento cuando la carrera comienza.

El fragmento anterior funciona, pero tiene algunos problemas. Primero, está un poco hinchado porque hemos creado muchas subclases con el único propósito de cambiar un solo comportamiento. Si los corredores variaban de más maneras, esto podría valer la pena; sin embargo, en este sencillo programa, estas clases son probablemente innecesarias. Otro problema, tal vez más notable, es que nuestros corredores están fijados permanentemente a una subclase en particular. Si, por ejemplo, alice_ruby quería correr como una maratonista, no hay una buena manera de ayudarla a hacerlo sin cambiar completamente su clase.

Si la capacidad de cambiar el comportamiento dinámicamente es deseable, entonces veamos una posible solución.

Estrategias ingenuas y flujo de control

En un intento de mejorar nuestra implementación anterior del programa runner, a continuación tenemos una versión reformulada que no hace uso de la herencia.

En esta versión, tenemos una sola clase de Runner con un constructor que acepta un nuevo argumento: una estrategia. En este caso, nuestra estrategia es sólo un símbolo que utilizamos en un método de ejecución refactorizado. El nuevo método de ejecución contiene una sentencia de caso que verifica el atributo de estrategia de una instancia dada y ejecuta un poco de código en consecuencia. De hecho, cuando salimos de la carrera esta vez obtenemos el mismo resultado que antes.

De alguna manera, esta versión del programa es una mejora con respecto a la versión anterior, aunque en otros casos es un paso atrás. Por el lado positivo, ahora podemos cambiar la estrategia ingenua de un corredor usando un setter para asignarle un nuevo símbolo, como en alice_ruby.strategy = :marathon. De esta manera, somos capaces de cambiar efectivamente el comportamiento de un objeto en particular sin cambiar su clase. Sin embargo, la declaración de caso largo en el método Runner#run es problemática. Este tipo de flujo de control es una clara violación del OCP porque no podemos extender el método de ejecución sin abrirlo para su modificación. Entonces, ¿qué hacemos si queremos tener la capacidad de cambiar dinámicamente las estrategias sin dejar de adherirnos al OCP?

El Patrón de la Estrategia en Acción

En nuestra versión final de este programa finalmente vamos a usar el patrón de estrategia. En este caso, definimos un conjunto de estrategias en sus propias clases y luego proporcionamos esas clases a nuestros corredores a través de la inyección de dependencia.

Al igual que en nuestro segundo fragmento, nuestra clase Runner acepta un argumento de estrategia en la construcción y también tiene un ajustador para cambiar esa estrategia si se desea. Sin embargo, en lugar de pasar un simple símbolo a Runner para utilizarlo en una estructura de control, le pasamos una de las varias clases de estrategia definidas en el módulo RunStrategies. Cada una de estas estrategias tiene un método de ejecución, lo que significa que nuestros objetos cliente pueden ejecutar cualquiera de ellas con el mismo código. Dado que Ruby no tiene interfaces formales, proporcionamos nuestro propio mecanismo de comprobación de errores simple haciendo que cada estrategia herede de una clase RunStrategyInterface que genera un error si se llama a su método de clase de ejecución. (Si una estrategia no implementa una versión de este método por sí sola, entonces el método de clase de ejecución de RunStrategyInterface se ejecutará y generará un error que podremos probar antes de la implementación).

Cuando se ejecuta este programa, a cada corredor se le proporciona la estrategia deseada en la instanciación. Durante la ejecución del programa, los corredores son capaces de utilizar estas estrategias según sea necesario, pasando su propio nombre como contexto a la estrategia. Y si quisiéramos actualizar la estrategia de un corredor en particular a mitad del programa, podríamos hacerlo fácilmente con un método de setter, como en alice_ruby.strategy = RunStrategies::Marathon.

Al usar el patrón de estrategia, hemos dado a nuestro programa la capacidad de cambiar dinámicamente los algoritmos en tiempo de ejecución basado en el contexto. Además, nuestro método Runner#run es consistente con OCP porque podemos crear nuevos comportamientos simplemente implementando nuevas estrategias (en lugar de cambiar una estructura de control en el método Run)

.

TL;DR

El patrón de estrategia es un patrón de diseño de comportamiento utilizado para elegir y ejecutar dinámicamente los algoritmos en tiempo de ejecución. Este patrón es particularmente útil cuando una clase dada necesita ejecutar el mismo comportamiento de diferentes maneras en diferentes momentos. El patrón estratégico permite a un programa utilizar el polimorfismo sin una estructura de herencia de clase inflada y seguir siendo coherente con el principio de abierto/cerrado. Clásicamente, el patrón de estrategia se implementa usando abstracciones de interfaz, lo que nos permite crear múltiples implementaciones de estrategia para pasar a los objetos del cliente según sea necesario. Sin embargo, es posible utilizar el patrón de estrategia en idiomas sin interfaces formales siguiendo una serie de convenciones e implementando la comprobación de errores personalizada.

¡Eso es todo por nuestra discusión del patrón de estrategia! Manténgase en sintonía para futuras entradas en el blog sobre otros patrones de diseño.

Si quieres recibir alertas cuando se publique un nuevo artículo, puedes seguirme aquí en Medium, en Twitter o suscribirte a mi blog personal donde se publican estos artículos de forma cruzada. Feliz codificación!

Referencias

  1. Blog: Estrategia; Refactoring Guru
  2. Blog: Estrategia; OODesign
  3. Blog: Cómo usar el Patrón de Diseño de Estrategia en Ruby; RubyGuides
  4. Wikipedia: Patrón de estrategia
  5. Wikipedia: Patrones de diseño

Aprender a programar códigoDesarrollo de softwareCodificaciónProgramaciónIngeniería de softwareContinuar la discusión