Patrones de Diseño I

Patrones de Diseño

Ofrecen soluciones probadas a problemas frecuentes en la programación

Patrones de Diseño

Hay ~24 en la literatura. Muchos son aplicados en FW. Muchos no son tan frecuentes. Hay al menos 10 que hay que conocer.

Factory Method

Operador new

  • Rígido, el tipo de objeto
    siempre es el mismo
  • Genera acoplamiento
Link link = new SimpleLink(url, texto);

Una instanciación así puede llevarse a
cabo en varias clases

Si queremos cambiar
la implementación debemos hacer cambios
en muchos lugares

¿Qué otras opciones hay para crear objetos?

Static Factory 

Static Factory

Un método estático
que retorna un objeto

public class Link {
   private String url;
   private String texto;

   // constructor private
   private Link(String url, String texto) {
      this.url = url;
      this.texto = texto;
   }

   public static Link of(String url, String texto) {
      // aca puede ir cualquier mejora:
      // podemos retornar siempre el mismo objeto
      // o usar un pool de objetos
      // o podemos cambiar la implementación
      return new Link(url, texto);
   }
}

Static Factory

El método estático
suele estar en la clase,
pero desde Java 8 puede
ir en la interface

public interface Link {
   getUrl();
   getTexto();

   static Link of(String url, String texto) {
      if (url.startsWith("http")) {
         return new ExternalLink(url, texto);
      }
      if (url.contains("@")) {
         return new EmailLink(url, texto);
      }
      if (url.contains("/assets/")) {
         return new FileLink(url, texto);
      }
      return SimpleLink(url, texto);
   }
}

También pueden ir en una clase utilitaria. Si construye objetos llamado Foo la clase suele llamarse Foos

public final class Barcos {

    // usualmente el parámetro es un Enum
    public static Barco of(Tipo tipo) {
       switch(tipo) {
          case Barco.ACORAZADO:
             return new Acorazado();
          case Barco.PORTAAVIONES:
             return new Portaaviones();
          case Barco.SUBMARINO:
             return new Submarino();
          case Barco.BUQUE:
             return new Buque();
          case Barco.DESTRUCTOR:
             return new Destructor();
          default:
             throw new AssertionError("Inesperado: " + tipo);
       }
    }
}

Factory Method

Técnicamente el Factory Method busca lo mismo pero a través de herencia

Factory Method

Sin embargo la manera más común de lograr lo que busca, es con Static Factory

Static Factory

Podemos considerar
Static Factory como un Factory Method light

Factory Method

  • Minimice el use de new
  • Cree objetos dentro de métodos
  • Estos métodos pueden ser estáticos,
    o hacer parte de un árbol de herencia

Factory Method

  • Utilícelo para inicializar objetos
    cuya implementación puede cambiar
  • Utilícelo para poder decidir la
    implementación de un objeto
    en tiempo de ejecución

Builder

Builder

Si la construcción de un objeto es compleja, es mejor sacarla a otro objeto

public class Ubicación {
   private String dirección;
   private String códigoPostal;
   private String bloque;
   private String apartamento;
   private String ciudad;
   private String departamento;
}

¿Cómo construimos un objeto de Ubicación?

Constructor de Ubicación

  • ¿6 parámetros? ¿Cómo nos acordamos del orden?
  • Si tenemos parámetros que son opcionales ¿creamos constructores adicionales con menos parámetros? ¿Qué pasa si algunos de estos opcionales tienen la misma cantidad de parámetros?

Builder

Saquemos la responsabilidad a
otra clase

public class Ubicación {
   // código
   public static class Builder {
      private String dirección;
      private String códigoPostal;
      private String bloque;
      private String apartamento;
      private String ciudad;
      private String departamento;
      // código
   }
}
public static class Builder {
  private String dirección;
  private String ciudad;
  private String departamento;
  
  public Builder dirección(String dirección) {
    this.dirección = dirección;
    return this;
  }
  public Builder ciudad(String ciudad) {
    this.ciudad = ciudad;
    return this;
  }
  public Builder departamento(String departamento) {
    this.departamento = departamento;
    return this;
  }
  // código
}
public class Ubicación {
  private final String dirección; // final
  private final String ciudad;
  private final String departamento;

  private Ubicación(Builder builder) { // private
    this.dirección = builder.dirección;
    this.ciudad = builder.ciudad;
    this.departamento = builder.departamento;
  }

  public static class Builder {
    // código
    public Ubicación build() {
       return new Ubicación(this);
    }
  }
}
Ubicación u = 
    new Ubicación.Builder()
                 .dirección("Cll 100 # 7a - 81")
                 .ciudad("Bogotá")
                 .build();

Ubicación u2 = 
    new Ubicación.Builder()
                 .dirección("Cll 100 # 7a - 81")
                 .códigoPostal("110125")
                 .apartamento("101") 
                 .ciudad("Bogotá")
                 .build();

