Builder
https://java-design-patterns.com/patterns/builder/
Sirve para construir objetos pudiendo escoger qué parámetros y en qué orden se proporcionan.
Es com hacer un constructor "universal".
En el siguiente ejemplo, observa que para crear un objeto de clase Algo llamamos primero al constructor de la clase Algo.Builder.
Sobre el objeto de clase Algo.Builder vamos llamando a los métodos setXXX. Estos métodos almacenan los datos temporalmente en el objeto
Algo.Builder, y a su vez retornan el propio objeto Algo.Builder (de esta forma podemos ir encadenando métodos setXXX). La llamada final
al método build() es la que construye el objeto de clase Algo y le pone los valores que se habían ido almacenando.
class Algo {
String esto;
String otro;
int aquello;
static class Builder {
String esto;
String otro;
int aquello;
Builder setEsto(String esto) {
this.esto = esto;
return this;
}
Builder setOtro(String otro) {
this.otro = otro;
return this;
}
Builder setAquello(int aquello) {
this.aquello = aquello;
return this;
}
Algo build(){
Algo algo = new Algo();
algo.esto = this.esto;
algo.otro = this.otro;
algo.aquello = this.aquello;
return algo;
}
}
}
public class Main {
public static void main(String[] args) {
Algo algo = new Algo.Builder()
.setEsto("valorParaEsto")
.setAquello(123241)
.setOtro("valorParaOtro")
.build();
}
}
En la vida real:
- Los diálogos en Android se crean con un builder:
AlertDialog
new AlertDialog.Builder(this)
.setTitle("ALERTA!!!")
.setMessage("Este mensaje se autodestruirá en 3 milenios")
.show();
- Las notificaciones en Android se crean con un builder:
Notification
new Notification.Builder(this, CANNEL_ID)
.setContentText("Has recibido un mensaje")
.sertSmallIcon(R.drawable.icono_nuevo_mensaje)
.build();
Callback
https://java-design-patterns.com/patterns/callback/
Es una "alternativa" al return. En lugar de que un método retorne algo, lo que hará ese método es llamar a un método que le habremos pasado, poniendo como
parámetros lo que haya que retornar.
En el siguiente ejemplo, observa que al llamar al método hacerAlgo le hemos pasado un objeto de clase Algo.Callback.
El método hacerAlgo, en lugar de retornar el texto "algo a retornar", lo pone como parámetro en la llamada de vuelta
al método call del objeto que le habíamos pasado.
class Algo {
interface Callback {
void call(String valorDeRetorno);
}
void hacerAlgo(Callback callback){
callback.call("algo a retornar");
}
}
public class Main {
public static void main(String[] args) {
Algo algo = new Algo();
algo.hacerAlgo(new Algo.Callback() {
@Override
public void call(String valorDeRetorno) {
System.out.println(valorDeRetorno);
}
});
}
}
Cuando un interface solo tiene un único método, podem usar una expresión lambda cuando haya que pasar como parámetro una instancia
de dicho interface.
En el ejemplo anterior, el interface Callback solo tiene el método call.
Podemos pasar la instancia de Algo.Callback con una expresión lambda:
algo.hacerAlgo(valorDeRetorno -> System.out.println(valorDeRetorno));
Observa que una expresión lambda no es más que una simplificación de la instanciación del objeto; se indica solamente el nombre del parámetro y el cuerpo del método:
algo.hacerAlgo(new Algo.Callback() {
@Override
public void call(String valorDeRetorno) {
System.out.println(valorDeRetorno);
}
});
algo.hacerAlgo(valorDeRetorno -> System.out.println(valorDeRetorno));
Por otra parte, un callback puede tener varios métodos, y el que lo recibe puede llamar a uno u otro según sea conveniente. De esta forma, un
método puede "retornar" distintos tipos de datos, o quizá "retornar" errores,...
class Algo {
interface Callback {
void retornoUnString(String valorDeRetorno);
void retornoUnInt(int valorDeRetorno);
void retornoUnBoolean(boolean valorDeRetorno);
void retornoUnError(String valorDeRetorno);
}
void hacerAlgo(Callback callback){
if (...) {
callback.retornoUnString("algo a retornar");
} else if (...) {
callback.retornoUnInt(324342);
} else if (...) {
callback.retornoUnBoolean(true);
} else if (...) {
callback.retornoUnError("Error xyz");
}
}
}
public class Main {
public static void main(String[] args) {
Algo algo = new Algo();
algo.hacerAlgo(new Algo.Callback() {
@Override
public void retornoUnString(String valorDeRetorno) {
System.out.println("El resultado es: " + valorDeRetorno);
}
@Override
public void retornoUnInt(int valorDeRetorno) {
System.out.println("El resultado es: " + valorDeRetorno);
}
@Override
public void retornoUnBoolean(boolean valorDeRetorno) {
System.out.println("El resultado es: " + valorDeRetorno);
}
@Override
public void retornoUnError(String valorDeRetorno) {
System.out.println("Ha habido un error: " + valorDeRetorno);
}
});
}
}
En este caso, como el interface Algo.Callback tiene más de un método, no se puede usar una expresión lambda.
En la vida real:
- La librería retrofit sirve para hacer llamadas HTTP. La respuesta se retorna mediante un callback:
Callback
api.obtenerNombre().enqueue(new Callback<String>() {
@Override
public void onResponse(Call<String> call, Response<String> response) {
System.out.println("Nombre: " + response.body());
}
@Override
public void onFailure(Call<String> call, Throwable t) {
System.out.println("Fallo al obtener el nombre");
}
});
- La implementación tyrus del
protocolo websockets utiliza callbacks para gestionar los eventos de conexión a un servidor:
Endpoint
ClientManager.createClient().connectToServer(
new Endpoint() {
@Override
public void onOpen(Session session, EndpointConfig config) {
System.out.println("Conectado al servidor");
}
@Override
public void onClose(Session session, CloseReason closeReason) {
System.out.println("Desconectado del servidor");
}
@Override
public void onError(Session session, Throwable thr) {
System.out.println("Error conectando al servidor");
}
},
new URI("ws://websocket.server:1234"));
Factory
https://java-design-patterns.com/patterns/factory/
Proporciona una forma de crear objetos sin tener que especificar su clase. Un método factory retorna los objetos de la clase adecuada.
Un método factory retorna objetos de una misma superclase, pero estos pueden ser de diferente subclase.
interface SuperClase {
void metodo();
}
class SubClaseA implements SuperClase {
@Override
public void metodo() {
// do something A-style
}
}
class SubClaseB implements SuperClase {
@Override
public void metodo() {
// do something B-style
}
}
class Factory {
static SuperClase obtenerObjeto(){
if (...) {
return new SubClaseA();
else {
return new SubClaseB();
}
}
}
public class Main {
public static void main(String[] args) {
SuperClase objeto = Factory.obtenerObjeto();
}
}
Hay muchas formas de implementar este patrón. Otra opción muy común es incluir el método factory en la superclase:
abstract class SuperClase {
abstract void metodo();
static SuperClase obtenerObjeto() {
if (...) {
return new SubClaseA();
else {
return new SubClaseB();
}
}
}
class SubClaseA extends SuperClase {
@Override
public void metodo() {
System.out.println("Soy de clase A");
}
}
class SubClaseB extends SuperClase {
@Override
public void metodo() {
System.out.println("Soy de clase B");
}
}
public class Main {
public static void main(String[] args) {
SuperClase objeto = SuperClase.obtenerObjeto();
}
}
En la vida real:
- El método Calendar.getInstance() retorna una implementación diferente de la clase Calendar en función de la localización del sistema:
Calendar
Calendar calendar = Calendar.getInstance();
// el objeto 'calendar' puede ser de clase 'BuddhistCalendar', 'JapaneseImperialCalendar' o 'GregorianCalendar'
- El método List.of retorna una lista inmutable con los elementos que se han pasado como parámetros:
List
List<String> lista = List.of("this", "list", "is", "immutable");
// estos métodos darían error
list.add("error");
lista.set(0,"error");
Observer (aka Listener)
https://java-design-patterns.com/patterns/observer/
Proporciona una manera de realizar varias acciones cuando sucede un evento, desligando las acciones del evento.
El objeto en el cual sucede el evento notifica a otros objetos que el evento ha sucedido, y estos otros objetos realizan las acciones correspondientes.
Previamente, estos objetos se deben haber suscrito para ser notificados.
import java.util.ArrayList;
import java.util.List;
class NotificadorDeEvento {
interface ObservadorDeEvento {
void cuandoSucedaElEvento();
}
List<ObservadorDeEvento> observadoresDeEvento = new ArrayList<>();
void suscribirAEvento(ObservadorDeEvento observadorDeEvento) {
observadoresDeEvento.add(observadorDeEvento);
}
void sucederEvento(){
for (ObservadorDeEvento observadorDeEvento: observadoresDeEvento) {
observadorDeEvento.cuandoSucedaElEvento();
}
}
}
class ObservadorDeEvento1 implements NotificadorDeEvento.ObservadorDeEvento {
@Override
public void cuandoSucedaElEvento() {
System.out.println("Accion 1");
}
}
class ObservadorDeEvento2 implements NotificadorDeEvento.ObservadorDeEvento {
@Override
public void cuandoSucedaElEvento() {
System.out.println("Accion 2");
}
}
public class Main {
public static void main(String[] args) {
NotificadorDeEvento notificadorDeEvento = new NotificadorDeEvento();
ObservadorDeEvento1 observadorDeEvento1 = new ObservadorDeEvento1();
notificadorDeEvento.suscribirAEvento(observadorDeEvento1);
ObservadorDeEvento2 observadorDeEvento2 = new ObservadorDeEvento2();
notificadorDeEvento.suscribirAEvento(observadorDeEvento2);
notificadorDeEvento.sucederEvento();
}
}
En la vida real:
- La mayoría de bibliotecas de UI utilizan Listeners para notificar los eventos de interacción del usuario.
Por ejemplo, en Android, cuando un usuario hace "clic" sobre un botón, el propio botón notifica el evento a los listeners que se hayan suscrito.
View
button.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
// do something when the button is clicked
}
});
Ocurre lo mismo en Java Swing
AbstractButton
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent actionEvent) {
// do something when the button is clicked
}
});
- Las bases de datos de Firebase utilizan Listeners para notificar los cambios en los datos en tiempo real:
FirebaseDatabase
database.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
// do something with new data
}
@Override
public void onCancelled(DatabaseError databaseError) {
// getting data failed
}
});
Singleton
https://java-design-patterns.com/patterns/singleton/
Sirve para asegurar que solo se pueda instanciar un único objeto de una clase, proporcionando una forma de acceder a él.
Hay diversas formas de implementar este patrón; distinguiremos 3:
Eager:
class Algo {
private static final Algo INSTANCIA = new Algo();
private Algo(){}
static Algo getInstance(){
return INSTANCIA;
}
}
public class Main {
public static void main(String[] args) {
Algo algo = Algo.getInstance(); // se obtiene la unica instancia
Algo otro = new Algo(); // error!, no se puede crear otra instancia (el constructor es private)
}
}
Lazy:
class Algo {
private static Algo INSTANCIA;
private Algo(){}
static Algo getInstance(){
if (INSTANCIA == null) {
INSTANCIA = new Algo();
}
return INSTANCIA;
}
}
public class Main {
public static void main(String[] args) {
Algo algo = Algo.getInstance(); // se obtiene la unica instancia
Algo otro = new Algo(); // error!, no se puede crear otra instancia (el constructor es private)
}
}
Synchronized:
class Algo {
private static Algo INSTANCIA;
private Algo(){}
static synchronized Algo getInstance(){
if (INSTANCIA == null) {
INSTANCIA = new Algo();
}
return INSTANCIA;
}
}
public class Main {
public static void main(String[] args) {
Algo algo = Algo.getInstance(); // se obtiene la unica instancia
Algo otro = new Algo(); // error!, no se puede crear otra instancia (el constructor es private)
}
}
En la vida real:
- El objeto Runtime permite, entre otras cosas, ejecutar otras aplicaciones del sistema operativo:
Runtime
Runtime.getRuntime();
- Android Room Database:
Room database
Adapter
https://java-design-patterns.com/patterns/adapter/
Es una alternativa al paso de parámetros. En lugar de que un método reciba los datos por parámetros, este método llama a los métodos del Adapter para obtenerlos.
En el siguiente ejemplo, el método imprimirValor() obtiene el valor llamando al método obtenerValor() del Adaptador:
class Algo {
interface Adaptador {
int obtenerValor();
}
Adaptador adaptador;
void establecerAdaptador(Adaptador adaptador) {
this.adaptador = adaptador;
}
void imprimirValor(){
System.out.println(adaptador.obtenerValor());
}
}
public class Main {
public static void main(String[] args) {
Algo algo = new Algo();
algo.establecerAdaptador(new Algo.Adaptador() {
@Override
public int obtenerValor() {
return 0;
}
});
algo.imprimirValor();
}
}
En el siguiente ejemplo, el método imprimirValores() obtiene primero la cantidad de valores del Adaptador, y luego obtiene los diversos valores y los imprime.
Observa también que el Adaptador se lo pasamos directamente al método, en lugar de establecerlo previamente como en el ejemplo anterior.
class Algo {
interface Adaptador {
int obtenerCantidad();
String obtenerValor(int i);
}
void imprimirValores(Adaptador adaptador){
int cantidad = adaptador.obtenerCantidad();
for (int i = 0; i < cantidad; i++) {
System.out.println(adaptador.obtenerValor(i));
}
}
}
public class Main {
public static void main(String[] args) {
Algo algo = new Algo();
algo.imprimirValores(new Algo.Adaptador() {
@Override
public int obtenerCantidad() {
return 10;
}
@Override
public String obtenerValor(int i) {
return "Valor: " + i;
}
});
}
}
En la vida real:
- Para mostrar listas en una App Android se usa un RecyclerView. Los datos a mostrar los obtiene con un Adaptador:
RecyclerView.Adapter
recyclerView.setAdapter(new RecyclerView.Adapter<RecyclerView.ViewHolder>() {
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new RecyclerView.ViewHolder(new TextView(parent.getContext())){};
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
((TextView) holder.itemView).setText("" + position);
}
@Override
public int getItemCount() {
return 10;
}
});