El patrón Estrategia en C#


Publicado el jueves, 17 de agosto de 2017

Patrón Estrategia en C#


Cuando conocí los patrones de diseño ya llevaba algunos años trabajando en el sector y hasta entonces no les había dado importancia. Estaban ahí pero era un tema distante. Incluso trabajando en equipos de grandes empresas con grandes proyectos, nadie los mencionaba.

Fue leer el primer capítulo del libro “Head First Dessign Patterns” y hacer “clic”: descubrí nuevos puntos de vista y nuevas maneras de programar. Hasta entonces utilizaba clases y objetos pero no estaba aprovechando todo el potencial de los conceptos de POO (Programación Orienteda a Objetos). Cuando un problema se complicaba, o cuando había un cambio en una especificación, lo resolvía utilizando la fuerza bruta: copy-paste y tira pa’lante.

Los patrones de diseño, y en especial el patrón estrategia, me llevó a comprender y aplicar mejor algunos de los conceptos de POO.

En este post voy a explicar el patrón de diseño estrategia con el mismo ejemplo que utiliza el libroHead First Dessign Patterns”. Una vez explicado también mostraré un caso real donde lo he aplicado.

La aplicación de los patos

Sí, los protagonistas de la aplicación de ejemplo son patos :)

Todos los patos graznan (hacen cuac) y nadan, pero no todos tienen la misma forma.

Diagrama del patrón estrategia

La clase base Pato implementa los métodos Graznar() y Nadar() que son comunes para todos los patos, pero el método Mostrar() es abstracto. Cada clase subtipo de pato se encarga de implementar el método.

    public abstract class Pato
    {
        public void Graznar()
        {
            Console.WriteLine("¡Cuac, cuac!");
        }

        public void Nadar()
        {
            Console.WriteLine("Nadando");
        }
        
        public abstract void Mostrar();
    }

    public class PatoDomestico : Pato
    {
        public override void Mostrar()
        {
            Console.WriteLine("Soy un pato doméstico");
        }
    }

    public class PatoPelirrojo : Pato
    {
        public override void Mostrar()
        {
            Console.WriteLine("Soy un pato pelirrojo");
        }
    }

Para poder reutilizar los métodos Graznar() y Nadar() hemos creado un árbol de herencia, haciendo que todas las clases que hereden de Pato ya incorporen los métodos Graznar() y Nadar().

Aparece una nueva especificación

Ok, parece que el programa tiene éxito y nos piden que lo evolucionemos añadiendo una funcionalidad nueva: quieren que los patos también vuelen.

¿Dónde colocar el nuevo método volar? Muy fácil, en la clase abstracta Pato podríamos añadir un nuevo método Volar() de modo que todos los patos tengan en común esta funcionalidad.

    public abstract class Pato
    {
        public void Graznar()
        {
            Console.WriteLine("¡Cuac, cuac!");
        }

        public void Nadar()
        {
            Console.WriteLine("Nadando");
        }

        public void Volar()
        {
            Console.WriteLine("Volando");
        }

        public abstract void Mostrar();
    }

Pero algo va mal

Resulta que en la aplicación también hay patos de juguete, y estos aparecen volando por la pantalla. ¡Vaya descuido!

Diagrama con pato de jugete

Claro, el PatoDeJuguete hereda de Pato y de todos los métodos en común.

Para solucionarlo podríamos sobreescribir el método Volar() en el PatoDeJuguete y forzarlo a que no haga nada. Por ejemplo así:

    public class PatoDeJuguete : Pato
    {
        public override void Volar()
        {
            Console.WriteLine("No puedo volar :( ");
        }

        public override void Mostrar()
        {
            Console.WriteLine("Soy un pato de juguete");
        }
    }

Algo parecido tendríamos que hacer con el método Graznar() porque el sonido del pato de juguete es diferente, más bien como una bocina.

    public class PatoDeJuguete : Pato
    {
        public override void Volar()
        {
            Console.WriteLine("No puedo volar :( ");
        }

        public override void Graznar()
        {
            Console.WriteLine("¡Meec, meec!");
        }

        public override void Mostrar()
        {
            Console.WriteLine("Soy un pato de juguete");
        }
    }

