Qué es el patron de diseño Strategy

Qué es el patron de diseño Strategy

Siguiendo nuestro curso de programación hoy veremos el patrón Strategy. El patrón de comportamiento Strategy define una familia de algoritmos, encapsula cada uno y los hace intercambiables. Permite que el algoritmo varíe de forma independiente a los clientes que lo usan.

fina_strategy

Aplicaciones

Se debe utilizar el patrón Strategy cuando:

  • Se quiera configurar una clase con un comportamiento determinado de entre varios.
  • Se necesitan distintas variaciones de un algoritmo.
  • Los distintos comportamientos de una clase aparecen como múltiples sentencias condicionales. El patrón Strategy permite mover cada rama de esos condicionales anidados a su propia clase.

Ejemplo

Supongamos que tenemos un procesador de textos que puede incorporar distintos algoritmos de separación de un texto en líneas. Tendríamos tres posibles opciones:

  • Con una estrategia línea a línea.
  • Con el algoritmo de TeX.
  • Con un número fijo de elementos en cada línea.

strategy1

Estructura

A tendiendo al ejemplo anterior podemos sacar una estructura general:

strategy2

Tenemos que:

  • Strategy (Compositor)
  • ConcreteStrategy (SimpleCompositor, TeXCompositor, ArrayCompositor)
  • Context (Composition).

Colaboraciones

  • La Estrategia y el Contexto colaboran para implementar el algoritmo escogido: el contexto puede pasar todos los datos que necesita al llamar a la estrategia concreta o se puede pasar a sí mismo como referencia para que aquélla llame a los métodos que necesite.
  • Los clientes colaboran con el contexto: pueden pasarla la estrategia concreta o no.

Beneficios

  • Define familias de algoritmos relacionados.
  • Es una alternativa a la herencia. - El contexto sea más fácil de entender, modificar y mantener. - Evita la duplicación de código y la explosión de subclases. - Se pueden cambiar dinámicamente.
  • Elimina las múltiples sentencias condicionales. Estas son muchas veces son el indicador de que necesitamos aplicar un patrón.

Problemas

  • Puede complicar la comunicación entre el contexto y las estrategias.
  • Crece el número de objetos.

Ejemplo

Vamos utilizar la idea que

Supongamos que tenemos una clase Main encargada de crear una formulario con una serie de campos. Estos campos serán muy dispares, y tendrán, dependiendo de que campo sea unas comprobaciones que pueden ser totalmente distintas o muy parecidas entre unos y otros.

public class Main {public static void main(String[] args) {    Formulario formulario = new Formulario();    formulario.addCampo(new CampoTexto("Nombre"));    formulario.addCampo(new CampoTexto("Apellido"));    formulario.addCampo(new CampoNumero("Telefono"));    formulario.addCampo(new CampoPredefinido("Ciudad", "Santander", "Oviedo", "Cadiz"));    formulario.PideDatos();}}

Tendremos un diagrama de clases similar a este:

inicial_strategy

Y las clases tendrán un código similar a este:

public interface Campo {public void pideDato();public String getString();}

Las clases que implementan la interfaz Campo tendrán un aspecto similar a esto:

public class CampoNumero implements Campo {private String etiqueta;private String texto;public CampoNumero(String etiqueta) {    this.etiqueta = etiqueta;}public void pideDato() {    BufferedReader consola = new BufferedReader(new InputStreamReader(System.in));    boolean valido;    do {        valido = true;        try {            System.out.print(etiqueta + ": ");            texto = consola.readLine();            for (char ch : texto.toCharArray()) {                if (!Character.isDigit(ch)) {                    valido = false;                    break;                }            }        } catch (IOException ex) {            System.out.println(ex);        }    } while (!valido);}public String getString() {    return texto;}}

Y nuestro formulario será:

public class Formulario {private List<Campo> campos = new ArrayList<Campo>();public void addCampo(Campo campo) {    campos.add(campo);}public void PideDatos() {    for (Campo campo : campos) {        campo.pideDato();        System.out.println(campo.getString());    }}}

Lo primero que deberíamos hacer es aplicar los principios SOLID a nuestras clases, es realmente inútil e ineficiente tener el método pideDato() en todas las clases que implementan la interfaz, ¿no seria más lógico que este método estuviese en una clase Campo de la que derivasen todos los demás campos? También es importante programar no solo para nosotros o para un posible cliente, si no también para otros programadores por ello vamos a refactorizar el código pero en inglés, eso si, manteniendo los mensajes al usuario en español, pues el cliente que suponemos que utilizará esta aplicación será español.

Tenemos que nuestra interfaz Campo pasa a ser una clase llamada Field desde la cual los demás campos extenderán su funcionalidad.

