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