Link, otra vez

public class Link {
   private String url;
   private String text;

   public Link(String url, String text) {
      this.url = url;
      this.text = text;
   }
   // código
}
Link google = 
    new Link("Google", "http://google.com");
Link wikipedia = 
    new Link("Wikipedia", "http://wikipedia.com");
Link twitter = 
    new Link("Twitter", "http://twitter.com");

¿Notaron el error?

En el constructor el url
va primero que el texto, como ambos son Strings
no es fácil recordarlo

¿Una posible solución?

public class Link {
   private final String url;
   private final String texto;
   
   private Link(Builder builder) {
      this.url = builder.url;
      this.texto = builder.texto;
   }

   public static class Builder {
      private String url;
      private String texto;
      public Builder url(String url) {
        this.url = url;
        return this;
      }
      public Builder texto(String texto) {
        this.texto = texto; 
        return this;
      }
      public Link build() {
         return new Link(this);
      }
   }
}
Link google = 
    new Link.Builder()
            .url("http://google.com")
            .texto("Google")
            .build();

Link twitter = 
    new Link.Builder()
            .texto("Twitter")
            .url("http://twitter.com")
            .build();

Tenga en cuenta que combinar Builder y herencia es más complejo de lo que parece,
pero ¡es posible!

Otro ejemplo, tomado
de Effective Java

public class NutritionFacts {
   private final int servingSize;
   private final int servings;
   private final int calories;
   private final int fat;
   private final int sodium;
   private final int carbohydrate;
   // code
}
public class NutritionFacts {
   // código
  public static class Builder {
  // Parámetros requeridos
  private final int servingSize;
  private final int servings;
  // Opcionales - inicializar con valores "default"
  private int calories = 0;
  private int fat = 0;
  private int carbohydrate = 0;
  private int sodium = 0;

  public Builder(int servingSize, int servings) {
    this.servingSize = servingSize;
    this.servings = servings;
  }

  public Builder calories(int val)
  { calories = val; return this; }

  public Builder fat(int val)
  { fat = val; return this; }

  public Builder carbohydrate(int val)
  { carbohydrate = val; return this; }

  public Builder sodium(int val)
  { sodium = val; return this; }
}
public class NutritionFacts {
   // código
  public static class Builder {
  // Parámetros requeridos
  private final int servingSize;
  private final int servings;
  // Opcionales - inicializar con valores "default"
  private int calories = 0;
  private int fat = 0;
  private int carbohydrate = 0;
  private int sodium = 0;

  public Builder(int servingSize, int servings) {
    this.servingSize = servingSize;
    this.servings = servings;
  }

  public Builder calories(int val)
  { calories = val; return this; }

  public Builder fat(int val)
  { fat = val; return this; }

  public Builder carbohydrate(int val)
  { carbohydrate = val; return this; }

  public Builder sodium(int val)
  { sodium = val; return this; }
}
public class NutritionFacts {
   // código
  public static class Builder {
    // código
    public NutritionFacts build() {
      return new NutritionFacts(this);
    }
  }

  private NutritionFacts(Builder builder) {
    this.servingSize = builder.servingSize;
    this.servings = builder.servings;
    this.calories = builder.calories;
    this.fat = builder.fat;
    this.sodium = builder.sodium;
    this.carbohydrate = builder.carbohydrate;
  }
}
NutritionFacts cocaCola = 
    new NutritionFacts.Builder(240, 8)
                      .calories(100)
                      .sodium(35)
                      .carbohydrate(27)
                      .build();

Builder

  • Permite construir objetos con mucha flexibilidad
  • No afecta la inmutabilidad de la clase
  • Hay una buena separación entre la
    funcionalidad del objeto y la creación de este
  • Java tiene String y StringBuilder

Builder

  • Utilícelo cuando el código
    en el constructor es muy complejo
  • Utilícelo cuando el constructor tenga
    muchos parámetros
  • Utilícelo cuando el constructor tome
    más de un parámetro del mismo tipo
    y el orden no sea obvio

Decorator

Decorator

Queremos añadir funcionalidad a una
clase sin modificarla

// Arrancamos de una interface
// Esta clase permite leer un
// archivo CSV sobre el protocolo FTP
public interface FtpCSVReader {
   InputStream getInputStream();
   CSVParser fetch(InputStream inputStream);
}
// Esta es la clase que provee la funcionalidad
public class SimpleFtpCsvReader 
      implements FtpCsvReader {
  // código

  @Override
  public InputStream getInputStream() {
    // implementación
  }

  @Override
  public CSVParser fetch(InputStream inputStream) {
    // implementación
  }
}

OCP

Las clases deben estar abiertas para la extensión, pero cerradas para
la modificación

FtpCSVReader