Bien, todo vuelve a estar en orden y nuestro diseño mantiene la filosofía inicial de la herencia para reutilizar el método. Pero ya intuimos que es fácil cometer descuidos si seguimos por el mismo camino. Cuando las aplicaciones evolucionan hacia sistemas más complejos la herencia suele traer más desventajas que ventajas. Por lo general, es mejor evitarla si queremos construir aplicaciones mantenibles a largo plazo.

De hecho, ¿qué pasa cuando la aplicación sigue evolucionando y seguimos utilizando la herencia?

Más patos que no vuelan

Pues sí, resulta que el PatoDecorativo tampoco vuela. Pero además tampoco grazna. Otro reto que pone a prueba el diseño inicial.

Siguiendo la filosofía utilizada hasta ahora podríamos sobreescribir los métodos Graznar() y Volar() en el PatoDecorativo para que no hagan nada.

   public class PatoDecorativo : Pato
   {
    public override void Volar()
    {
      Console.WriteLine("No puedo volar :( ");
     }
 
    public override void Graznar() 
    { 
       Console.WriteLine("No puedo grznar :( ");
    }


   public override void Mostrar()
   {
      Console.WriteLine("Soy un pato de juguete");
    }
   }

Pero entonces sí que estamos haciendo algo mal. Estamos violando el principio DRY (Don’t Repeat Yourself). El método Volar() en el PatoDecorativo es el mismo que el del PatoDeJuguete. Está duplicado. Si seguimos así tendremos que copy-pastear este método en cada uno de los patos que no vuelen.

Está claro que hay que buscar un diseño que se adapte mejor a nuestras necesidades.

¿Qué tal si lo solucionamos con interfaces?

Nos hemos dado cuenta que algunos patos pueden hacer ciertas cosas que otros no pueden. ¿Y si creamos interfaces IGraznar y IVolar y hacemos que sólo los patos que vuelen implementen IVolar, y sólo los que graznen implementen IGraznar?

Parece una buena idea, veamos cómo se resolvería:

    public class PatoDomestico : Pato, IVolar, IGraznar
    {
        public override void Mostrar()
        {
            Console.WriteLine("Soy un pato doméstico");
        }

        public virtual void Graznar()
        {
            Console.WriteLine("¡Cuac, cuac!");
        }
        public virtual void Volar()
        {
            Console.WriteLine("Volando");
        }

    }

    public class PatoPelirrojo : Pato, IVolar, IGraznar
    {
        public override void Mostrar()
        {
            Console.WriteLine("Soy un pato pelirrojo");
        }

        public virtual void Graznar()
        {
            Console.WriteLine("¡Cuac, cuac!");
        }
        public virtual void Volar()
        {
            Console.WriteLine("Volando");
        }

    }

    public class PatoDeJuguete : Pato, IGraznar
    {
        public void Graznar()
        {
            Console.WriteLine("¡Meec, meec!");
        }
        
        public override void Mostrar()
        {
            Console.WriteLine("Soy un pato de juguete");
        }
    }

    public class PatoDecorativo : Pato
    {
        public override void Mostrar()
        {
            Console.WriteLine("Soy un pato decorativo");
        }
    }

Si nos fijamos bien en las consecuencias de este diseño veremos que todavía lo hemos empeorado puesto que estamos duplicando el código para cada uno de los patos que sí que puede volar y graznar. He destacado los métodos de Graznar() y Volar() para que se vea claramente que están duplicados.

¿Qué pasaría si quisieramos realizar un pequeño cambio en la manera de volar de los patos? Habría que ir a los 48 tipos de patos que vuelan y modificar el método en cada uno de ellos. No parece demasiado mantenible.

La solución con interfaces parecía prometedora pero a la hora de la verdad en lugar de reutilizar código lo estamos duplicando, y cualquier cambio en una de las funciones puede convertirse en un infierno.

Tenemos que buscar otra solución más adecuada a nuestras necesidades.

Principio de programación

Para resolver el problema podemos hacer uso de uno de los principios básicos de la programación:

  • Identifica los aspectos que varían de tu aplicación y sepáralos de aquellos que permanecen igual.
  • Encapsula lo que varía para que no afecte al resto de tu código.

