Programación Avanzada

Patrones de diseño

Estrategia (Strategy)

Introducción

Un principio general en cualquier ámbito de la vida es: No reinventes la rueda.

Ante un nuevo problema, después de analizarlo, lo primero que nos debemos preguntar, antes de atacarlo, es: ¿Existe ya una solución para este problema?

De eso tratan los patrones de diseño, son soluciones bien conocidas a problemas recurrente en POO.

En este primer capítulo sobre patrones de diseño vamos a ver el patrón Estrategia (Strategy)

Bibliografía

Contenidos

  1. Voy a contarte un chiste.
  2. Programemos el chiste.
  3. Encapsulemos lo que varía.
  4. Definición de patrón de diseño Estrategia (Strategy).
  5. Ejemplos del paquete estándar.
  6. Recursos en Internet.

Voy a contarte un chiste

La empresa para la que trabajamos quiere contratar nuevo personal, y nos encargan que hagamos la selección técnica de los candidatos.

La prueba, extremadamente complicada, consiste en contar desde el 1 hasta el 10.

Voy a contarte un chiste

Hacemos pasar al primer candidato y le pedimos que cuente desde el 1 hasta el 10.

El candidato empieza a contar: diez, nueve, ocho, siete, seis, cinco, cuatro, tres, dos, uno.

Ante nuestra sorpresa, le preguntamos al candidato por qué cuenta de manera descendente.

Antes de cambiar de empleo trabajé como astronauta, y contábamos de esta manera en los lanzamientos.

Voy a contarte un chiste

Hacemos pasar al segundo candidato y, de nuevo, le pedimos que cuente desde el 1 hasta el 10.

El candidato empieza a contar: uno, tres, cinco, siete, nueve, dos, cuatro, seis, ocho y diez.

Sorprendidos de nuevo, le preguntamos al candidato por qué cuenta primero los pares y luego los impares.

Yo era cartero antes de cambiar de trabajo, y cuando repartía las cartas, primero recorría la acera de los impares y luego la de los pares.

Voy a contarte un chiste

Hacemos pasar al tercer y último candidato, y como tenemos la mosca detrás de la oreja, le preguntamos cual era su trabajo anterior.

Yo trabajaba como funcionario.

Confiamos en no tener ninguna sorpresa y le pedimos que cuente desde el 1 hasta el 10.

Uno, dos, tres, cuatro, cinco, seis, siete,...

sota, caballo y rey.

Programemos el chiste

Intentemos programar el chiste.

Lo primero que se nos ocurre es definir tres clases: Astronauta, Cartero y Funcionario:

public class Astronauta {
    private String nombre;
    
    public Astronauta(String nombre) {
        this.nombre = nombre;
    }
    
    public String cuenta() {
        return "Diez, nueve, ocho, siete, seis, 
            cinco, cuatro, tres, dos, uno.";
    }
}

Programemos el chiste

La clase para representar al Cartero:

public class Cartero {
    private String nombre;
    
    public Cartero(String nombre) {
        this.nombre = nombre;
    }
    
    public String cuenta() {
        return "Uno, tres, cinco, siete, nueve, 
            dos, cuatro, seis, ocho y diez.";
    }
}

Programemos el chiste

Finalmente, la clase para representar al Funcionario:

public class Funcionario {
    private String nombre;
    
    public Funcionario(String nombre) {
        this.nombre = nombre;
    }
    
    public String cuenta() {
        return "Uno, dos, tres, cuatro, cinco, 
            seis, siete, sota, caballo y rey.";
    }
}

Programemos el chiste

Para completar el ejemplo, definamos también el candidato ideal:

public class Ideal {
    private String nombre;
    
    public Ideal(String nombre) {
        this.nombre = nombre;
    }
    
    public String cuenta() {
        return "Uno, dos, tres, cuatro, cinco, 
            seis, siete, ocho, nueve y diez.";
    }
}

He omitido en todos los casos public String getNombre()

Programemos el chiste