public class Field {public static final Validator TEXT = new TextValidator();public static final Validator NUMBER = new NumberValidator();private String label;private String value;private Validator validator;public Field(String label, Validator validator){    if (label == null || label.trim().isEmpty())        throw new IllegalArgumentException("Se necesita la etiqueta para este campo de formulario");    if (validator == null)        throw new IllegalArgumentException("¿Qué tipo de campo es? Se necesita un validador no nulo");    this.label = label;    this.validator = validator;}public void askUser(){    BufferedReader input = new BufferedReader(new InputStreamReader(System.in));    while (true) {        try {            System.out.print(label + ": ");            String value = input.readLine();            if (validator.isValid(value)) {                this.value = value;                return;            }            System.out.println("El valor introducido para "" + label + "" no es válido: " + value + " (" + validator.getMessage() + ")");        } catch (IOException e) {            System.out.println("No se pudo leer el valor del campo: " + e);        }        System.out.println("Inténtelo de nuevo...");    }}String getValue(){    return value;}@Overridepublic String toString(){    return label + ": " + value;}}

Las demás clases también deberán ser refactorizadas. Nuestra clase formulario pasará a llamarse Form.

public class Form {private List<Field> fields = new ArrayList<Field>();public void addField(Field field) {    fields.add(field);}public void askUser() {    for (Field field : fields)    {        field.askUser();        System.out.println(field.getValue());    }}}

Si nos fijamos por ahora no tendríamos porque introducir el patrón, por ahora no tenemos ningún problema, pero si por ejemplo tuviésemos dos nuevos campos como: edad o código postal en los cuales fuese necesario comprobar que la longitud de las palabras o que siguiesen una o varias estructuras determinadas. Seria bastante costoso tener que realizar una clase para cada una de estas subclases y quizás también un coñazo tener que modificar en todas las clases una pequeña modificación. Tenemos que aplicar el patrón Strategy.

Nuestra idea es tener una clase Main que siga creando los distintos campos que tendrá nuestro formulario como por ejemplo:

public class Main {public static void main(String[] args){    Form form = new Form();    form.addField(new Field("Nombre", Field.TEXT));    form.addField(new Field("Apellidos", Field.TEXT));    form.addField(new Field("Teléfono", Field.NUMBER));    Validator cities = new PredefinedValidator("Santander", "Oviedo", "Cádiz");    form.addField(new Field("Ciudad", cities));    // Ampliación    form.addField(new Field("Código de producto", new LengthValidator(4)));    Validator postalCode = new AndValidator(Field.NUMBER, new LengthValidator(5));    form.addField(new Field("Código postal", postalCode));    form.addField(new Field("Edad", new GreaterThanValidator(18)));    form.addField(new Field("Sueldo", new AndValidator(new GreaterThanValidator(800), new LessThanValidator(1200))));    form.addField(new Field("Ubicación", new OrValidator(cities, postalCode)));    form.addField(new Field("Código de promoción", new OrValidator(Field.TEXT, new AndValidator(Field.NUMBER, new LengthValidator(3)))));    form.askUser();}}

Será necesario añadir una interfaz Validator la cual deberá ser implementada por todas las posibles validaciones que tengan nuestros campos. La idea ahora no es tener una campo texto, un campo número... si no tener un campo predefinido que permita recibir validaciones como vemos en la clase Main.

Nos quedará un diagrama de clase similar al siguiente:

fina_strategy

Y un código como este:

public interface Validator {boolean isValid(String value);String getMessage();}public class TextValidator implements Validator{@Overridepublic boolean isValid(String value) {    for (char ch : value.toCharArray()) {        if (!(Character.isLetter(ch) || Character.isWhitespace(ch)))            return false;    }    return true;}@Overridepublic String getMessage() {    return "Se necesita un valor de texto";}   }

Para finalizar vamos a ver las diferencias básicas entre este patrón y el State. Aunque el diagrama de clases pueda llegar a ser el mismo la intención de los dos patrones es diferente. En ambos caso se pretende encapsular el comportamiento e independizarlo del objeto, el State se utiliza para cuando el comportamiento de un objeto depende de que en qué estado este (estos estados serán las subclases), mientras que el Strategy se utiliza cuando dos subclases tienen comportamientos diferentes (cada una de las subclases redefinirán el comportamiento).

  • State permite hacer diferentes cosas dependiendo del estado del objeto. Estado será una nueva subclase que el programador deberá añadir. En el videoclub dependiendo de en qué estado este la película se hará una cosa u otra,
  • Strategy permite hacer lo mismo de diferentes maneras. Cada manera de realizar eso definirá en una clase que cambiará como lo hace. En el formulario dependiendo que formulario sea se hace una comprobación del texto de diferente manera.

Como siempre os aquí dejo el código final.

Para ti
Queremos saber tu opinión. ¡Comenta!