En el diseño de software, el patrón estrategia permite definir una familia de algoritmos, encapsular cada uno y hacerlos intercambiables. Este patrón garantiza que los algoritmos puedan variar independientemente de los clientes que los utilizan. Para ilustrarlo, consideremos un sistema de simulación de aves llamado "SimAves", donde diferentes tipos de aves nadan y graznan. Inicialmente, el diseño usa herencia con una clase base abstracta.
public abstract class Ave
{
public void Nadar()
{
Console.WriteLine("Puedo nadar");
}
public void Graznar()
{
Console.WriteLine("Puedo graznar");
}
public void Volar()
{
Console.WriteLine("Puedo volar");
}
public abstract void Mostrar();
}
public class AvePatoReal : Ave
{
public override void Mostrar()
{
Console.WriteLine("Soy un pato real");
}
}
public class AvePatoModelo : Ave
{
public override void Mostrar()
{
Console.WriteLine("Soy un pato de juguete");
}
}
class Programa
{
static void Main(string[] args)
{
Ave aveModelo = new AvePatoModelo();
aveModelo.Graznar();
aveModelo.Mostrar();
aveModelo.Volar();
Console.ReadLine();
}
}
Este enfoque presenta un problema: el ave de juguete hereda el método Volar, aunque no debería volar. Agregar interfaces como IVolador e IGraznador podría ayudar, pero conduciría a duplicación de código y falta de reutilización. Aquí surge un prinicpio clave: aislar los aspectos que cambian de los que permanecen constantes.
Para aplicar este principio, separamos las comportamientos volar y graznar en interfaces independientes. Luego, implementamos clases concretas para cada comportamiento, lo que permite que las aves cambien dinámicamente su comportamiento en tiempo de ejecución. Otro principio relevante es programar hacia interfcaes, no hacia implementaciones concretas.
Diseñemos las interfaces y sus implementaciones:
public interface IComportamientoVuelo
{
void EjecutarVuelo();
}
public interface IComportamientoGraznido
{
void EjecutarGraznido();
}
public class VueloNoPosible : IComportamientoVuelo
{
public void EjecutarVuelo()
{
Console.WriteLine("No puedo volar");
}
}
public class VueloConAlas : IComportamientoVuelo
{
public void EjecutarVuelo()
{
Console.WriteLine("Vuelo con alas");
}
}
public class GraznidoSilencioso : IComportamientoGraznido
{
public void EjecutarGraznido()
{
Console.WriteLine("No hago sonido");
}
}
public class GraznidoFuerte : IComportamientoGraznido
{
public void EjecutarGraznido()
{
Console.WriteLine("¡Graznido fuerte!");
}
}
La clase base Ave ahora delega los comportamientos a objetos de estas interfaces, usando composición en lugar de herencia. Se agregan métodos para establecer dinámicamente los comportamientos.
public abstract class Ave
{
protected IComportamientoVuelo comportamientoVuelo;
protected IComportamientoGraznido comportamientoGraznido;
public abstract void Mostrar();
public void Nadar()
{
Console.WriteLine("Puedo nadar");
}
public void RealizarVuelo()
{
comportamientoVuelo.EjecutarVuelo();
}
public void RealizarGraznido()
{
comportamientoGraznido.EjecutarGraznido();
}
public void EstablecerComportamientoVuelo(IComportamientoVuelo nuevoComportamiento)
{
comportamientoVuelo = nuevoComportamiento;
}
public void EstablecerComportamientoGraznido(IComportamientoGraznido nuevoComportamiento)
{
comportamientoGraznido = nuevoComportamiento;
}
}
public class AvePatoModelo : Ave
{
public AvePatoModelo()
{
comportamientoVuelo = new VueloNoPosible();
comportamientoGraznido = new GraznidoSilencioso();
}
public override void Mostrar()
{
Console.WriteLine("Soy un pato de juguete");
}
}
class Programa
{
static void Main(string[] args)
{
Ave aveModelo = new AvePatoModelo();
aveModelo.Mostrar();
aveModelo.RealizarVuelo();
aveModelo.RealizarGraznido();
aveModelo.EstablecerComportamientoVuelo(new VueloConAlas());
aveModelo.RealizarVuelo();
Console.ReadLine();
}
}
Este diseño ilustra el patrón estrategia: los comportamientos están encapsulados en clases separadas y se componen dentro de las aves. La relación "tiene-un" (composición) es más flexible que la relación "es-un" (herencia), permitiendo cambios en tiempo de ejecución y reutilización de código. Los principios clave son: encapsular lo que varía, programar hacia interfaces y favorecer la composición sobre la herencia.