Objetivo:
«Definir una familia de algoritmos, encapsular cada uno de ellos y hacerlos intercambiables. Strategy permite cambiar el algoritmo independientemente de los clientes que lo utilicen».
Design Patterns: Elements of Reusable Object-Oriented Software
En el artículo anterior veíamos cómo un patrón de diseño puede ayudarnos a encapsular distintos algoritmos a partir de una «plantilla» cuyos hijos se encargaban de especializar. El patrón Strategy, por su parte, no bucea en los detalles, sino que va un paso más allá: encapsulará un algoritmo completo ignorando los detalles de su implementación, permitiendo intercambiarlo en tiempo de ejecución para permitir actuar a la clase cliente con un comportamiento distinto.
El nombre de este patrón evoca la posibilidad de realizar un cambio de estrategia en tiempo de ejecución sustituyendo un objeto que se encargará de implementarla. No nos preocupará el «cómo». De hecho, ni siquiera nos importará «el qué»: la clase que actúa como interfaz del patrón únicamente tendrá que exponer el método o métodos que deberá invocar el cliente.
Strategy pattern y videojuegos
Un ejemplo clásico para entender este patrón es el de un protagonista de un videojuego en el cual manejamos a un soldado que puede portar y utilizar varias armas distintas. La clase (o clases) que representan a nuestro soldado no deberían de preocuparse de los detalles de las armas que porta: debería bastar, por ejemplo, con un método de interfaz «atacar» que dispare el arma actual y otro método «recargar» que inserte munición en ésta (si se diera el caso). En un momento dado, otro método «cambiarArma» podrá sustituir el objeto equipado por otro, manteniendo la interfaz intacta. Da igual que nuestro soldado porte un rifle, una pistola o un fusil: los detalles de cada estrategia estarán encapsulados dentro de cada una de las clases intercambiables que representan las armas. Nuestra clase cliente (el soldado) únicamente debe preocuparse de las acciones comunes a todas ellas: atacar, recargar y cambiar de arma. Éste último método, de hecho, será el encargado de realizar la operación de «cambio de estrategia» que forma parte del patrón.
Por lo tanto, el patrón Strategy es, como podemos imaginar, uno de los patrones más utilizados a la hora de diseñar software. Siempre que exista una posibilidad de realizar una tarea de distintas formas posibles, el patrón Strategy tendrá algo que decir al respecto.
Nuestro diagrama de clases muestra una interfaz denominada IStrategy que expone un método operacion(). Las clases que implementen esta interfaz serán aquellas que implementen las distintas estrategias a realizar por el cliente, y como podemos observar, no tienen mayor dificultad: una interfaz, varias clases que la implementan. Programación Orientada a Objetos básica.
La filosofía del patrón, por lo tanto, radica en el enlace entre la llamada clase de contexto y la propia interfaz. Esta clase de contexto será el «broker» o intermediario entre el cliente y las clases que implementan la estrategia, y por lo tanto, sus funciones serán simples: cambiar la estrategia actual y ejecutarla. Si hablásemos de un videojuego, el contexto sería el objeto encargado de equipar la pistola al pulsar la tecla «1» y de invocar la funcionalidad de disparo cuando hacemos click con el ratón. Esto se traduciría por instanciar un objeto StrategyPistola en la referencia IStrategy e invocar el método «atacar()» de ésta para que la clase que implementa la interfaz se encargue del trabajo sucio (¡nunca mejor dicho!).
Implementando el patrón
Pese a que el ejemplo de nuestro videojuego es bastante ilustrativo, dado que en esta serie de artículos tenemos fijación por los coches, implementaremos el patrón a través de un hipotético módulo de la centralita del vehículo que nos permitirá alternar entre una conducción normal y deportiva. La diferencia entre ambas será simple: mayor consumo, mayor potencia y mayor velocidad. Podríamos añadir más comportamientos «personalizados», como el endurecimiento de la suspensión, pero con estos dos elementos será suficiente para captar la idea.
Comencemos añadiendo la interfaz ITipoConduccion, que simbolizará la interfaz de la estrategia (IStrategy). Le añadiremos tres métodos: uno para obtener la descripción del tipo de conducción actual, otro que proporcione el incremento de velocidad en relación al combustible inyectado y un tercer método que indique la cantidad de potencia suministrada por el motor, también en proporción al combustible que recibe.
ITipoConduccion.cs
ITipoConduccion.cs
public interface ITipoConduccion { string ObtenerDescripcion(); int ObtenerPotencia(float decilitrosCombustible); int ObtenerIncrementoVelocidad(float decilitrosCombustible); }
A continuación añadiremos las estrategias en sí, es decir, las clases que implementan la interfaz y dotan de distintos comportamientos que serán seleccionados por el contexto. Habíamos dicho que utilizaríamos dos: conducción normal y conducción deportiva. Por lo tanto, crearemos dos clases que proporcionen distintos comportamientos para los mismos métodos:
ConduccionNormal.cs
public class ConduccionNormal : ITipoConduccion { public string ObtenerDescripcion() { return "Conduccion Normal"; } public int ObtenerPotencia(float decilitrosCombustible) { return (int)(decilitrosCombustible * 0.842) + 3; } public int ObtenerIncrementoVelocidad(float decilitrosCombustible) { return (int)(decilitrosCombustible * 0.422) + 2; } }
ConduccionDeportiva.cs
public class ConduccionDeportiva : ITipoConduccion { public string ObtenerDescripcion() { return "Conduccion Deportiva"; } public int ObtenerPotencia(float decilitrosCombustible) { return (int)(decilitrosCombustible * 0.987) + 5; } public int ObtenerIncrementoVelocidad(float decilitrosCombustible) { return (int)(decilitrosCombustible * 0.618) + 3; } }
Lo siguiente será crear el contexto. Esta clase será la encargada de establecer la conexión entre el cliente y las clases que implementan la estrategia, sustituyendo la clase que la implementa dependiendo del comportamiento esperado. Por lo tanto, se compondrá de una referencia a la interfaz que implementarán las estrategias más un método que permita cambiar de instancia (es decir, una property o un setter de toda la vida). A partir de esta funcionalidad básica, el contexto podrá realizar otras operaciones relacionadas con la estrategia que pretende modelar, como por ejemplo la invocación de sus métodos o la encapsulación del cambio de estrategia.
Contexto.cs
public class Contexto { // Referencia a la interfaz private ITipoConduccion tipoConduccion; // Propiedad que establecerá un nuevo tipo de conducción (cambio de estrategia) public ITipoConduccion TipoConduccion { get { return tipoConduccion; } set { this.tipoConduccion = value; } } // Métodos de servicio (invocan los métodos implementados por las estrategias) public string ObtenerDescripcion() { return tipoConduccion.ObtenerDescripcion(); } public int IncrementarVelocidad(float combustible) { return tipoConduccion.ObtenerIncrementoVelocidad(combustible); } public int IncrementarPotencia(float combustible) { return tipoConduccion.ObtenerPotencia(combustible); } }
En realidad, la propia clase cliente puede actuar como clase de contexto, pero siempre será mejor minimizar el acoplamiento entre las estrategias y las reglas de negocio. De este modo, respetaremos otro de los principios de la orientación a objetos: una clase, una responsabilidad. Para comprobar el funcionamiento de nuestro cliente, bastará con utilizar el siguiente código que hará uso del contexto para cambiar de estrategia en tiempo de ejecución:
Vehiculo.cs
public class Vehiculo { private Contexto contexto; public Vehiculo() { contexto = new Contexto(); } public void ConduccionDeportiva() { ITipoConduccion conduccionDeportiva = new ConduccionDeportiva(); contexto.TipoConduccion = conduccionDeportiva; } public void ConduccionNormal() { ITipoConduccion conduccionNormal = new ConduccionNormal(); contexto.TipoConduccion = conduccionNormal; } // Métodos que invocan la funcionalidad implementada por la interfaz public void Acelerar(float combustible) { string descripcion = contexto.ObtenerDescripcion(); int incrementoVelocidad = contexto.IncrementarVelocidad(combustible); int potencia = contexto.IncrementarPotencia(combustible); Console.WriteLine("Tipo de conducción " + descripcion); Console.WriteLine("Combustible inyectado: " + combustible); Console.WriteLine("Potencia proporcionada: " + potencia); Console.WriteLine("Incremento de velocidad: " + incrementoVelocidad); } }
Finalmente, el código que invoca a nuestro cliente, que será el siguiente:
static void Main(string[] args) { Vehiculo v = new Vehiculo(); v.ConduccionDeportiva(); v.Acelerar(2.4f); Console.WriteLine(""); v.ConduccionNormal(); v.Acelerar(2.4f); Console.ReadLine(); }
El resultado final será el siguiente:
¿Cuándo utilizar este patrón? Ejemplos reales
Este patrón es aconsejable, como ya hemos comentado, en situaciones en los que una misma operación (o conjunto de operaciones) puedan realizarse de formas distintas. A grosso modo, el patrón Strategy realiza una tarea bastante similar al patrón Template Method, salvo porque en este caso el algoritmo no tiene por qué contar con pasos en común y porque Strategy confía en la composición mientras que Template Method se basa en la herencia.
Ejemplos reales de este patrón se aplican, por ejemplo, la serialización de objetos. Una interfaz que exponga un método serialize() podrá codificar un objeto en distintos formatos (String64, XML, JSON). El cliente no necesita saber cómo se realizará esta operación: bastará con que el contexto seleccione la estrategia adecuada y el resultado de la operación dependerá de la opción concreta que se haya seleccionado. Del mismo modo podemos pensar en una conexión a un servicio web: podremos realizarla mediante TCP/IP, HTTP, HTTPS, Named Pipes… todo esto deberá ser transparente para el cliente: El contexto será el encargado de adoptar una forma concreta de conexión.
¿Más ejemplos? Los compresores funcionan a través de estrategias (se utiliza un algoritmo distinto para comprimir en zip o en rar), y en general, cualquier programa capaz de almacenar y transmitir datos en distintos formatos implementarán este patrón. Por lo tanto, como podemos imaginar, nos encontramos, tanto por su utilidad como por su sencillez, con uno de los patrones más utilizados de todos los expuestos por el GoF.
Fuentes:
- Head First Design Patterns (Freeman et al.)
Hola Daniel,
Como siempre, aquí estoy puntual a tu patrón semanal. 🙂
Creo que has descrito perfectamente este patrón y su funcionamiento pero la implementación que has realizado en el cliente no corresponde con el nombre de la clase «Contexto» que has definido.
En cualquier caso, queda lo suficientemente claro.
Muchas gracias.
Hola, José Miguel.
Siento el retraso de esta semana, ha sido complicada y he tardado algo más de lo habitual en publicar. En cuanto al ejemplo, ¡tienes razón! En realidad lo que ha ocurrido es que he olvidado incluir el código de la clase cliente, «Vehiculo.cs». ¡Actualizado!
Como siempre, mil gracias por tus comentarios 😉
Eres un crack!!!
buenas tardes mira me sale el siguiente error
Error 1 Incoherencia de accesibilidad: el tipo de propiedad ‘iestrategia.ITipoConduccion’ es menos accesible que la propiedad ‘iestrategia.Contexto.TipoConduccion’ C:\Users\Seal\Documents\Visual Studio 2012\Projects\iestrategia\iestrategia\Contexto.cs 16 33 iestrategia
Excelente ejemplo Daniel Garcia.. solo tengo una observación en el diagrama UML -si respetamos la simbología de los diagramas- tienes una agregación de estrategias dentro de tu Contexto por lo tanto debería tener un método «addStategy» y un atributo tipo colección donde almacene las diferentes estrategias por ejemplo «- Strategies : Array».
De igual manera, se podría cambiar el símbolo de agregación por la de dependencia del Contexto hacia la Interfaz IStrategy y dejar la propiedad «setStretegy» y el atributo privado «estrategia».
Cualquiera de las 2 opciones son validas, solamente sería la corrección del diagrama UML.
Saludos,
Muy claro y bien explicado. Gracias.
Muchas gracias Daniel, ha sido de gran utilidad!