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