  • La clase ya ha sido probada, modificar su
    código puede implicar agregar bugs a
    funcionalidad que ya está en producción
  • También puede pasar que no tengamos
    acceso al código, porque es una librería externa

Decorador

Según un parámetro
de configuración el
archivo CSV puede
venir en formato Zip

public class FtpCsvReaderZippedDecorator implements FtpCsvReader {

   private FtpCsvReader csvReader;

   // Delegación
   public FtpCsvReaderZippedDecorator(FtpCsvReader csvReader) {
     this.csvReader = Objects.requireNonNull(csvReader);
   }
   @Override
   public InputStream getInputStream() {
     try {
       // delegar
       InputStream in = csvReader.getInputStream();
       // y luego decorar
       ZipInputStream zis = new ZipInputStream(in);
       zis.getNextEntry();
       return zis;
    } catch (IOException e) {
       throw new FTPServiceException(":(", e);
    }
  }
  @Override
  public CSVParser fetch(InputStream inputStream) {
    // para métodos no-decorados, simplemente delegar
    return csvReader.fetch(inputStream);
  }
}
public static FtpCsvReader build(final boolean zipped) {
  FtpCsvReader csvReader = new SimpleFtpCsvReader();
  return zipped ? new FtpCsvReaderZippedDecorator(csvReader) 
                : csvReader;
}

Para decorar una clase
solo basta usarla
como parámetro y devolverla decorada

Otro ejemplo, tomado de Head First Design Patterns

Bebidas de Café

  • Mocha, Latte y Expresso
  • Leche: entera, deslactosada o soya
  • Opcionalmente Crema Chantilly
    una o varias porciones
  • Opcionalmente Chispas de Chocolate
    una o varias porciones
  • Precio depende de los ingredientes usados

¿Creamos una clase
con todas las combinaciones posibles?

Decorator

Cada ingrediente extra
es un Decorador

Primero, la interface

public interface Café {
   double precio();
}

Ahora una clase abstracta

public abstract class DecoradorCafé 
      implements Café {
  private Café café;

  public DecoradorCafé(Café café) {
    this.café = Objects.requireNonNull(café);
  }

  @Override
  public double precio() {
    return café.precio();
  }
}

Ahora los cafés, que pueden ser decorados

public class Mocha implements DecoradorCafé {
   // código
   public double precio() {
      return 2.0;
   }
}
* * *
public class Latte implements DecoradorCafé {
   // código
   public double precio() {
      return 2.5;
   }
}
* * *
public class Expresso implements DecoradorCafé {
   // código
   public double precio() {
      return 1.0;
   }
}

Y los decoradores

public class LecheEntera extends DecoradorCafé {

   public LecheEntera(DecoradorCafé decorado) {
      super(decorado);
   }

   public precio() {
      return super.precio() + 0.5;
   }
}
public class LecheDeslactosada extends DecoradorCafé {

   public LecheDeslactosada(DecoradorCafé decorado) {
     super(decorado);
   }

   public precio() {
      return super.precio() + 0.6;
   }
}
public class Soya extends DecoradorCafé {

   public LecheDeslactosada(DecoradorCafé decorado) {
     super(decorado);
   }

   public precio() {
      return super.precio() + 0.75;
   }
}
public class CremaChantilly extends DecoradorCafé {

   public CremaChantilly(DecoradorCafé decorado) {
     super(decorado);
   }

   public precio() {
      return super.precio() + 0.25;
   }
}
public class ChispasChocolate extends DecoradorCafé {

   public ChispasChocolate(DecoradorCafé decorado) {
     super(decorado);
   }

   public precio() {
      return super.precio() + 0.3;
   }
}

¡A servir!

DecoradorCafé mochaSoyaDobleCremaChantilly = 
   new CremaChantilly(new CremaChantilly(new Soya(new Mocha())));

// 0.25 + 0.25 + 0.75 + 2.0
System.out.println(mochaSoyaDobleCremaChantilly.precio());

DecoradorCafé expressoLecheCremaChantillyChispas = 
   new CremaChantilly(new ChispasChocolate(new LecheEntera(new Expresso())));

// 0.25 + 0.3 + 0.5 + 1.0
System.out.println(expressoLecheCremaChantillyChispas.precio());

Decorador

  • Agregar funcionalidad a una clase existente
  • Parte de una interface y una clase inicial
  • Por cada decorador se crea una clase que
    implementa la interface y tiene como un atributo
    el objeto que decora, inyectado en el constructor
  • Para los métodos a decorar usar delegación
    y antes o después agregar nueva funcionalidad
  • Para los métodos que no se decoran,
    usar solo delegación

Singleton

Singleton

Queremos asegurar
que solo exista una instancia de una clase

Singleton

Un patrón más o menos útil, pero que ha sido
usado incorrectamente

¿Cuándo tiene sentido
que una clase solo
tenga una instancia?