En nuestra aplicación de patos ¿qué es lo que varía? los métodos Graznar() y Volar() varían según el tipo de pato. Cojamos estas variaciones y encapsulémoslas en clases separadas.

Pero, ¿cómo diseñamos las nuevas clases para que implementen las funciones de Graznar() y Volar()?

  1. El objetivo es mantener las cosas flexibles.
  2. Poder asignar comportamientos concretos a las instancias de Pato
  3. Y poder cambiar dicho comportamiento dinámicamente

La solución con interfaces parecía buena, pero en lugar de que cada clase de pato implemente la interface, ¿por qué no creamos implementaciones propias y únicas, y éstas se las asignamos a la clase pato?

En lugar de que los tipos de patos implementen las interfaces creamos nuevas clases con significado propio:

    public class VolarConAlas : IVolar
    {
        public void Volar()
        {
            Console.WriteLine("Volando con alas");
        }
    }

    public class NoVolar : IVolar
    {
        public void Volar()
        {
            Console.WriteLine("No puedo volar");
        }
    }

Y para Graznar() podemos hacer lo mismo:

 public class GraznarComoUnPato : IGraznar
    {
        public void Graznar()
        {
            Console.WriteLine("¡Cuac, cuac!");
        }
    }

    public class GraznarComoUnaBocina : IGraznar
    {
        public void Graznar()
        {
            Console.WriteLine("¡Meec, meec!");
        }
    }

    public class GraznarEnSilencio : IGraznar
    {
        public void Graznar()
        {
            Console.WriteLine("");
        }
    }

Una vez hemos separado lo que varía toca integrarlo en la clase pato.

Solución con el patrón estrategia

    public abstract class Pato
    {
        private readonly IGraznar _estrategiaGraznar;
        private readonly IVolar _estrategiaVolar;

        protected Pato(IGraznar estrategiaGraznar, IVolar estrategiaVolar)
        {
            _estrategiaGraznar = estrategiaGraznar;
            _estrategiaVolar = estrategiaVolar;
        }

        public void Graznar()
        {
          _estrategiaGraznar.Graznar();
        }

        public void Volar()
        {
           _estrategiaVolar.Volar();
        }

        public void Nadar()
        {
            Console.WriteLine("Nadando");
        }


        public abstract void Mostrar();
    }

La clase base Pato recibe por el constructor la estrategia a utilizar para Graznar() y para Volar(). Al utilizar cualquiera de estos dos métodos se delega en el objeto estrategia para que realice el trabajo.

Sólo falta ver cómo se definen los subtipos de patos:

    public class PatoDomestico : Pato
    {
        public PatoDomestico() : 
            base(new GraznarComoUnPato(), new VolarConAlas())
        {
        }

        public override void Mostrar()
        {
            Console.WriteLine("Soy un pato doméstico");
        }
    }

    public class PatoDeJuguete : Pato
    {
        public PatoDeJuguete() : 
                base(new GraznarComoUnaBocina(), new NoVolar())
        {
        }


        public override void Mostrar()
        {
            Console.WriteLine("Soy un pato de juguete");
        }
    }

    public class PatoDecorativo : Pato
    {
        public PatoDecorativo() :
            base(new GraznarEnSilencio(), new NoVolar())
        {
        
        }


        public override void Mostrar()
        {
            Console.WriteLine("Soy un pato decorativo");
        }
    }

Cuando cada subtipo de pato llama al constructor base le pasa las estrategias adecuadas según sus características. Esta es la clave de todo, en lugar de heredar directamente, se le pasa la estrategia. En lugar de "ser" se utiliza "tener". Los patos tienen comportamientos (estrategias).

Definición del patrón estrategia

  • Define una familia de algoritmos,
  • Encapsula cada algoritmo, y
  • Hace que los algoritmos sean intercambiables dentro de esa familia.

La estrategia permite que el algoritmo varíe independientemente de los clientes que lo utilizan.

Se puede utilizar como sustituto de la herencia.

Participantes

