Autor Tema: Interfaces de Java, Listeners y clases genéricas  (Leído 12511 veces)

0 Usuarios y 1 Visitante están viendo este tema.

Desconectado mxgxw

  • Global Moderator
  • Trade Count: (1)
  • The Communiter-
  • *
  • Thank You
  • -Given: 27
  • -Receive: 651
  • Mensajes: 5666
  • Starlet - 999cc
    • mxgxw
Interfaces de Java, Listeners y clases genéricas
« : agosto 25, 2011, 09:34:25 pm »
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 Problema

Citar
Tengo 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"
Código: (Questionnaire.java) [Seleccionar]
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".
Código: (Section.java) [Seleccionar]
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 Listas

Si 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.

Código: (Questionnaire.java) [Seleccionar]
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.
Código: (Questionnaire.java) [Seleccionar]
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:

Código: [Seleccionar]
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:
Código: [Seleccionar]
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.

Código: (MyArrayList.java) [Seleccionar]
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.

Código: (Questionnaire.java) [Seleccionar]

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:
Código: [Seleccionar]
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:
Código: [Seleccionar]
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 Interfaces

Podemos 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:
Código: (ArrayListListener.java) [Seleccionar]
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:

Código: (ListenedArrayList.java) [Seleccionar]
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.

Código: (Questionnaire.java) [Seleccionar]
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:

Código: (Main.java) [Seleccionar]
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:
Código: [Seleccionar]
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

« Última Modificación: agosto 25, 2011, 09:54:32 pm por mxgxw »


Desconectado Jaru

  • Trade Count: (21)
  • The Communiter-
  • *
  • Thank You
  • -Given: 782
  • -Receive: 1555
  • Mensajes: 13250
  • some text
Re: Interfaces de Java, Listeners y clases genéricas
« Respuesta #1 : agosto 25, 2011, 10:14:45 pm »
yo no le doy mucho a java pero lo lei todo y de paso entendí

muy bien redactado por cierto
N/A

Desconectado edu_guerr

  • Trade Count: (0)
  • Sv Member
  • ***
  • Thank You
  • -Given: 51
  • -Receive: 70
  • Mensajes: 463
Re: Interfaces de Java, Listeners y clases genéricas
« Respuesta #2 : agosto 25, 2011, 10:29:23 pm »
Segun entiendo con la clase "Listeners" que programe, segun el evento que ocurra puedo llamar metodos especificos?

Desconectado mxgxw

  • Global Moderator
  • Trade Count: (1)
  • The Communiter-
  • *
  • Thank You
  • -Given: 27
  • -Receive: 651
  • Mensajes: 5666
  • Starlet - 999cc
    • mxgxw
Re: Interfaces de Java, Listeners y clases genéricas
« Respuesta #3 : agosto 25, 2011, 10:45:18 pm »
Segun entiendo con la clase "Listeners" que programe, segun el evento que ocurra puedo llamar metodos especificos?


Más bien podemos decir que un Listener es una clase que tiene diferentes métodos que pueden reaccionar a distintos eventos.

¿Que vas a considerar como un evento? Pues eso depende de lo que estes programando.

Para mi caso tengo dos eventos, el evento de adición de item a la lista y el evento de remoción de item. Mi problema es que entre la clase ArrayList y Questionnarie no hay nada que las relacione. Pude haber utilizado otra clase para provocar esos eventos.

Lo importante de este mini-tutorial es entender que las Interfaces nos sirven para que dos clases que originalmente no tienen nada que ver una con otra pueden comunicarse a travez de un "Listener".

Aplicado al concepto de eventos, vos tenes una clase que es la que "genera eventos" (en mi caso ListenedArrayList) y tenes otra clase que los consume. La interfaz lo que te permite es que cualquier clase, sin importar del tipo que sea puede ser capaz de escuchar eventos específicos implementando las funciones especificadas en la interfaz (que aplicado de esta manera se conoce como Listener).

La verdad es un concepto un tanto abstracto y es un poco difícil de entenderlo o hayarle uso a la primera. :)