Me encontraba "modelando" unas clases que representan un cuestionario y de pronto me vi en la necesidad de monitorear cuando ingresaba o removía objetos de una lista.
¿El problema? Tanto en Java como en C# no podemos monitorear cuando un item se agrega a una lista o cuando se elimina. Realmente si podríamos hacerlo en código, pero... ¡Imaginen tener que hacer esto para cada ArrayList que creamos!
Esta vez pienso introducirlos a un concepto muy utilizado en Java llamado "Listener" (que podríamos traducir como "oyente"). Un Listener es una clase que contiene un conjunto de métodos que son llamados cada vez que ocurre un evento.
Seguramente algunos de ustedes han trabajado con EventListeners cuando trabajan con interfaces gráficas. Pero los listeners no están limitados a ello, utilizados de buena manera pueden monitorear lo que sucede en nuestro propio código.
El ProblemaTengo una clase cuestionario que tiene diferentes secciones, las secciones pueden agregarse o eliminarse dinámicamente. Cada vez que yo agregue una sección a la lista quiero que me muestre un mensaje en consola de la sección que ha sido agregada o eliminada.
Comencemos con lo básico, nuestro problema voy a solucionarlo primero de la manera "tradicional" (al estilo C) y luego voy a aplicar un enfoque diferente utilizando interfaces, listeners y clases genéricas.
Primero Vamos a crear una clase llamada "Questionnaire"
package com.mxgxw.questionnaire.model; // En este paquete voy a "modelar mi cuestionario"
public class Questionnaire {
public String ID; // Identificador del cuestionario
public Questionnaire() {
this.ID = "My questionnaire"; // Nombre por defecto del cuestionario
}
public Questionnaire(String id) {
this.ID = String.valueOf(id); // Utilizo valueOf para que se duplique el string y no se pase por referencia
}
}
Ahora necesitamos crear las secciones de nuestro cuestionario, vamos a crear una nueva clase "Section".
package com.mxgxw.questionnaire.model; // En este paquete voy a "modelar mi cuestionario"
public class Section {
public String ID; // Identificador de seccion
public Section() {
this.ID = "My Section"; // Nombre por defecto de la seccion
}
public Section(String id) {
this.ID = String.valueOf(id); // Utilizo valueOf para que se duplique el string y no se pase por referencia
}
}
Utilizando las ListasSi sabemos que nuestra cuestionario va a tener N secciones, podemos seguir dos caminos: Almacenar dichas secciones en un array, que implicaria estar regenerando el arreglo cuando este supere el tamaño máximo. O utilizar la clase "
ArrayList" de java.
La clase ArrayList de Java es un tipo de colección que representa una lista dinámica que nos permite guardar N items y su tamaño se va ajustando en tanto vamos agregando nuevos elementos. Esto es una gran ventaja si no sabemos cuantas secciones vamos a tener que almacenar.
Como ya tenemos nuestra clase Questionnaire, vamos a modificarla un poco.
package com.mxgxw.questionnaire.model; // En este paquete voy a "modelar mi cuestionario"
import java.util.*;
public class Questionnaire {
public String ID; // Identificador del cuestionario
// *** agregamos nuestra lista aqui.
public ArrayList<Section> sections;
public Questionnaire() {
this.ID = "My questionnaire"; // Nombre por defecto del cuestionario
sections = new ArrayList<Section>(); // Debemos instanciar nuestra lista para poder utilizarla.
}
public Questionnaire(String id) {
this.ID = String.valueOf(id); // Utilizo valueOf para que se duplique el string y no se pase por referencia
}
}
ArrayList es una
clase genérica, esto significa que puede trabajar con cualquier tipo de objeto. Para nuestro caso queremos forzar a que trabaje con objetos del tipo "Section", al colocar "Section" entre "<" y ">" le estamos diciendo al compilador que esa lista en particular solo va a almacenar objetos de ese tipo.
Vamos a agregar una función extra que nos permita ver las secciones que tiene agregada actualmente la clase.
package com.mxgxw.questionnaire.model; // En este paquete voy a "modelar mi cuestionario"
import java.util.*;
public class Questionnaire {
public String ID; // Identificador del cuestionario
// *** agregamos nuestra lista aqui.
public ArrayList<Section> sections;
public Questionnaire() {
this.ID = "My questionnaire"; // Nombre por defecto del cuestionario
sections = new ArrayList<Section>(); // Debemos instanciar nuestra lista para poder utilizarla.
}
public Questionnaire(String id) {
this.ID = String.valueOf(id); // Utilizo valueOf para que se duplique el string y no se pase por referencia
}
public void printSections() { // Esta función me servira para mostrar las secciones agregadas actualmente
for(int i=0;i<sections.size();i++)
System.out.println(sections.get(i).ID);
}
}
Hasta aquí... podríamos dar la primera solución al problema de forma tradicional:
import com.mxgxw.quest.model.*;
public class Main {
public static void main(String[] args) {
Questionnaire q1 = new Questionnaire();
Section s1 = new Section("Section 1");
q1.sections.add(s1);
System.out.println(s1+" added.");
Section s2 = new Section("Section 2");
q1.sections.add(s2);
System.out.println(s2+" added.");
Section s3 = new Section("Section 3");
q1.sections.add(s3);
System.out.println(s3+" added.");
q1.sections.remove(s3);
System.out.println(s3+" removed.");
q1.printSections();
}
}
Salida:
run:
Section 1 added.
Section 2 added.
Section 3 added.
Section 3 removed.
Section 1
Section 2
BUILD SUCCESSFUL (total time: 1 second)
¡¡MOMENTO!! Yo quería que me dijera cuando agregara o quitara una sección de manera dinámica... No estar codificando cada println en cada momento que agregue o quite un elemento.
Claro, Aquí vamos a intentar hacer algo. La segunda solución la realizaremos aplicando el paradigma de la POO.... Vamos a heredar una clase nueva del ArrayList para "atrapar" cuando agreguemos o quitemos un objeto.
Ok... Manos a la obra!, vamos a crear una nueva clase que se llamará "MyArrayList" y voy a sobreponer mis funciones de add y remove.
package com.mxgxw.questionnaire.model; // En este paquete voy a "modelar mi cuestionario"
import java.util.*;
public class MyArrayList extends ArrayList {
public boolean add(Section item) {
System.out.println(item.ID+" added.");
return super.add(item);
}
public boolean remove(Object item) {
if(super.contains(item))
System.out.println(((Section)item).ID+" removed.");
return super.remove(item);
}
}
Ahora, solo tenemos que modificar nuestra clase Questionnaire para que haga uso de nuestro nuevo "MyArrayList" en vez del original.
package com.mxgxw.questionnaire.model; // En este paquete voy a "modelar mi cuestionario"
import java.util.*;
public class Questionnaire {
public String ID; // Identificador del cuestionario
// *** agregamos nuestra lista aqui.
public MyArrayList sections; // Ya no usamos el array list original
public Questionnaire() {
this.ID = "My questionnaire"; // Nombre por defecto del cuestionario
sections = new MyArrayList(); // Debemos instanciar nuestra lista para poder utilizarla.
}
public Questionnaire(String id) {
this.ID = String.valueOf(id); // Utilizo valueOf para que se duplique el string y no se pase por referencia
}
public void printSections() { // Esta función me servira para mostrar las secciones agregadas actualmente
for(int i=0;i<sections.size();i++)
System.out.println(((Section)sections).get(i).ID);
}
}
Modificamos ahora nuestro código:
package com.mxgxw.questionnaire.model;
public class Main {
public static void main(String[] args) {
Questionnaire q1 = new Questionnaire();
q1.sections.add(s1);
Section s2 = new Section("Section 2");
q1.sections.add(s2);
Section s3 = new Section("Section 3");
q1.sections.add(s3);
q1.sections.remove(s3);
q1.printSections();
}
}
Salida:
run:
Section 1 added.
Section 2 added.
Section 3 added.
Section 3 removed.
Section 1
Section 2
BUILD SUCCESSFUL (total time: 1 second)
Con esto sería suficiente para solucionar nuestro problema. Sin embargo, hay un pequeño problema:
Nuestro nuevo MyArrayList solo funciona con objetos de tipo "Section". Esto es bueno y válido a nivel de POO... Pero el problema es: ¿Que tal si luego quiero re-utilizar el código? no puedo hacerlo porque he obligado a mi ArrayList a tener un solo tipo de elementos.
Pero hagamolo más complicado: ¿Que tal si quisiera que la clase Questionnaire imprimera una lista de sus secciones cada vez que agregara o quitara una?
Obviamente desde la clase
MyArrayList no puedo acceder a la funcion
printSections de la clase
Questionnaire. ¿Cómo lo hago?
Las InterfacesPodemos llamar a las interfaces "
clases incompletas", una interface es como un plano, solo nos dice que métodos tiene que implementar una clase más sin embargo no los implementa.
Digamos que es una especie de "plantilla". Se utiliza muy seguido para comunicar "mensajes" entre clases que no están relacionadas entre sí. Como en nuestro caso, queremos que la clase ArrayList comunique a la clase Questionnaires cuando se agregue o quite un elemento.
Primero, antes que nada vamos a crear nuestra interface:
package com.mxgxw.questionnaire.model;
public interface ArrayListListener<T> {
public void onAdd(T item); // Método que se llamará cuando se agregue un item
public void onRemove(Object o); // Método que se llamará cuando se elimine un item
}
Como un "extra" hemos hecho nuestra interfaz "genérica", esto significa que trabajará con cualquier tipo de objeto y no únicamente con las "secciones".
Si se fijan, las funciones se llaman "onAdd" (Al agregar) y "onRemove" (Al eliminar). En Java, como convención, a este tipo de interfaz se les llaman "Listener". ¿Por qué? porque nuestro objetivo final es lograr "escuchar" cuando ocurra un evento.
Los prefijos "on" nos indican una acción, en este ejemplo las acciones de agregar y eliminar.
Creando nuestro ArrayListener "monitoreado"Aquí viene la parte más complicada. Originalmente nos conformamos con derivar una clase de ArrayList, en esta ocasión queremos mantener la clase genérica para poder reutilizar nuestro código en cuantos objetos querramos.
Para ello vamos a crear una clase que llamaremos "ListenedArrayList", elejí "listened" porque suena más bonito que "monitored"
(espero no venga ningún talibán del inglés a corregirme).
Esta clase a diferencia del ArrayList normal, guardará dentro de si misma "sorpresa" una lista de listeners
¿Que significa esto? Que podemos tener N diferentes clases escuchando cuando se agregue o se elimine un item y que serán notificadas justo cuando esto suceda.
Pero dejemos de hablar y pongamonos a codificar:
package com.mxgxw.questionnaire.model;
import java.util.*;
public class ListenedArrayList<T> extends ArrayList<T> { // Mantenemos el uso de la clase genérica
private ArrayList<ArrayListListener<T>> listeners; // Muy importante, mantenemos nuestra interfaz genérica
public ListenedArrayList() { // Nuestro constructor por defecto
super(); // Esto crea un array list comun y corriente
listeners = new ArrayList<ArrayListListener<T>>(); // Inicializamos nuestra lista de listeners
}
// addListener agrega un "oyente" a nuestra lista, cada oyente que agreguemos
// será notificado cuando se agregue o elimine un item
public void addListener(ArrayListListener<T> listener) {
listeners.add(listener);
}
// Utilizamos @Override porque nos interesa reemplazar la funcionalidad original del ArrayList
@Override
public boolean add(T item) {
boolean result = super.add(item); // Agregamos el item al ArrayList original
for(int i=0;i<listeners.size();i++) { // Buscamos todos los listeners
listeners.get(i).onAdd(item); // Llamamos a su funcion onAdd
}
return result;
}
// Utilizamos @Override porque nos interesa reemplazar la funcionalidad original del ArrayList
@Override
public boolean remove(Object o) {
boolean result = super.remove(o); // Eliminamos el item de ArrayList original
if(super.contains(o)) { // Si el objeto está contenido en el ArrayList
for(int i=0;i<listeners.size();i++) { // Buscamos todos los listeners
listeners.get(i).onRemove(o); // Llamamos a su funcion onRemove
}
}
return result;
}
}
¿Que va a ocurrir ahora? Tenemos listo nuestro "ArrayList" monitoreado (ListenedArrayList), tenemos nuestra interface lista. ¿Que más necesitamos hacer?
Bueno, necesitamos crear clases que puedan "escuchar" los eventos onAdd y onRemove.
Vamos a usar nuestra clase original Questionnaire y vamos a hacer unas pequeñas modificaciones.
public class Questionnaire implements ArrayListListener<Section> { // Implementamos ArrayListListener para poder recibir las notificaciones
public ArrayList<Section> sections; // Usamos el ArrayList Original
public Questionnaire() {
sections = new ListenedArrayList<Section>(); // Instanciamos un ListenedArrayList
((ListenedArrayList)sections).addListener(this);
}
public void printSections() {
for(int i=0;i<sections.size();i++)
System.out.println(sections.get(i).ID);
}
public void onAdd(Section item) { // Esta funcion es la implementación del método de la interface
System.out.println(item.ID+" added."); // Se llama cada vez que se agrega un item a sections
this.printSections();
}
public void onRemove(Object o) { // Esta funcion es la implementación del método de la interface
System.out.println(((Section)o).ID+" removed."); // Se llama cada vez que se elimina un item a sections
this.printSections();
}
}
Modificamos ahora nuestro código:
package com.mxgxw.questionnaire.model;
public class Main {
public static void main(String[] args) {
Questionnaire q1 = new Questionnaire();
q1.sections.add(s1);
Section s2 = new Section("Section 2");
q1.sections.add(s2);
Section s3 = new Section("Section 3");
q1.sections.add(s3);
q1.sections.remove(s3);
}
}
Salida:
run:
Section 1 added.
Section 1
Section 2 added.
Section 1
Section 2
Section 3 added.
Section 1
Section 2
Section 3
BUILD SUCCESSFUL (total time: 0 seconds)
¡¡Eureka!! Hemos creado nuestro propio Listener para eventos de lista y hemos aplicado Interfaces, Clases genéricas y hemos aplicado el concepto de Event Listeners.
Java si bien es un lenguaje Orientado a Objetos, muchas de sus clases funcionan "orientadas a eventos". Los Eventos y los Listeners son la base para poder programar aplicaciones basadas en eventos.
Haciendo uso de listeners y de clases genéricas pueden monitorear tanto eventos físicos como eventos "abstractos" de manipulación de datos como en este ejemplo.
Como ejercicio para el lector me gustaría preguntar
¿En que otra cosa se te ocurre que podrías aplicar un Event Listener?¿Se anima alguien a hacer otro ejemplo de Event Listener sencillo?Espero les haya gustado esta pequeña programación guiada
¡¡Hasta la próxima!! fskajfs kfashfkjfasd