The Strategy Pattern

Comments

The Strategy Pattern o el Patron Estrategia nos permite que el algoritmo varíe dependiendo de los clientes que lo utilizan.

The Strategy Pattern

Primeros pasos

Nos contratan para desarrollar una aplicación que gestione animales de un zoológico. Como sabemos, en un zoo existen diferentes tipos de animales. Por este motivo, inicialmente los diseñadores del sistema han creado una superclase llamada   Animal, la cual todos los tipos de animales heredan de ella.

/

Observaciones

  Todos los animales deben poder emitir un sonido, también deben poder comer, entre otras acciones comunes. Cada subtipo de animal, es responsable de implementar el método display() para determinar de que manera será visualizado en pantalla. El método display() es abstracto, todos los subtipos de animal, lucen de manera diferente.  

En el ultimo año, el gerente del zoológico ha querido comprobar que animales pueden volar y cuáles no. El problema aquí, radica en que no todos los animales tienen pueden volar, por ejemplo, un perro o un gato no vuela, pero un pajaro, si lo hace.

El primer diseño que se nos ocurre implementar, es el siguiente:

/

Perfecto, este diseño añade una función a la superclase Animal, llamada fly() y en los subtipos de animales que no puedan volar, se sobrescribe el método retornando un valor nulo.

Aquí existe varios inconvenientes. El código es duplicado en las subclases. Por otra parte, es difícil conocer los comportamientos de todos los animales. Además, cada vez que se decida añadir un nuevo tipo de animal, ya sea porque el zoológico va creciendo a lo largo del tiempo, se necesita sobrescribir los métodos en los diferentes tipos de ejemplares. Además, si la aplicación desea también comprobar que animales pueden nadar, debemos hacer exactamente lo mismo en cada uno de los animales que no lo pueden hacer, por lo que el código se hace difícil de mantener.

Separando los comportamientos variables de los comunes

¿Por dónde empezamos? Como hemos dicho, existen comportamientos que todos los animales tienen, por ejemplo, el de comer, emitir sonido, caminar, entre otros, y comportamientos que solo una clase de animales poseen, como nadar o volar.

Dicho esto, dejemos solo en la clase Animal, aquellos comportamientos que no deberían cambiar. Solo vamos a separar aquellas partes que cambian. Para ello, vamos a crear un conjunto de clases fuera de la clase Animal. Cada conjunto de clase, contendrá todas las implementaciones de sus respectivos comportamientos. Por ejemplo, nosotros podríamos tener una clase que implemente swinning, otra que implemente running, otra que implemente flying.

Como sabemos, las partes de la clase Animal que cambian, es fly(). Para separar este comportamiento, lo que haremos será extraer este método y crear un nuevo conjunto de clases para representar esta acción.

Diseñando los comportamientos de la clase Animal

Entonces, ¿cómo vamos a diseñar el conjunto de clases que implementan los comportamientos fly? Nos gustaría mantener las cosas flexibles; después de todo, fue la inflexibilidad en los comportamientos de los animales lo que nos metió en problemas en primer lugar. Y sabemos que queremos asignar comportamientos a las instancias de Animal. Por ejemplo, podríamos instanciar una nueva instancia de Butterfly e inicializarla con un tipo específico de comportamiento de vuelo. Y mientras estamos allí, ¿por qué no nos aseguramos de que podamos cambiar el comportamiento de un animal dinámicamente? En otras palabras, deberíamos incluir métodos de establecimiento de comportamiento en las clases de Animal para que podamos, por ejemplo, cambiar el comportamiento de vuelo de Butterfly o Mariposa en tiempo de ejecución. Dados estos objetivos, veamos nuestro segundo principio de diseño: utilizaremos una interfaz para representar cada comportamiento, por ejemplo, FlyBehavior, y cada implementación de un comportamiento implementará una de esas interfaces. Haremos un conjunto de clases cuya razón de existir, sea representar un comportamiento (por ejemplo, "flying"), y es la clase del comportamiento, en lugar de la clase Animal, la que implementará la interfaz del comportamiento. Esto está en contraste con la forma en que hacíamos las cosas antes, donde un comportamiento provenía de una implementación concreta en la superclase Animal, o al proporcionar una implementación especializada en la subclase misma. En ambos casos confiamos en una implementación. Estábamos atrapados en el uso de esa implementación específica y no había lugar para cambiar el comportamiento (que no sea escribir más código). Con nuestro nuevo diseño, las subclases de Animal usarán un comportamiento representado por una interfaz (FlyBehavior), de modo que la implementación real del comportamiento (en otras palabras, el comportamiento concreto específico codificado en la clase que implementa FlyBehavior) no estará bloqueado en la subclase de Animal.

/

La clase IfFlys que implementa la interfaz FlyBehavior será usada por todos los animales que puedan volar, en cambio, la clase CantFly, será usada por los animales que no puedan volar.

Con este diseño, otros tipos de objetos pueden reutilizar nuestros comportamientos de vuelo porque éstos ya no están ocultos en nuestra clase Animal.