  • Cuando la clase no tiene estado
  • Cuando lógicamente, una funcionalidad debería tener un solo punto de acceso: cache, conexión a una base de datos, etc.

Lo que no está bien
es colocar estado-global
en un Singleton

Si estoy programando
un juego de Blackjack,
las cartas de un jugador
no deben estar accesibles
a todas las clases

Usar un Singleton como un punto de acceso global, destruye las bondades de un buen diseño OOP

Dicho eso, Singleton no
es un anti-patrón.
Usarlo mal, si lo es.

Singleton

¿Cómo se usa bien?

Apóyese en su FW

  • No hay necesidad de modificar su código
  • Si llegara a existir un problema, este seguramente se solucionaría simplemente actualizando el FW
  • Aun no evite del todo su mal uso, al menos la documentación del FW no lo promueve

Utilice Enums

  • Un Enum es en efecto un Singleton
  • No hay necesidad de código especial
  • No necesita de un FW

Singleton

  • No lo satanice, pero sepa realmente cómo usarlo
  • Apóyese en su FW o utilice Enums
  • Si le preguntan sobre qué Patrones
    de Diseño conoce, ¡no mencione el Singleton!

Null Object

¡Null debe morir!

Null era una conveniencia que mutó en algo que nunca debió ser

Null debe morir

  • Nunca inicialice una variable con null
  • Nunca acepte null como paramétro
  • Nunca retorne null

Nunca inicialice con null

  • Si aún no conoce el valor de una variable,
    entonces no la defina aún
  • Existe un montón de código viejo que
    inicializa con null pero siempre se puede evitar
  • No inicializar con null evite un montón de errores

Nunca acepte null
como paramétro

  • ¡Si usted pide un parámetro úselo!
  • Cree 2 funciones: una que use el parámetro
    y otra que no lo use. Use nombres
    significativos para explicar la diferencia
  • No aceptar null como parámetro
    evita un montón de errores

Nunca retorne null

  • Devolver null hace que la firma
    de un método sea deshonesto
  • Para quien usa el método no es "obvio"
    que un método puede retornar null
  • ¿Cómo devolvemos un objeto de
    modo que sea claro que el resultado
    del método no fue exitoso?

Null Object

Un Null Object es un objeto, cuyo estado permite que se comporte como si estuviera "vacío"

public List<Transacciones> buscarPorId(String userId) {
   User user = userBO.buscar(id);
   return userBO.transacciones(user); // retorna null
}
public double totalizarTransacciones(String userId) {
   // Pre Java 8
   double total = 0.0;
   // Lanzaría un NPE en la actual implementación
   for (Transacción t : buscarPorId(userId)) {
      total += t.monto();
   }
   return total;
}
public List<Transacciones> buscarPorId(String userId) {
   User user = userBO.buscar(id);
   List<Transacciones> transacciones = userBO.transaccion(user);
   return transacciones != null 
        ? transacciones 
        : Collections.emptyList();
}
public double totalizarTransacciones(String userId) {
   // Pre Java 8
   double total = 0.0;
   // Código no lanza NPE, retorna 0
   // si el usuario no tiene transacciones
   for (Transacción t : buscarPorId(userId)) {
      total += t.monto();
   }
   return total;
}

Null Object

Está bien si a una clase hay que añadir un método como isVacío()

public interface Link {
   String getUrl();
   String getTexto();
   boolean isVacío();

   public static EMPTY = new Link {
      public String getUrl() {
         return "";
      }
      public String getTexto() {
         return "";
      }
      public isVacío() {
         return true;
      }
   }
}
public SimpleLink implements Link {
  
  private String url;
  private String texto;
 
  public SimpleLink(String url, String texto) {
    this.url = url;
    this.texto = texto;
  }

  public String getUrl() {
    return url;
  }
  public String getTexto() {
    return texto;
  }
  public boolean isVacío() {
    return false;
  }
}
public static Link of(String url, String texto) {
  if (url.isEmpty()) {
    return Link.EMPTY;
  }
  return new SimpleLink(url, texto);
}
<c:if test="${link.vacío}">
   <a href="${link.url}">${link.texto}</a>
</c:if>
<a href="${link.url}">${link.texto}</a>

Algunos FW quitan un vínculo
si el atributo href no tiene valor

Sea lo que sea,
¡nunca retorne null!

Optional

Java 8 incluye la clase Optional que permite
crear fácilmente
"Null Object" para
cualquier tipo de dato

Null Object

  • Nunca utilice null, salvo como
    un detalle de implementación
  • Nunca retorne null, para expresar
    la ausencia de un valor use un Null Object
  • Si puede usar Java 8 use Optional

Q&A

¡Gracias!

@gaijinco
gaijinco@gmail.com

Patrones de Diseño I

By Carlos Obregón

Patrones de Diseño I

  • 1,814