Cosas que podemos mejorar en las clases, como siempre, intentamos eliminar código repetido:

  1. Todas tienen un método String cuenta().
  2. Todas tienen un atributo String nombre.

Una pregunta: ¿Recuerdas por qué el método cuenta devuelve un String y no lo muestra directamente por consola usando System.out.println(...)?

Una posible solución es definir un interface que declare el método común y hacer que Astronauta, Cartero, Funcionario e Ideal lo implementen.

Programemos el chiste

Otra posible solución es definir una superclase abstracta, que contenga el atributo y el método común declarado como abstract, y hacer que Astronauta, Cartero, Funcionario e Ideal extienda esa superclase, definiendo cada uno de ellos de modo distinto el método abstracto.

¿Alguna otra idea?

¿Qué te parece mejor?

¿Por qué?

Programemos el chiste

Empecemos con la opción del interface.

Llamémoslo Contador

public interface Contador {
    String cuenta();
    String getNombre(); // Añadido por comodidad
}

Programemos el chiste

La clase Astronauta quedaría como:

public class Astronauta implements Contador {
    private String nombre;
    
    public Astronauta(String nombre) {
        this.nombre = nombre;
    }
    @Override
    public String cuenta() {
        return "Diez, nueve, ocho, siete, seis, 
            cinco, cuatro, tres, dos, uno.";
    }
    @Override
    public String getNombre() {
        return nombre;
    }
}

Programemos el chiste

Juguemos un poco con las clases:

Contador contador = new Astronauta("Gagarin");
System.out.println("Soy " + contador.getNombre() + 
    ": " + contador.cuenta());
contador = new Cartero("Cartero de Pablo Neruda");
System.out.println("Soy " + contador.getNombre() + 
    ": " + contador.cuenta());
contador = new  Funcionario("Oscar");
System.out.println("Soy " + contador.getNombre() + 
    ": " + contador.cuenta());
contador = new Ideal("Galileo");
System.out.println("Soy " + contador.getNombre() + 
    ": " + contador.cuenta());

Programemos el chiste

Lo que obtenemos es:

Soy Gagarin: Diez, nueve, ocho, siete, seis, cinco, cuatro, tres, dos, uno.
Soy Cartero de Pablo Neruda: Uno, tres, cinco, siete, nueve, dos, cuatro, seis, ocho y diez.
Soy Oscar: Uno, dos, tres, cuatro, cinco, seis, siete, sota, caballo y rey.
Soy Galileo: Uno, dos, tres, cuatro, cinco, seis, siete, ocho, nueve y diez.

Programemos el chiste

Resumiendo:

  • Hemos promocionado el método común a un interface.
  • Todas las clases tienen el mismo atributo nombre.
  • Un Astronauta siempre contará como un Astronauta.

Veamos si con la clase abstracta mejoramos la implementación.

Programemos el chiste

Llamemos a la superclase Candidato:

public abstract class Candidato {
    private String nombre;
    
    public Candidato(String nombre) {
        this.nombre = nombre;
    }
    
    public String getNombre() {
        return nombre;
    }
    
    public abstract String cuenta();
}

Programemos el chiste

Ahora hagamos que Astronauta extienda a Candidato

public class Astronauta extends Candidato {
    public Astronauta(String nombre) {
        super(nombre);
    }
    
    @Override
    public String cuenta() {
        return "Diez, nueve, ocho, siete, seis, cinco, 
            cuatro, tres, dos, uno.";
    }
}

Hmmm, parece que esta clase es más sucinta. Quizás en este caso la clase abstract sea mejor opción.

Programemos el chiste

Juguemos un poco con estas clases:

Candidato candidato = new Astronauta("Gagarin");
System.out.println("Soy " + candidato.getNombre() + ": " + 
    candidato.cuenta());
candidato = new Cartero("Cartero de Pablo Neruda");
System.out.println("Soy " + candidato.getNombre() + ": " + 
    candidato.cuenta());
