La mayor parte de los problemas que nos podemos encontrar al usar patrones de diseño vienen de no ser capaces de reconocer en qué contextos hay que aplicarlos. Caemos en la trampa de, al intentar escribir código mejor, generar deuda técnica, ya que el patrón aplicado no resolvía el problema que teníamos en realidad. Es por eso que la pregunta más importante que tenemos que responder cuando empezamos a estudiar un patrón de diseño es: ¿para qué sirve?.
El patrón Strategy tiene sentido cuando nos encontramos en un escenario en el que para conseguir un objetivo, tenemos diferentes formas de afrontarlo. Por poner un ejemplo, imaginemos que estamos desarrollando un programa que añade estilos en formato HTML a un texto. Los estilos que soporta serán: poner en negrita, letra cursiva y subrayar.
Una solución que podríamos dar sería:
public enum Styles { Bold, Italic, Underline } public class Styler { public string SetStyle(string input, Styles style) { switch (style) { case Styles.Bold: return "<b>" + input + "</b>"; case Styles.Italic: return "<i>" + input + "</i>"; case Styles.Underline: return "<u>" + input + "</u>"; default: throw new NotImplementedException("This style is not supported"); } } }
Como podemos ver, hemos usado un bloque de tipo “switch” y en dependencia del estilo que queramos añadirle al texto de entrada (“input”), realizamos una acción u otra. El resultado final es que si dentro de dos días tenemos el requerimiento de añadir dos o tres estilos nuevos a nuestro sistema, tendremos que añadir más bloques “case” a nuestro código. Esto en definitiva, lo que provocaría es que no estuviéramos cumpliendo con el principio SOLID de Open Closed.
Este principio señala que nuestro código debería estar abierto a la extensión, pero cerrado a la modificación. Es decir, que para añadir una nueva funcionalidad, no tengamos la necesidad de modificar los algoritmos que ya están programados. Y es aquí es donde el patrón strategy cobra su importancia.
Con el fin de no tener que modificar nuestro bloque “switch” con cada nuevo estilo que queramos aplicar al texto, vamos a dividir cada uno de los estilos existentes, en clases más pequeñas, que solo resuelvan un estilo cada una:
public class BoldStyler { public string SetStyle(string input) { return "<b>" + input + "</b>"; } } public class ItalicStyler { public string SetStyle(string input) { return "<i>" + input + "</i>"; } } public class UnderlineStyler { public string SetStyle(string input) { return "<u>" + input + "</u>"; } }
Como podemos ver, todas las clases son muy parecidas y podríamos sacar una interface común que defina su comportamiento:
public interface IStyler { string SetStyle(string input); } public class BoldStyler : IStyler //... public class ItalicStyler : IStyler //... public class UnderlineStyler : IStyler //...
Ahora tendríamos que modificar el código inicial para que use estas nuevas clases y seguir resolviendo el problema:
public class Styler { public string SetStyle(string input, IStyler styler) { return styler.SetStyle(input); } }
Y si por un casual, ahora necesitáramos añadir un nuevo formato a nuestro programa, solo tendríamos que desarrollar un nuevo objeto que implementara “IStyler”. Y así evitaríamos tener que modificar el código que ya tenemos escrito.
Dando un repaso a los objetos que hemos ido desarrollando hasta este momento, podríamos extraer un diagrama de clases como este:
Donde vemos que nuestro objeto “Styler” consume objetos que implementan “IStyler” como son “BoldStyler”, “ItalicStyler”, … . Un diagrama semejante al que define el patrón Strategy:
Donde los artefactos son:
- Context: es el objeto que orquesta el funcionamiento de las estrategias. Puede recibir una de estas por parámetro o gestionarlas internamente.
- IStrategy: es la definición común que tiene que implementar todo algoritmo que soporte el sistema.
- ConcreteStrategyX: los objetos que, implementando la interface común, desarrollan un algoritmo concreto.
En este primer ejemplo hemos creado un contexto que acepta recibir una estrategia como parámetro, pero quizá, el problema podría ser que obligatoriamente hay que aplicar los tres formatos al texto que le pasemos. En este caso podríamos generar una clase de contexto como esta:
public class Styler { private readonly List<IStyler> strategies = new List<IStyler> { new BoldStyler(), new ItalicStyler(), new UnderlineStyler() }; public string SetStyle(string input) { var result = input; foreach(var strategy in this.strategies) { result += strategy.SetStyle(result); } return result; } }
Y si usáramos alguna Framework de inyección de dependencias como Autofac, StructureMaps o NInject, podríamos aprovecharnos de los sistemas de escaneo de ensamblados para poder obtener en el constructor todas las estrategias de un tipo correspondiente:
public class Styler { private readonly IEnumerable<IStyler> strategies; public Styler(IEnumerable<IStyler> strategies) { this.strategies = strategies; } public string SetStyle(string input) { var result = input; foreach(var strategy in this.strategies) { result += strategy.SetStyle(result); } return result; } }
Una de las grandes ventajas del Strategy Pattern es que no es exclusivo de c# o la plataforma .Net, puede ser usado con diferentes lenguajes de programación, como por ejemplo Javascript. Una implementación de este mismo código podría ser esta:
function HtmlStyler() { this.setStyle = function (input) { var result = input; for (var key in this.strategies) { var strategy = this.strategies[key]; if (strategy.setStyle) result = strategy.setStyle(result); else throw "Invalid strategy"; } return result; }; } HtmlStyler.prototype.strategies = { }; HtmlStyler.prototype.strategies.boldStyler = { setStyle: function(input) { return '<b>' + input + '</b>'; }, }; HtmlStyler.prototype.strategies.italicStyler = { setStyle: function (input) { return '<i>' + input + '</i>'; }, }; HtmlStyler.prototype.strategies.underlineStyler = { setStyle: function (input) { return '<u>' + input + '</u>'; }, };
La peculiaridad de esta implentación en Javascript es el uso de “prototype” para facilitar las futuras características nuevas que se pueden desarrollar. No hará falta modificar el programa original, si no añadir una nueva propiedad a las estrategias del prototipo de nuestro objeto.
Y cómo no, también podríamos realizar el mismo código en el lenguaje de programación de moda, Typescript:
interface IStyler { setStyle: (input: string) => string; } class HTMLStyler { strategies: IStyler[]; constructor (strategies: IStyler[]) { this.strategies = strategies; } setStyle(input: string):string { var result: string = input; for (var i = 0; i < this.strategies.length; i++) { var strategy : IStyler = this.strategies[i]; result = strategy.setStyle(result); } return result; } } class BoldStyler implements IStyler { setStyle(input: string) { return '<b>' + input + '</b>'; } } class ItalicStyler implements IStyler { setStyle(input: string) { return '<i>' + input + '</i>'; } } class UnderlineStyler implements IStyler { setStyle(input: string) { return '<u>' + input + '</u>'; } } var styler = new HTMLStyler([new BoldStyler(), new ItalicStyler(), new UnderlineStyler()]); alert(styler.setStyle('hola!'));
Y podremos ver claramente que el resultado final se asemeja más al que desarrollamos con c#, que a la solución propuesta en javascript.
Conclusiones
Mientras describíamos el patrón Strategy hemos dejado caer alguno de sus beneficios, como por ejemplo que es más fácil de leer el código. También será más fácil por tanto de mantener y por supuesto de ampliar con nuevas funcionalidades. Gracias a este patrón vamos a cumplir con dos de los principios de SOLID: el principio de responsabilidad única, al crear pequeñas clases que contienen un algoritmo muy concreto; y el de abierto/cerrado, abriendo nuestra solución a la extensión pero no a la modificación.
También queda claro el problema que podría suponer a largo plazo: una gran cantidad nueva de objetos para que sean gestionados por el hilo principal de nuestro programa. Por lo que para un sistema de tiempo real o donde la velocidad de respuesta y el poco consumo de recursos, fueran lo más importante, no sería la implementación ideal.
No obstante, teniendo en cuenta que en este ejemplo usamos lenguajes como Javascript o c#, en los que la velocidad no es su punto fuerte, siempre deberíamos pensar en este patrón antes de escribir un bloque “switch”.