Patrones de Diseño II

Command

Todo comienza con
una simple función:

public double totalizar() {
   double total = 0.0;
   List<Factura> facturas = servicio.obtenerFacturas();
   for (Factura f : facturas) {
      total += f.valor();
   }
   return total;
}

Pero ahora necesitamos
un cambio, sólo necesitamos las
facturas cuyo
contratante sea Google

public double totalizar() {
   double total = 0.0;
   List<Factura> facturas = servicio.obtenerFacturas();
   for (Factura f : facturas) {
      if (f.getContratante().equals("Google") {
         total += f.valor();
      }
   }
   return total;
}

¡Ah pero ahora solo necesitamos las facturas
de los 2 últimos meses!

public double totalizar() {
   double total = 0.0;
   List<Factura> facturas = servicio.obtenerFacturas();
   YearMonth ahora = YearMonth.now();
   YearMonth hace2Meses = ahora.minusMonths(2);
   for (Factura f : facturas) {
      if (f.getContratante().equals("Google") && f.esDespués(hace2Meses)) {
         total += f.valor();
      }
   }
   return total;
}

Sabemos que los cambios van a seguir llegando,
así que no podemos
seguir esta ruta

Command

Permite encapsular una acción para poder ejecutarla después

public interface Predicate<T> {
   
   boolean test(T t);
}
public double totalizar(final Predicate<Function> predicado) {
  double total = 0.0;
  List<Factura> facturas = servicio.obtenerFacturas();
  for (Factura f : facturas) {
    if (predicado.test(f)) {
      total += f.valor();
    }
  }
  return total;  
}

Antes de Java 8 debíamos usar clases anónimas para escenarios como este

double total = totalizar(new Predicate<Factura>() {
   @Override
   public boolean test(Factura factura) {
      return factura.getContratante().equals("Google");
   }
});
public static final Predicate<Factura> ES_GOOGLE = 
    new Predicate<Factura>() {
      @Override
      public boolean test(Factura factura) {
        return factura.getContratante().equals("Google");
      }
    };

* * *

double total = totalizar(ES_GOOGLE);

Esta idea de parametrizar comportamiento es muy común en Java

Interfaces Funcionales

  • Comparable, Comparator
  • Runnable
  • ActionListener
  • etc

Command

También podemos evitar repetición, con la idea de encapsular una acción

public interface Command {

   public void run() throws DrawException;
}
public abstract class DrawingCommand
      implements Command {

   private Canvas canvas;

   public DrawingCommand(final Canvas canvas) {
      this.canvas = canvas;
   }
}
public class DrawLine
      extends DrawingCommand {

   private Point p1;
   private Point p2;

   public DrawLine(Canvas canvas, Point p1, Point p2) {
      super(canvas);
      this.p1 = p1;
      this.p2 = p2;
   }

   public void run() throws DrawException {
     // dibuja una línea en el canvas
   }
}
public class RectangleLine
      extends DrawingCommand {

   private Point p1;
   private Point p2;

   public DrawLine(Canvas canvas, Point p1, Point p2) {
      super(canvas);
      this.p1 = p1;
      this.p2 = p2;
   }

   public void run() throws DrawException {
     // dibuja un rectángulo
   }
}
public abstract class BucketFill
      extends DrawingCommand {

   private Point p1;
   private Color color;

   public DrawLine(Canvas canvas, Point p1, Color color) {
      super(canvas);
      this.p1 = p1;
      this.color = color;
   }

   public void run() throws DrawException {
     // usa la cubeta
   }
}
public class DrawingApp {

   public void execute(Command command) {
      try {
         command.run();
      } catch (DrawException e) {
         // maneja de una manera consistente
         // los errors de los comandos
      }
   }
}

También podemos implementar una funcionalidad de "deshacer"

public interface Command {

   void run() throws DrawException;
   void undo();
}
public abstract class BucketFill
      extends DrawingCommand {

   private Point p1;
   private Color before;
   private Color color;

   public BucketFill(Canvas canvas, Point p1, Color color) {
      super(canvas);
      this.before = canvas.getColor(p1);
      this.p1 = p1;
      this.color = color;
   }

   public void run() throws DrawException {
     run(p1, color);
   }

   private void run(Point p1, Color color) {
      // pinta a p1 con el color dado
      // se llama recursivamente para pintar
      // los vecinos
   }

   public void undo() {
      run(p1, before);
   }
}
public class DrawingApp {

   private Deque<Command> cola = new ArrayDeque<>();

   public void execute(Command command) {
      try {
         cola.add(command);
         command.run();
      } catch (DrawException e) {
         // maneja de una manera consistente
         // los errors de los comandos
      }
   }

   public void undo() {
      Command último = cola.pop();
      último.undo();
   }
}

Command

La flexibilidad viene porque la acción se puede ejecutar controladamente

Command

  • Parametriza comportamiento
  • Permite evitar repetición
  • Permite crear acciones
    de "deshacer"
  • Permite ejecutar las acciones
    de una manera lazy

Command

  • Solo se necesita un método
    dentro de una clase
  • Si utilizas esa clase como
    parámetro, ya puedes
    usarlo como un Command

Observer

El desacoplamiento nos permite cambiar una clase sin tener que cambiar otras

Especialmente importante con una relación
de 1 a muchos

Tenemos una casa que tiene un sistema de alarma

Diferentes componentes
de la casa, reaccionan si
la alamar suena: las luces, se prenden, las puertas
se aseguran, etc.

Como siempre, empezamos con
una interface

public interface AlarmaListener {
   void actuar();
}

La Interface establece que una clase está interesa en conocer que la alarma sonó

Ahora necesitamos "observadores"

public class Luces implements AlarmaListener {
  public void actuar() {
    System.out.println("encender luces");
  }
}

* * *

public class Puertas implements AlarmaListener {
  public void actuar() {
    System.out.println("puertas cerradas");
  }
}

¡Ahora necesitamos
definir el observado!

public class SistemaCentral {
  private List<AlarmaListener> listeners = 
      new ArrayList<>();

  public void registrar(AlarmListener listener) {
    listeners.add(listener);
  }

  public void activarAlarma() {
    for (AlarmaListener listener : listeners) {
      listener.actuar();
    }
  }
}

¡Juntemos las piezas!

public static void main(String[] args) {
   Luces luces = new Luces();
   Puertas puertas = new Puertas();
   SistemaCentral sistema = new SistemaCentral();
   sistema.registrar(luces);
   sistema.registrar(puertas);
   sistema.sonarAlarma();
   // luces encendidas
   // puertas cerradas
}

Ahora contratamos una empresa de vigilancia a la que queremos notificar ¿qué tan desacoplado
es el código?

public class Empresa implements AlarmaListener {
   public void actuar() {
      System.out.println("notificando empresa");
   }
}
public static void main(String[] args) {
   AlarmaListener luces = new Luces();
   AlarmaListener puertas = new Puertas();
   AlarmaListener empresa = new Empresa();
   SistemaCentral sistema = new SistemaCentral();
   sistema.registrar(luces);
   sistema.registrar(puertas);
   sistema.registrar(empresa);
   sistema.sonarAlarma();
   // luces encendidas
   // puertas cerradas
   // notificando empresa
}

Observer

  • Deseamos desacoplar
    completamente 2 clases
  • Deseamos poder añadir
    n clases que reaccionen
    frente a un "evento"

Observer

  • Crear una interface con el
    método que va a ejecutarse
    una vez suceda el evento
  • Implementar la
    interface en n clases
  • La clase que causa el evento,
    contiene una lista de observadores
  • El método que causa el evento,
    recorre los observadores y llama
    el método definido en la interface

¿Parece familiar?

GUI's como Swing utilizan este patrón

El observado puede ofrecer más métodos, como uno para desregistrarse, etc.

Java incluye
java.util.Observable y
java.util.Observer
pero no las use

Strategy

Para un videojuego, tenemos un Avatar
que tiene diferentes
armas que pueden
ser cambiadas
en tiempo real

¿Cómo modificamos el comportamiento de una clase en tiempo real?

public class Avatar {

   public void disparar() {
      // esto debe poder
      // cambiar en tiempo real
   }
}

¡Delegación y Polimorfismo!

/**
 * Esta clase encapsula lo que puede
 * cambiar de la clase Avatar
 */
public interface Arma {
   void disparar();
}
public class Blaster implements Arma {
   public void disparar() {
      System.out.println("BLASTER");
   }
}

* * *

public class Laser implements Arma {
   public void disparar() {
      System.out.println("LASER");
   }
}

* * *

public class Lanzallamas implements Arma {
   public void disparar() {
      System.out.println("LANZALLAMAS");
   }
}
public class Avatar {
   private Arma arma;

   public Avatar() {
      this.arma = new Blaster();
   }
   // un "setter" es una opción
   public void cambiarArma(final Arma arma) {
      this.arma = arma;
   }

   public void disparar() {
      arma.disparar();
   }
}
public static void main(String[] args) {
   Avatar avatar = new Avatar();
   avatar.disparar(); // BLASTER
   avatar.cambiarArma(new Laser());
   avatar.disparar(); // LASER
}

¿Notaron que las armas
no tienen estado?

public enum Arma {
  BLASTER {
    public void disparar() {
      System.out.println("BLASTER");
    }
  }, LASER {
    public void disparar() {
      System.out.println("LASER");
    }
  }, LANZALLAMAS {
    public void disparar() {
      System.out.println("LANZALLAMAS");
    }
  };

  public abstract void disparar();
}
public static void main(String[] args) {
   Avatar avatar = new Avatar();
   avatar.disparar(); // BLASTER
   avatar.cambiarArma(Arma.LASER);
   avatar.disparar(); // LASER
}

Strategy

Si el comportamiento no tiene estado, las clases que implementan la Estrategia pueden ser Singletons 

Strategy

  • Permite cambiar el comportamiento
    de un objeto en tiempo de ejecución
  • También permite cumplir con OCP
  • Y no ¡no tiene nada que ver con
    algoritmos de ordenamiento!

Strategy

  • Define una interfaz con el
    método que puede cambiar en
    tiempo de ejecución
  • El objeto que va a tener el
    comportamiento tiene un atributo
    de esa interfaz y la implementación
    del método delega a este objeto
  • El constructor puede recibir la
    implementación inicial o puede
    simplemente tenerla quemada
  • Mediate un método "setter",
    la implementación puede cambiar

State

Vamos a modelar una máquina dispensadora

Una máquina dispensadora tiene una ranura de monedas y un botón

El comportamiento
de la ranura y el botón cambian según el
estado de la máquina

Estados

  • Vacía:
    - el botón no hace nada
    - la ranura acepta monedas
    y después cambia el estado
  • Moneda insertada:
    - el botón despacha el producto
    y después cambia el estado
    - la ranura no acepta más monedas

Empecemos con una interface para los métodos que dependen del estado

public interface Estado {
  // el estado puede cambiar
  // después de llamar uno
  // de los métodos!
  Estado insertarMoneda();
  Estado presionarBotón();
}

Implementemos
los estados

public class MonedaInsertada implements Estado {

  @Override
  public Estado insertarMoneda() {
    System.out.println("Moneda ya insertada. Ignorando");
    return this; // estado no cambia
  }

  @Override
  public Estado presionarBotón() {
    System.out.println("Producto en la bandeja");
    return new Vacía(); // el estado cambia
  }
}
public class Vacía implements Estado {

  @Override
  public Estado insertarMoneda() {
    System.out.println("Moneda recibida.");
    return new MonedaInsertada(); // estado cambia
  }

  @Override
  public Estado presionarBotón() {
    System.out.println("No se ha insertado una moneda");
    return this; // estado no cambia
  }
}

Y ahora la máquina

public class MáquinaDispensadora {

  private Estado actual = new Vacía();

  public void insertarMoneda() {
    actual = actual.insertCoin();
  }

  public void presionarBotón() {
    actual = actual.pressButton();
  }
}
public static void main(String[] args) {
  MáquinaDispensadora máquina = 
      new MáquinaDispensadora();
  máquina.insertarMoneda(); // recibida
  máquina.presionarBotón(); // producto
  máquina.presionarBotón(); // ignorado
  máquina.insertarMoneda(); // recibida
  máquina.insertarMoneda(); // ignorada
  máquina.presionarBotón(); // producto
}

¡Y si, podemos usar Singletons si los estados
no tienen estado!

¿Se parece el código
a otro patrón?

La forma de implementar State es muy, muy
parecida a Strategy

En State, el objeto mismo cambia de estado
En Strategy, código cliente utiliza un "setter" para cambiar el comportamiento

Muchos patrones se parecen entre sí.
Y en general
la mayoría se basan en
los mismos principios

State

  • Los métodos de una clase cambian
    según el "estado" de del objeto
  • Modelan máquinas de estado

State

  • Crear una interface con los métodos
    que cambian según el estado
  • Los métodos regresan el
    estado después de ejecutarse
  • Se crean clases que implementen
    la interface, una por cada estado
  • La clase con el estado tiene un
    atributode la interface y en los
    métodos delega actualizando el
    atributo con el valor de retorno

Template

Tenemos una página
que permite hacer
giros internacionales

Tenemos 2 proveedores
de pago: uno para tarjetas internacional y otro
para tarjetas nacionales

El algoritmo para hacer
el pago es en esencia
el mismo, pero cada proveedor realiza
algunos pasos de
una manera específica

Algoritmo

  • Se valida el usuario
  • Se autentica la tarjeta de crédito
  • Se hace el cobro a la tarjeta de crédito
  • Se guarda la transacción en la base de datos

Autenticación
Tarjeta de Crédito

  • En algunos países se hace un cobro por $0
  • En otros países se hace un cobro
    por un monto muy pequeño

Guardar Información

  • Para las tarjetas nacionales se almacena
    la cédula del usuario, la fecha y el monto
  • Para las tarjetas internacionales se
    almacena el nombre del usuario,
    fecha, monto y los datos del destinatario

Manera ingenua

Cada método que puede variar tendría un if por cada personalización.
Si tenemos n maneras distintas
de hacer el método tenemos n if's

Usando el Template

Definimos una clase abstracta con un método final que define el algoritmo en términos
de métodos

Los métodos que
varían son abstractos
y protected

public abstract class Giro {
   public final void realizar(Transacción transacción) {
      try {
         validar(transacción.getUsuario());
         autorizar(transacción.getTarjeta());
         cobrar(transacción);
         auditar(transacción);
      } catch (TransacciónException e) {
         revertir(transacción);
      }
   }
   private void validar(Usuario usuario) {
      // ir a la base de datos y verificar
      // que usario no tiene restricciones
   }
   protected abstract void autorizar(Tarjeta tarjeta);
   protected abstract void cobrar(Transacción trx);
   protected abstract void auditar(Transacción trx);
   protected abstract void revertir(Transacción trx);
}

Ahora una clase
por cada variación

public final class GiroNacional extends Giro {   
  protected void autorizar(Tarjeta tarjeta) {
     // intentar cobrar $0
  }
  protected void cobrar(Transacción trx) {
     // se realiza el cobro al proveedor nacional
  }
  protected void auditar(Transacción trx) {
     // se guarda la cédula del usuario
     // el número de la tarjeta, el monto y la fecha
  }
  protected void revertir(Transacción trx) {
     // se revierten los pasos
  }
}
public final class GiroInternacional extends Giro {
   protected void autorizar(Tarjeta tarjeta) {
     // intentar cobrar $.5
   }
   protected void cobrar(Transacción trx) {
     // se realiza el cobro al proveedor 
     // internacional por el monto menos $.5
   }
   protected void auditar(Transacción trx) {
     // el nombre del usuario, número de la tarjeta, 
     // el monto, la fecha y el destinatario
   }
   protected void revertir(Transacción trx) {
     // se revierten los pasos
   }
}

Agregar nuevas variaciones es fácil, solo se extiende la clase apropiada y se sobreescriben los métodos

Este patrón es muy útil en el contexto de páginas web en que una acción es diferente para diferentes tipos de usuarios

Vamos a hacer una página web para 3 tipos de usuarios: Empleado,
Líder de Área, Accionista

El Mega Nav de la
página es distinto dependiendo del usuario

Los líderes de área
y accionistas
tienen acceso a
información exclusiva

También hay una página destacada que cambia
 según el usuario

public abstract class MegaNav {
  private PageFetcher fetcher;
  public MegaNav(PageFetcher fetcher) {
    this.fetcher = fetcher;
  }
  public final NavMenu build(User user) {
    Item featured = getFeatured(user);
    List<Link> headers = getHeaders(user);
    List<Column> columns = new ArrayList<>(6);
    for (Link link : headers) {
      Column c = new Column(
          link, fetcher.subPages(link.getUrl());
      columns.add(c);
    }
    return new NavMenu(features, columns);
  }
  protected Item getFeatured(User user);
  protected List<Column> getHeaders(User user);
}

Ya definido el
algoritmo necesitamos implementaciones de los pasos que cambian

public final class EmpleadoMegaNav extends MegaNav {
  
  public EmpleadoMegaNav(PageFetcher fetcher) {
    super(fetcher);
  }
  @Override
  protected Item getFeatured(User user) {
    // obtener la última noticia del departamento
    // al que pertenece el usuario
  }
  @Override
  protected List<Column> getHeaders(User user) {
    // Los cabezotes son Compañía, Departamento, 
    // Oficina, Recursos Humanos
  }
}
public final class LíderMegaNav extends MegaNav {
  
  public LíderMegaNav(PageFetcher fetcher) {
    super(fetcher);
  }
  @Override
  protected Item getFeatured(User user) {
    // Obtener la próxima reunión de equipo
  }
  @Override
  protected List<Column> getHeaders(User user) {
    // Los cabezotes son Compañía, Empleados Área 
    // Oficina, Recursos Humanos
  }
}
public final class AccionistaMegaNav extends MegaNav {
  
  public AccionistaMegaNav(PageFetcher fetcher) {
    super(fetcher);
  }
  @Override
  protected Item getFeatured(User user) {
    // Obtener la próxima reunión de accionistas
  }
  @Override
  protected List<Column> getHeaders(User user) {
    // Los cabezotes son Compañía, Oficina, 
    // Recursos Humanos, Información Financiera
  }
}

Template

  • Tenemos un método que cambia ligeramente
    según una o más condiciones
  • El tipo de métodos que se llenaría de if's
    si lo hiciéramos de una manera ingenua

Template

  • Creamos una clase abstract
    dónde definimos nuestro algoritmo
  • Nuestro algoritmo está definido
    en un método public y final
  • Este método está implementado llamando
    otros métodos protected y abstract

Template

  • Creamos clases concretas que implementan
    los métodos de manera particular

¿Y si tenemos varias clases concretas cómo escogemos la correcta?

public final class GiroFactory {
  public static Giro(País país) {
    if (país.esNacional()) {
      return new GiroNacional();
    }
    return new GiroInternacional();
  }
}
public final class MegaNavFactory {
  public static MegaNav(PageFetcher fetcher, User user) {
    switch(user.getType()) {
    case EMPLEADO:
      return new EmpleadoMegaNav(fetcher);
    case LÍDER:
      return new LíderMegaNav(fetcher);
    case ACCIONISTA:
      return new AccionistaMegaNav(fetcher);
    default:
      throw new AssertionError(
          "Tipo de Usuario no esperado: " + type);
    }
  }
}

Los Patrones de Diseño muchas veces
se acompañan:
Factory Method y
Template van bien juntos

Q&A

Gracias

@gaijinco
gaijinco@gmail.com

Patrones de Diseño II

By Carlos Obregón

Patrones de Diseño II

  • 1,705