Saltar al contenido

Patrones de comportamiento

Los patrones que organizan cómo colaboran los objetos en ejecución: Chain of Responsibility, Command, Strategy, Iterator, State, Memento, Observer y Template Method.

Chain of Responsibility y Command

En la cadena de responsabilidad la petición recorre una serie de eslabones. Cada eslabón tiene una tarea específica y, cuando termina lo suyo, le pasa el control al siguiente. Quien lanza la petición no sabe cuántos eslabones hay ni en qué orden están, así que meter uno nuevo o quitarlo no le afecta.

El ejemplo más reconocible es la cadena de filtros de una aplicación web: uno comprueba la sesión, otro registra la petición, otro mide el tiempo, y al final llega al servlet que hace el trabajo.

El Command parte de una idea distinta: convertir los métodos en clases. En lugar de que una clase tenga diez métodos, cada acción pasa a ser un objeto con una única operación de ejecución. La ganancia aparece cuando hay que ampliar el comportamiento: añadir una acción nueva es añadir una clase, no modificar la que ya existía.

El Command es la forma canónica de cumplir el principio de abierto y cerrado con las acciones de un sistema, y además permite guardarlas, encolarlas o deshacerlas, porque una acción convertida en objeto se puede manipular como cualquier otro dato.

Para el examen

  • Chain of Responsibility: la petición recorre eslabones que hacen lo suyo y pasan el control

  • Su ejemplo: los filtros web

  • Command: convierte los métodos en clases

  • Qué permite: una acción hecha objeto se guarda, se encola o se deshace

Strategy

El Strategy se parece al Command, pero está orientado a un algoritmo concreto. Se trata de separar en clases distintas las implementaciones alternativas de un mismo método, para poder elegir una u otra sin tocar a quien las usa.

El montaje es siempre el mismo: una interfaz que declara la operación, una clase por cada algoritmo, y un cliente que recibe la estrategia desde fuera y se limita a invocarla. Ordenar por burbuja, por mezcla o por partición son tres estrategias del mismo método de ordenación.

La idea es la misma que sostiene un sistema de ficheros virtual: hay unas operaciones fijas de abrir, leer y cerrar, y por debajo cada sistema de ficheros concreto las resuelve a su manera sin que el que las llama tenga que enterarse.

java
public interface SeleccionDePreguntas {
    List<Pregunta> elegir(Alumno alumno, int cuantas);
}

public class PorFalladas implements SeleccionDePreguntas { /* ... */ }
public class PorMenosVistas implements SeleccionDePreguntas { /* ... */ }
public class AlAzar implements SeleccionDePreguntas { /* ... */ }

public class GeneradorDeRepaso {
    private final SeleccionDePreguntas estrategia;   // se recibe de fuera

    public List<Pregunta> repasoDeHoy(Alumno alumno) {
        return estrategia.elegir(alumno, 20);
    }
}

Command convierte en clases las acciones de un sistema; Strategy convierte en clases las formas alternativas de hacer una misma cosa. Si las clases resultantes son intercambiables entre sí, es Strategy.

Para el examen

  • Qué hace: cada algoritmo alternativo en su clase, con una interfaz común

  • De dónde viene la estrategia: el cliente la recibe desde fuera

  • Señal para reconocerlo: las clases son intercambiables

Iterator

El Iterator permite recorrer una colección de objetos sin conocer de qué tipo de colección se trata. Aísla al cliente de lo que hay por debajo: este solo pide el siguiente elemento y no sabe si detrás hay un árbol, una pila o una lista.

Para conseguirlo se dispone de clases especializadas en recorrer cada tipo de colección, y todas ellas ofrecen la misma interfaz de recorrido. Cambiar la estructura de datos por dentro no obliga a tocar el código que la recorre.

El valor del Iterator es que separa dos responsabilidades que suelen ir pegadas: guardar los elementos y recorrerlos. La colección se ocupa de lo primero y el iterador de lo segundo.

Para el examen

  • Qué permite: recorrer una colección sin saber qué estructura hay debajo

  • Qué separa: guardar de recorrer

State

El State convierte los estados en clases. Un objeto que se comporta de forma distinta según en qué situación se encuentre deja de resolverlo con condicionales y pasa a delegar en un objeto estado, que es el que sabe qué hacer.

El objeto principal guarda una referencia a su estado actual y, cuando recibe una petición, se la pasa al estado. Cada clase de estado implementa la operación a su manera y decide, además, a qué estado hay que transitar después.

El síntoma que pide un State es el condicional gigante que pregunta en qué situación está el objeto antes de cada operación, repetido en varios métodos. Ese bloque desaparece al repartir la lógica entre las clases de estado.

Para el examen

  • Qué hace: convierte los estados en clases

  • Cómo funciona: el objeto delega en su estado actual y cada estado decide la transición

  • Qué elimina: el condicional gigante

Memento y Observer

El Memento crea clases específicas cuyo único cometido es guardar la información necesaria para poder deshacer las acciones ya ejecutadas. Es el patrón que hay detrás de cualquier función de deshacer, la de toda la vida asociada a la combinación de teclas de control y zeta.