candidato = new Funcionario("Oscar");
System.out.println("Soy " + candidato.getNombre() + ": " + 
    candidato.cuenta());
candidato = new Ideal("Galileo");
System.out.println("Soy " + candidato.getNombre() + ": " + 
    candidato.cuenta());

Programemos el chiste

Lo que obtenemos es:

Soy Gagarin: Diez, nueve, ocho, siete, seis, cinco, cuatro, tres, dos, uno.
Soy Cartero de Pablo Neruda: Uno, tres, cinco, siete, nueve, dos, cuatro, seis, ocho y diez.
Soy Oscar: Uno, dos, tres, cuatro, cinco, seis, siete, sota, caballo y rey.
Soy Galileo: Uno, dos, tres, cuatro, cinco, seis, siete, ocho, nueve y diez.

Programemos el chiste

Pero, ¿cómo podemos entrenar a un candidato que no sea ideal para que cuente en el orden correcto?

Si tomamos a un Astronauta siempre contará en orden descendente, y si tomamos a un Cartero siempre contará primero los impares y luego los pares, no hay modo de cambiar el comportamiento del objeto una vez que lo hemos instanciado.

Hemos resuelto el problema, pero necesitamos más flexibilidad.

Encapsulemos lo que varía

Las clases Astronauta, Cartero, Funcionario e Ideal comparten un método común que hemos promocionado a un interface o a una clase abstract, pero cada una de ellas se encarga de dar un implementación diferente.

Eso está bien, pero el comportamiento está demasiado ligado a la definición de las clases concretas. No podemos hacer que un Astronauta cambie su forma de contar.

PRINCIPIO

Encapsula lo que varía.

En este caso lo que varía es el algoritmo para contar.

Encapsulemos lo que varía

¿Cómo encapsulamos un algoritmo en una clase?

Muy fácil, aquí lo tienes:

public class ContadorDescendente implements Contador {
    @Override
    public String cuenta() {
        return "Diez, nueve, ocho, siete, seis, 
            cinco, cuatro, tres, dos, uno.";
    }
}

Y el interfaz con el método común.

public interface Contador {
    String cuenta();
}

Encapsulemos lo que varía

Vaya!, ¿y como queda la clase Astronauta?

public class Astronauta {
    private String nombre;
    private Contador contador;
    
    public Astronauta(String nombre) {
        this.nombre = nombre;
        contador = new ContadorDescendente();
    }
    
    public String cuenta() {
        return contador.cuenta(); //Método delegado
    }...
}

Para, para!, pero ahora estás ligando en el constructor de Astronauta la implementación concreta de Contador.

Encapsulemos lo que varía

Vaya!, me he colado, perdón quería escribir esto:

public class Astronauta {
    private String nombre;
    private Contador contador;
    
    public Astronauta(String nombre, Contador contador) {
        this.nombre = nombre;
        this.contador = contador;
    }
    public void setContador(Contador contador) {
        this.contador = contador;
    }
    public String cuenta() {
        return contador.cuenta();
    } ...
}

Encapsulemos lo que varía

¿Hemos creado alguna instancia concreta en el código anterior?

¿Cómo jugamos con la clase?

Astronauta astronauta = new Astronauta("Gagarin", 
    new ContadorDescendente());
System.out.println("Soy " + astronauta.getNombre() + 
    ": " + astronauta.cuenta());
Soy Gagarin: Diez, nueve, ocho, siete, seis, cinco, cuatro, tres, dos, uno.

Guau!, funciona.

Encapsulemos lo que varía

¿Y cómo hacemos para que cuente como un Cartero?

Primero definimos una clase que encapsule la forma de contar como un Cartero.

public class ContadorImparesPares implements Contador {
    @Override
    public String cuenta() {
        return "Uno, tres, cinco, siete, nueve, 
            dos, cuatro, seis, ocho y diez.";
    }
}

Encapsulemos lo que varía

Y luego inyectamos el comportamiento en tiempo de ejecución.