Las clases y objetos que participan en este patrón son:

  • Estrategia (en el ejemplo IVolar o IGraznar eran estrategias)
    • Declara una interfaz común a todos los algoritmos soportados. El contexto (Pato) utiliza esta interfaz para llamar a la funcion que implementa una EstrategiConcreta (por ejemplo: VolarConAlas o GraznarEnSilencio).
       
  •  Estrategia concreta ( en el ejemplo VolarConAlas o GraznarEnSilencio)
    • Implementa el algoritmo usando la interfaz de Estrategia
       
  •  Contexto (la clase base Pato era el contexto)
    • Se configura con un objeto ConcreteStrategy, es decir, en el constructor se le pasa la estrategia que lo configura.
    • Mantiene una referencia a un objeto de estrategia.
    • Puede definir una interfaz que permita a la estrategia acceder a sus datos (los métodos Volar() o Graznar() son la interfaz a través de la cual se delega a la estrategia la ejecución)

Un ejemplo real

Para terminar voy a dar un ejemplo de una aplicación real donde utilizamos el patrón estrategia.

Este ejemplo se trata de un CRM real. El CRM tiene una tabla que se llama Peticiones. Esta tabla tiene numerosos campos y uno de ellos es el tipo. Con ello consiguen que en una misma tabla se guarden registros de diferentes conceptos: FormaDePago, PeticionDeVacaciones, OrdenDeCobro, etc. Todo en una misma tabla.

Según el valor del campo “tipo” el registro representa un concepto u otro.

El problema a resolver por el equipo de programación es validar que antes de guardar una petición en la tabla, dicha petición cumpla ciertas condiciones.

Solución inicial

Una única clase de validación donde dada una Petición se compruebe que los valores introducidos cumplen las condiciones:

    public class ValidadorPeticiones
    {
        public bool Validar(Peticion peticion)
        {
            switch (peticion.TipoPeticion)
            {
                   case TipoPeticion.FormaDePago:
                       return ValidarFormaDePago(peticion);

                    case TipoPeticion.OrdenDeCobro:
                       return ValidarOrdenDeCobro(peticion);
                    
                default:
                    return false;
            }
        }
        

        private bool ValidarOrdenDeCobro(Peticion peticion)
        {
            /**/
            return true;
        }

        private bool ValidarFormaDePago(Peticion peticion)
        {
            /**/
            return true;
        }

	/* Un método de validación por cada tipo de petición */
    }


En un proyecto pequeño esta aproximación puede servir perfectamente, pero en el CRM que he puesto como ejemplo tiene más de 50 tipos de peticiones y puede seguir creciendo, con lo cual tener una clase con tantos métodos es un signo de que algo “huele mal”.

Solución con patrón Estrategia

Crear una interfaz o estrategia de validación:

    public interface IEstrategiaValidacion
    {
        bool Validar(Peticion peticion);
    }

Inyectar esta estrategia en el constructor del validador y delegar en ella la validación:

    public class ValidadorPeticionesConPatronEstrategia
    {
        private readonly IEstrategiaValidacion _estrategiValidacion;

        public ValidadorPeticionesConPatronEstrategia(IEstrategiaValidacion estrategiValidacion)
        {
            _estrategiValidacion = estrategiValidacion;
        }

        public bool Validar(Peticion peticion)
        {
            return _estrategiValidacion.Validar(peticion);
        }
    }

Cada tipo de petición tendrá su propia estrategia de validación:


public class ValidadorFormaDePago : IEstrategiaValidacion
    {
        public bool Validar(Peticion peticion)
        {
            /*  */
	         ComprobarPropiedadTarjeta(peticion.Tarjeta);
	         ComprobarFechaCaducidad(petición.FechaCaducidad);
            return true;
        }
    }

    public class ValidadorOrdenDeCobro : IEstrategiaValidacion
    {
        public bool Validar(Peticion peticion)
        {
            /* Realizar las validaciones oportunas */
            return true;
        }
    }


Con este diseño la clase queda cerrada, es decir, no es necesario modificarla en el caso de que aparezcan nuevos tipos de Peticiones y haya que crear nuevas validaciones.

Conclusiones

  • El patrón estrategia es uno de los más utilizados en el mundo real.
  • En algunos casos puede sustituir a diseños basados en herencia.
  • En otros puede sustituir bloques if-else o switch y dejar la clase cerrada.
  • Es posible cambiar el comportamiento de una clase cambiándole la estrategia en tiempo de ejecución.

Fuente del ejemplo: "Head First Design Patterns"