El detalle importante es que el recuerdo guarda el estado sin romper la encapsulación del objeto: nadie de fuera puede hurgar en él, solo el objeto que lo creó sabe interpretarlo para volver atrás.

El Observer aporta la infraestructura de avisos: cuando una parte del sistema cambia, se entera quien esté suscrito. Hay un sujeto observado, que es el recurso cambiante, y unos observadores suscritos a él. Cuando el sujeto cambia, notifica a todos los suscritos sin conocerlos de antemano.

Funciona como una suscripción entre productores y consumidores, muy parecido a un tema de mensajería. La regla de diseño que enuncia es esta: cuando una parte del sistema dependa de otra, en lugar de que se llamen directamente conviene montar en medio la infraestructura que permita que unas se suscriban a los cambios de las otras.

Memento y Observer se ocupan los dos del estado, pero en direcciones opuestas: el Memento lo guarda para poder volver atrás, y el Observer avisa a terceros de que ha cambiado.

Para el examen

  • Memento: guarda el estado para deshacer, sin romper la encapsulación

  • Su ejemplo: el control-Z

  • Observer: un sujeto notifica sus cambios a los observadores suscritos

  • Su gracia: el sujeto no conoce a los observadores

Template Method

El método plantilla define un flujo de ejecución de un proceso, pero no del todo: solo parcialmente. Se fija el esqueleto, es decir, qué pasos hay y en qué orden, y se dejan huecos para que cada caso concreto rellene los pasos que le son propios.

Es la técnica en la que se apoya la idea de marco de trabajo. Un framework es un trozo de código que da ya resueltas ciertas cosas: el equipo de arquitectura escribe clases con los procesos definidos a medias y el resto de unidades de negocio parten todas de la misma base, cada una completando lo suyo.

Template Method y Strategy resuelven lo mismo por caminos contrarios. El Template Method varía pasos sueltos dentro de un flujo fijo y lo hace por herencia; el Strategy cambia el algoritmo entero y lo hace por composición.

Para el examen

  • Qué fija: el esqueleto del proceso en una clase abstracta

  • Qué dejan las subclases: rellenar los huecos

  • Dónde se usa: es la base de los frameworks

  • Frente a Strategy: Template Method varía pasos por herencia; Strategy cambia el algoritmo entero por composición

Command, State y Template Method: cómo se implementan

En el Command, el punto de partida es una clase con muchos métodos, del tipo imprimir, guardar y buscar. La transformación consiste en sacar cada método a su propia clase, todas con una única operación de ejecución declarada en una interfaz común. A partir de ahí, añadir una exportación nueva es escribir una clase más y nada más.

El cliente deja de llamar a métodos concretos y pasa a manejar objetos de la interfaz común, lo que permite guardarlos en una lista, encolarlos o rehacerlos.

java
public interface Accion {
    void ejecutar();
}

public class ExportarIntentos implements Accion { public void ejecutar() { } }
public class ReabrirTema implements Accion { public void ejecutar() { } }

// El cliente solo conoce la interfaz: las acciones se acumulan y se lanzan.
public class Historial {
    private final Deque<Accion> pendientes = new ArrayDeque<>();
    public void lanzarTodas() {
        while (!pendientes.isEmpty()) pendientes.pop().ejecutar();
    }
}

En el State, el objeto arranca con un estado inicial y cada petición se resuelve invocando la operación del estado actual. Dentro de cada clase de estado vive la transición: ella misma decide cuál será el siguiente. Lo que desaparece es el condicional enorme que había en el método principal, con toda la lógica de negocio de cada situación amontonada y muy difícil de mantener. Esa lógica se reparte y cada trozo acaba en la clase de estado que le corresponde.

java
public class Intento {
    private EstadoIntento estado;

    public Intento() {
        this.estado = new EnCurso();      // estado inicial
    }

    public void avanzar() {
        this.estado = estado.ejecutar(this);   // el estado decide y transita
    }
}

public interface EstadoIntento {
    EstadoIntento ejecutar(Intento intento);
}

public class EnCurso implements EstadoIntento { /* ... */ }
public class Corregido implements EstadoIntento { /* ... */ }

En el Template Method hay dos niveles. En el nivel del marco de trabajo se escribe una clase abstracta con el método plantilla, que ya define el esqueleto y el flujo aunque los pasos que invoca todavía no estén implementados. En el nivel de la aplicación, cada negocio hereda de esa clase y rellena los pasos que le tocan.

java
public abstract class CorreccionDeTest {

    // Método plantilla: fija el flujo y no se puede cambiar.
    public final Resultado corregir(Intento intento) {
        validar(intento);
        int aciertos = contarAciertos(intento);   // lo pone cada negocio
        return puntuar(aciertos, intento);        // lo pone cada negocio
    }

    private void validar(Intento intento) { /* igual para todos */ }

    protected abstract int contarAciertos(Intento intento);
    protected abstract Resultado puntuar(int aciertos, Intento intento);
}

El método plantilla suele declararse final a propósito: lo que se hereda es el flujo, y permitir que una subclase lo reescriba destruiría la garantía que da el patrón.

Para el examen

  • En el State: cada clase de estado resuelve la operación y decide el siguiente estado

  • En el Template Method: el método plantilla se declara final

  • Para qué: para que ninguna subclase reescriba el flujo