Patrones de diseño

Ejercicios

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; } });