El patrón de diseño Strategy es un patrón de programación orientado a objetos que se utiliza para permitir que un objeto cambie su comportamiento en tiempo de ejecución sin cambiar su interfaz. El patrón Strategy define una familia de algoritmos, encapsulándolos y haciendo que sean intercambiables. Esto permite que el cliente elija el algoritmo que desee utilizar en tiempo de ejecución.
En este patrón, se crea una interfaz común para una familia de algoritmos. Luego, se implementan varias clases que representan cada uno de los algoritmos. Cada clase de algoritmo implementa la interfaz común y proporciona su propia implementación de la lógica del algoritmo. Luego, el cliente puede utilizar cualquiera de las clases de algoritmo intercambiables sin conocer los detalles de su implementación interna.
El patrón de diseño Strategy se utiliza comúnmente en situaciones en las que se necesitan múltiples algoritmos para realizar una tarea determinada y se desea que el algoritmo utilizado se pueda cambiar en tiempo de ejecución sin afectar el resto del código. También se puede utilizar para simplificar el código y reducir la complejidad al separar la lógica de la aplicación de la implementación del algoritmo.
Yo, sin conocer aun este patrón, lo implementé indirectamente en una aplicación para el envío de correos electrónicos, donde se disponía de diversos proveedores, Sendgrid o SMTP. Yo pensaba que estaba utilizando el patrón Factory, pero tras investigar, me di cuenta de que apliqué este patron Strategy.
Un ejemplo con c#
Supongamos que queremos implementar una clase que pueda calcular el costo de envío de un paquete. Podemos tener diferentes algoritmos para calcular el costo de envío, como el envío terrestre, el envío aéreo o el envío marítimo. En lugar de tener una clase grande que tenga todos estos algoritmos implementados, podemos utilizar el patrón Strategy para separar la implementación de los algoritmos y hacer que sean intercambiables.
Primero, definimos la interfaz común para nuestros algoritmos:
public interface IShippingStrategy
{
double CalculateShippingCost(double weight);
}
A continuación, implementamos nuestras clases de algoritmo:
public class GroundShippingStrategy : IShippingStrategy
{
public double CalculateShippingCost(double weight)
{
// Lógica para calcular el costo de envío terrestre
return weight * 0.5;
}
}
public class AirShippingStrategy : IShippingStrategy
{
public double CalculateShippingCost(double weight)
{
// Lógica para calcular el costo de envío aéreo
return weight * 1.5;
}
}
public class SeaShippingStrategy : IShippingStrategy
{
public double CalculateShippingCost(double weight)
{
// Lógica para calcular el costo de envío marítimo
return weight * 0.75;
}
}
Finalmente, creamos una clase que utiliza la interfaz común para calcular el costo de envío y permite cambiar el algoritmo de envío en tiempo de ejecución:
public class ShippingCostCalculator
{
private IShippingStrategy _shippingStrategy;
public ShippingCostCalculator(IShippingStrategy shippingStrategy)
{
_shippingStrategy = shippingStrategy;
}
public double CalculateShippingCost(double weight)
{
return _shippingStrategy.CalculateShippingCost(weight);
}
public void SetShippingStrategy(IShippingStrategy shippingStrategy)
{
_shippingStrategy = shippingStrategy;
}
}
Ahora podemos utilizar esta clase de la siguiente manera:
ShippingCostCalculator calculator = new ShippingCostCalculator(new GroundShippingStrategy());
double groundShippingCost = calculator.CalculateShippingCost(10); // Devuelve 5
calculator.SetShippingStrategy(new AirShippingStrategy());
double airShippingCost = calculator.CalculateShippingCost(10); // Devuelve 15
calculator.SetShippingStrategy(new SeaShippingStrategy());
double seaShippingCost = calculator.CalculateShippingCost(10); // Devuelve 7.5
De esta manera, podemos cambiar el algoritmo de envío en tiempo de ejecución sin tener que cambiar el código de la clase ShippingCostCalculator. Esto nos permite añadir nuevos algoritmos de envío en el futuro sin tener que modificar el código existente.
Se puede utilizar una variable o una enumeración para definir qué algoritmo utilizar en la clase ShippingCostCalculator. Aquí te dejo un ejemplo en C#:
Primero, definimos una enumeración que represente los diferentes algoritmos de envío:
public enum ShippingStrategyType
{
Ground,
Air,
Sea
}
A continuación, modificamos la clase ShippingCostCalculator para que pueda recibir el tipo de estrategia de envío y utilizar la implementación correspondiente:
public class ShippingCostCalculator
{
private IShippingStrategy _shippingStrategy;
public ShippingCostCalculator(ShippingStrategyType shippingStrategyType)
{
SetShippingStrategy(shippingStrategyType);
}
public double CalculateShippingCost(double weight)
{
return _shippingStrategy.CalculateShippingCost(weight);
}
public void SetShippingStrategy(ShippingStrategyType shippingStrategyType)
{
switch (shippingStrategyType)
{
case ShippingStrategyType.Ground:
_shippingStrategy = new GroundShippingStrategy();
break;
case ShippingStrategyType.Air:
_shippingStrategy = new AirShippingStrategy();
break;
case ShippingStrategyType.Sea:
_shippingStrategy = new SeaShippingStrategy();
break;
default:
throw new ArgumentException("Tipo de estrategia de envío inválido.");
}
}
}
Ahora podemos utilizar esta clase de la siguiente manera:
ShippingCostCalculator calculator = new ShippingCostCalculator(ShippingStrategyType.Ground);
double groundShippingCost = calculator.CalculateShippingCost(10); // Devuelve 5
calculator.SetShippingStrategy(ShippingStrategyType.Air);
double airShippingCost = calculator.CalculateShippingCost(10); // Devuelve 15
calculator.SetShippingStrategy(ShippingStrategyType.Sea);
double seaShippingCost = calculator.CalculateShippingCost(10); // Devuelve 7.5
De esta manera, podemos definir qué algoritmo utilizar mediante el uso de una variable o una enumeración en lugar de pasar una instancia de una clase específica. Esto nos permite tener un código más limpio y legible y facilita la adición de nuevos algoritmos en el futuro.
Utilizando el principio Open-Close
se puede hacer la selección del algoritmo de envío dinámica sin modificar la clase ShippingCostCalculator, respetando el principio de Open/Close. Esto se puede lograr utilizando un diccionario donde las claves sean los tipos de estrategias de envío y los valores sean las instancias correspondientes de las clases de estrategia.
Aquí te dejo un ejemplo de cómo hacerlo:
Primero, definimos una enumeración que represente los diferentes tipos de estrategias de envío:
public enum ShippingStrategyType
{
Ground,
Air,
Sea
}
A continuación, definimos una interfaz IShippingStrategyFactory que define un método para obtener una instancia de la estrategia de envío correspondiente a un tipo dado:
public interface IShippingStrategyFactory
{
IShippingStrategy GetShippingStrategy(ShippingStrategyType shippingStrategyType);
}
Luego, implementamos esta interfaz en una clase ShippingStrategyFactory que utiliza un diccionario para obtener las instancias de las clases de estrategia correspondientes a los diferentes tipos de estrategias:
public class ShippingStrategyFactory : IShippingStrategyFactory
{
private readonly Dictionary _shippingStrategies;
public ShippingStrategyFactory()
{
_shippingStrategies = new Dictionary
{
{ ShippingStrategyType.Ground, new GroundShippingStrategy() },
{ ShippingStrategyType.Air, new AirShippingStrategy() },
{ ShippingStrategyType.Sea, new SeaShippingStrategy() }
};
}
public IShippingStrategy GetShippingStrategy(ShippingStrategyType shippingStrategyType)
{
if (!_shippingStrategies.TryGetValue(shippingStrategyType, out var shippingStrategy))
{
throw new ArgumentException("Tipo de estrategia de envío inválido.");
}
return shippingStrategy;
}
}
Finalmente, utilizamos la clase ShippingCostCalculator modificando el constructor para que reciba una instancia de IShippingStrategyFactory y utilizamos el método GetShippingStrategy para obtener la estrategia de envío correspondiente al tipo de estrategia especificado:
public class ShippingCostCalculator
{
private readonly IShippingStrategy _shippingStrategy;
public ShippingCostCalculator(IShippingStrategyFactory shippingStrategyFactory, ShippingStrategyType shippingStrategyType)
{
_shippingStrategy = shippingStrategyFactory.GetShippingStrategy(shippingStrategyType);
}
public double CalculateShippingCost(double weight)
{
return _shippingStrategy.CalculateShippingCost(weight);
}
}
De esta manera, podemos utilizar la clase ShippingCostCalculator de la siguiente manera:
var shippingStrategyFactory = new ShippingStrategyFactory();
ShippingCostCalculator calculator = new ShippingCostCalculator(shippingStrategyFactory, ShippingStrategyType.Ground);
double groundShippingCost = calculator.CalculateShippingCost(10); // Devuelve 5
calculator = new ShippingCostCalculator(shippingStrategyFactory, ShippingStrategyType.Air);
double airShippingCost = calculator.CalculateShippingCost(10); // Devuelve 15
calculator = new ShippingCostCalculator(shippingStrategyFactory, ShippingStrategyType.Sea);
double seaShippingCost = calculator.CalculateShippingCost(10); // Devuelve 7.5
De esta manera, la selección del algoritmo de envío es dinámica dependiendo de la variable de entrada, pero no se requiere modificar la clase ShippingCostCalculator. Además, es fácil agregar nuevas estrategias de envío simplemente agregando una nueva entrada en el diccionario en la clase ShippingStrategyFactory, sin necesidad de modificar la lógica de la clase ShippingCostCalculator.
Cuando no es recomendable utilizar este patrón
El patrón de diseño Strategy es una buena opción cuando quieres tener múltiples algoritmos o estrategias intercambiables para resolver un problema específico, y necesitas seleccionar y cambiar entre ellos dinámicamente en tiempo de ejecución. Sin embargo, no siempre es la mejor opción en todas las situaciones. Aquí hay algunas consideraciones cuando puede no ser recomendable utilizar el patrón Strategy:
* Complejidad innecesaria: Si solo tienes un algoritmo o estrategia fija para resolver el problema y no anticipas la necesidad de cambiarlo en el futuro, implementar el patrón Strategy puede agregar complejidad innecesaria al código.
* Overhead de rendimiento: El uso del patrón Strategy puede implicar una capa adicional de indirección en la ejecución del código, lo que puede tener un impacto en el rendimiento. Si el rendimiento es una consideración crítica en tu aplicación y el cambio dinámico de algoritmos no es necesario, el patrón Strategy puede no ser la mejor opción.
* Simplicidad del problema: Si el problema que estás resolviendo es simple y no requiere múltiples algoritmos intercambiables, el uso del patrón Strategy puede ser excesivo y complicar innecesariamente el código.
* Cambios poco probables: Si los cambios en los algoritmos o estrategias son poco probables o poco frecuentes, y no tienen un impacto significativo en el diseño y mantenimiento del código, el patrón Strategy puede no ser necesario y se puede optar por una implementación más simple.
En resumen, el patrón de diseño Strategy es útil cuando necesitas cambiar dinámicamente entre múltiples algoritmos o estrategias en tiempo de ejecución. Sin embargo, en casos donde la complejidad, el rendimiento, la simplicidad del problema o la probabilidad de cambios son factores importantes, el uso del patrón Strategy puede no ser recomendable. Es importante evaluar cuidadosamente las necesidades y requerimientos de tu aplicación antes de decidir si el patrón Strategy es apropiado en tu caso.