También podemos agregar nuevos comportamientos sin modificar ninguna de sus clases de comportamiento existentes ni tocar ninguna de las clases de Animal que usan comportamientos de vuelo.

Integrando la interfaz FlyBehavior

La clave es que un animal ahora delegará su comportamiento de vuelo, en lugar de usar métodos de vuelo definidos en la clase (o subclase) de animal.

Primero agregaremos un atributo a la clase Animal llamadaflyBehavior que está declarada como interfaz). Cada objeto de animal configurará esta variable polimórficamente para hacer referencia al tipo de comportamiento específico que desearía en el tiempo de ejecución. También eliminaremos el método fly() de la clase Animal (y de cualquier subclase) porque hemos trasladado este comportamiento a la clase FlyBehavior. Reemplazaremos fly() en la clase Animal con un método similar, llamado tryToFly(); Verás cómo funcionan a continuación.

/

Ahora implementamos el método tryToFly()

        public class Animal {

            FlyBehavior flyBehavior

            public void tryToFly()
            {
                flyBehavior.fly();
            }
       }

Bastante simple. Ahora como vemos, un animal solo permite que el objeto al que es referenciado flyBehavior pueda comprobar si este vuela o no. En esta parte del código no nos importa qué tipo de objeto es, lo único que nos importa es que sepa cómo vuela, si es que lo hace. De acuerdo, es hora de aprender a utilizar las variables de instancia flyBehavior. Echemos un vistazo a la clase Bird

public class Bird extends Animal {

        public Bird()
        {
            flyBehavior = new ItsFlys();
        }

        @Override
        public void display() {
            System.out.println("I'm a Bird!!");
        }

        public void makeSound()
        {
            System.out.println("I'm a Bird,
                I can make a cuack!");
        }
}

Como observamos, en el constructor de la clase Bird, asignamos una nueva instancia de la clase ItsFly y la guardamos en el atributo flyBehavior. Esto quiere decir que, un objeto Bird tiene la capacidad de poder volar, pero si se tratase de un Dog, la instancia asignada en el atributo flyBehavior sería CantFly().

Testeando nuestra primera aplicación

Vamos a implementar la clase Animal y sus respectivos tipos de animales, (Bird y Dog), también crearemos la interfaz FlyBehavior y sus conjuntos de clases que implementan esta interfaz.

public abstract class Animal {

            FlyBehavior flyBehavior;

            public Animal(){

            }

            public abstract void display();

            public void makeSound()
            {
                System.out.println("I'm a Animal, I can make a sound");
            }

            public void eat()
            {
                System.out.println("I'm a Animal, I can eat a food");
            }

            public void tryToFly()
            {
                flyBehavior.fly();
            }

        }
public class Bird extends Animal {

            public Bird()
            {
                flyBehavior = new ItsFlys();
            }

            @Override
            public void display() {
                System.out.println("I'm a Bird!!");
            }

            public void makeSound()
            {
                System.out.println("I'm a Bird, I can make a cuack!");
            }

}
public class Dog extends Animal {

            public Dog()
            {
                flyBehavior = new CantFly();
            }

            @Override
            public void display() {
                System.out.println("I'm a Dog!!");
            }

            public void makeSound()
            {
                System.out.println("I'm a Dog, I can make a guau!");
            }

        }
public interface FlyBehavior {

        public void fly();
    }
public class ItsFlys implements FlyBehavior {

        @Override
        public void fly() {
            System.out.println("I can fly!");
        }

    }
public class CantFly implements FlyBehavior {

        @Override
        public void fly() {
            System.out.println("I can't fly...");
        }

    }

Ahora vamos a crear un mini test para verificar la funcionalidad de las mismas.

public class ZooTest {
            public static void main(String[] args)
            {
                Animal tweety = new Bird();

                tweety.tryToFly();

                tweety.display();

                Animal snoopy = new Dog();

                snoopy.tryToFly();

                snoopy.display();

            }
        }

Perfecto, ejecutamos dicho test y veremos en la consola de nuestro IDE el siguiente resultado:

/

Añadiendo comportamientos dinámicamente

Ahora podemos crear en la clase Animal, funciones miembros para cambiar dinámicamente dichos comportamientos. Es decir, en tiempo de ejecución, podemos decirle a un objeto Bird que no puede volar.

public void setFlyingAbility(FlyBehavior newFlyBehavior){
        flyBehavior = newFlyBehavior;
    }

Modificamos nuestro test añadiendo las siguientes líneas:

...
 tweety.setFlyingAbility(new CantFly());
        tweety.display();
        tweety.tryToFly();
...

Ejecutamos nuevamente el programa.

/

Hasta aquí, hemos visto como este patrón, define una familia de algoritmos, encapsula cada uno y los hace intercambiables.

The Strategy Pattern permite que el algoritmo varíe dependiendo de los clientes que lo utilizan.

Ejemplo real

Para ver nuestro ejemplo real de este patrón aplicado a la tienda online, visite el articulo Implementando el Patron Strategy en una nuestra Tienda Online