Astronauta astronauta = new Astronauta("Gagarin", 
    new ContadorDescendente());
System.out.println("Soy " + astronauta.getNombre() + 
    ": " + astronauta.cuenta());

astronauta.setContador(new ContadorImparesPares());
System.out.println("Soy " + astronauta.getNombre() + 
    ": " + astronauta.cuenta());
Soy Gagarin: Diez, nueve, ocho, siete, seis, cinco, cuatro, tres, dos, uno.
Soy Gagarin: Uno, tres, cinco, siete, nueve, dos, cuatro, seis, ocho y diez.

Increíble, ahora el Astronauta cuenta como un Cartero.

Definición de patrón de diseño Estrategia (Strategy)

Define una familia de algoritmos, encapsula cada uno de ellos y los hace intercambiables. Permite que un algoritmo varíe independientemente de los clientes que lo usan.
Patrones de diseño
Erich Gamma et al.

Definición de patrón de diseño Estrategia (Strategy)

En nuestro ejemplo:

  • Familia de algoritmos → Modos de contar de 1 a 10.
  • Encapsulamiento → Cada una de las clases que cuentan.
  • Intercambiables → Referencia al interface Contador.

Definición de patrón de diseño Estrategia (Strategy)

Este sería el modelo UML general del patrón Estrategia:

Definición de patrón de diseño Estrategia (Strategy)

Y en el caso concreto de los algoritmo de contar:

Definición de patrón de diseño Estrategia (Strategy)

Ventajas:

  1. Permite manipular de modo transparente familias de algoritmos.
  2. Es una alternativa a la herencia.
  3. Evitan las sentencias switch.

Desventajas

  1. La decisión del algoritmo a elegir quizás deba basarse en su implementación.
  2. Mayor número de objetos en la solución.

Ejemplos del paquete estándar

En el capítulo de programación con genéricos vimos como utilizar el método Arrays.sort(Object[] a). La clase Arrays proporciona otro método para ordenar un array de elementos Arrays.sort(T[] a, Comparator< ? super T> c). El segundo argumento de este método debe ser una clase que implemente el interface Comparator que define dos métodos:

  • int compare(T o1, T o2);
  • boolean equals(Object obj);

El método interesante en este caso es el primero. Del segundo, Object da una implementación por defecto.

Ejemplos del paquete estándar

La clase Arrays está utilizando el patrón Strategy pues espera que le inyectemos el algoritmo de comparación que utilizará merge sort dentro de una clase que implemente Comparator.

Del diagrama UML general podemos concretar:

  • Algoritmo → Comparator
  • Algoritmo Concreto → Una clase que implemente Comparator
  • Cliente → La clase Arrays

Ejemplos del paquete estándar

Definamos entonces la clase que implementa Comparator:

class Comparador< T extends Color > implements Comparator< T > {
    @Override
    public int compare(T color1, T color2) {
        if(color1.getRGB() < color2.getRGB()) return -1;
        else if(color1.getRGB() > color2.getRGB()) return 1;
        else return 0;
    }
}

Ejemplos del paquete estándar

Probemos el algoritmo

Color colores2[] = {new Color(0, 0, 0),
        new Color(1, 1, 1),
        new Color(1, 0, 1)};
Arrays.sort(colores2, new Comparador());
System.out.println(Arrays.asList(colores));

Obtenemos como resultado:

[[0, 0, 0], [1, 0, 1], [1, 1, 1]]

Resumen

Los patrones de diseño hacen uso intensivo de los principios de la POO.

En particular, el patrón Strategy hace uso del principio Encapsula lo que varía. Lo que varía en este caso es un algoritmo, que puede tener varias implementaciones.

La manera de encapsular el altoritmo es, simplemente, crear una clase con su implementación.

Para que el patrón sea la más flexible posible, las clases clientes no instancian el algoritmo directamente, simplemente tienen una referencia a un interface bajo el cual se encuentra la clase que implementa un algoritmo concreto.

Recursos en Internet