El patrón Observer en Swing

El patrón Observer en Swing

Esta entrada sirve para poner en práctica todo lo aprendido del patrón Observer en esta y esta entrada del blog. Si no sabéis qué es el patron Observer o teneis dudas, echadles un vistazo antes.

El patron observer en Swing es un poco más intrincado de lo que estudiamos en la teoría la primera vez, sobre todo por la gran cantidad de eventos y clases que hay, pero con un diagrama UML para una clase concreta, creo que podremos abstraer esta lógica.

Pongamos por ejemplo el caso de un JComboBox.

El JComboBox es un componente de Swing que nos permite seleccionar una opción de una lista desplegable, lo que nos da una pista: debe ser algo que se deba observar y este notificará cuando un evento como la selección de un ítem tenga lugar. Tendrá presumiblemente el rol de Observable.

Si echamos un vistazo a su implementación, nos encontraremos con que se compone entre otros, de un objeto de tipo EventListenerList. Si investigamos un poco, vemos que es un objeto muy sencillo , y la base del patrón Observer en Swing. No en vano JComponent (su clase padre) tiene este mismo campo, siendo según la documentación «The base class for both standard and custom components that use the Swing architecture».

Pongamos lo que sabemos por ahora en un diagrama UML: Un Componente de tipo JComboBox tiene un campo donde se almacenan diferentes objetos que extienden de la clase EventListener.

Click para ampliar

Ya tenemos al elemento Observable más o menos definido. Ahora falta definir el Observador, que puede ser cualquier cosa, siempre y cuando le podamos enviar mensajes.

Si recordamos, el observador debe implementar un método para recibir notificaciones. Ese método es el que se llama desde el elemento observado para desencadenar un algoritmo del que ya no es responsable.

¿Recuerdas de qué tipo son los oyentes en la lista de tipo EventListenerList del objeto Observable? Todos heredaban de una de tres interfaces: ActionListener, ItemListener y PopupMenuListener. Pues por definición, si queremos meter un oyente en esa lista para que sea notificado, debe implementar una de estas tres interfaces e implementar los métodos correspondientes.

El cómo llama al evento es algo que dejo para las personas que tengan paciencia para comprender a fondo como funciona Swing. Se ven implicadas otras clases y supondría desviarse del tema demasiado. Pero el cuándo, es fácil de adivinar: cuando se llaman a los métodos que comienzan por fire, por ejemplo fireActionEvent().

Así que si queremos que nuestra clase observadora sea notificada de un evento de un JComboBox solo tendremos que seguir estos dos pasos:

  1. Implementar la interfaz del evento que queramos escuchar y añadirle una funcionalidad.
  2. Añadir como oyente a nuestra clase observable de tipo JComboBox.

Click para ampliar

Y por último…

No hace falta decir que este modelo está muy simplificado, para facilitar la comparación con el modelo teórico. Muchas más interfaces heredan de EventListener y el JComboBox implementa varias interfaces para escuchar (es a la vez observadora y observada).

Os animo a bucear por el código fuente del JDK alguna vez en clases no muy enrevesadas. Es un gran ejercicio tanto para conocer el lenguaje como para coger fluidez en la lectura de código.

El patrón Observer en Java I

El patrón Observer en Java I

En la entrada del patrón observer, terminábamos con una pregunta.

¿Cómo podemos controlar cuándo se envían las notificaciones?

La respuesta es simple: añadiendo un estado al sujeto que envía las notificaciones mediante una variable. Si esta variable tiene un valor, notificamos a los observadores y la devolvemos a su estado original. En caso contrario no hacemos nada.

Por ejemplo, supongamos que la clase Balanza notifica a sus observadores cuando se ha puesto un peso sobre ella de más de 1000gr. Si no tuviéramos esa variable booleana en cada pesaje tendríamos que recorrer toda la lista, notificando de cualquier pesaje.

Podrías pensar acertadamente que existe cierta complicación en el código. ¿Por que no notificar directamente si se da una condición a los observadores? Bueno, en este ejemplo quizás si sería lo más lógico, pero sirve como excusa para extrapolar a otras situaciones donde las condiciones para notificar sean más complejas.

Aunque controlar las notificaciones no es algo contemplado en el propio patrón observer, la implementación de Java sí que añade por defecto esta posibilidad. Si echamos un vistazo al código de la clase Observable vemos que tiene dos campos: un Vector de objetos Observer y un atributo booleano.

Una herencia envenenada

Que Observable tenga una clase Vector para albergar los Observer puede ser indicativo de una necesaria huida. Pero aprovechemos que estamos aquí para hacer énfasis en el punto clave de esta implementación:

  • Observable es una clase concreta.

Esto implica que para hacer tu patrón Observer con la implementación de Java tienes una limitación: Tu objeto debe heredar de Observable, y si ya extiende otro objeto, no puedes hacer nada. ¿Que pasa si quieres customizar tu objeto Observable? Sigues teniendo que extender la clase. Y si quieres componer un objeto con otro que extienda Observable, olvídate de usar sus métodos para cambiar y leer el estado porque son protected.

Por supuesto esto tiene una explicación y es que estas dos clases, tal y como podéis ver en su documentación, existen desde el JDK 1.0. Por este y otros motivos se recomienda implementar tu propio patrón Observer a través de interfaces.

Puedes encontrar un ejemplo sencillo y comentado de la implementación del patrón observer en mi repositorio de Github.

 

El patrón Observer

El patrón Observer

El patrón Observer es posiblemente el más conocido de los patrones de diseño de software, quizás porque es el común denominador de cualquier aplicación gráfica.

Es muy fácil de explicar y se puede resumir en que cuando un objeto A le pasa algo, este se lo notifica a una serie de objetos llamados observadores. Quizás lo difícil sea implementarlo, pero vayamos por partes.

Según la definición tenemos dos tipos de agentes: un sujeto del cambio y un observador y por lo tanto tendremos dos clases: un Sujeto y un Observador.

Sabemos también que el sujeto debe notificar a un observador. Para ello haremos que el sujeto guarde en una lista a todos sus observadores.

¿Pero como lo notifica? Pues simplemente implementaremos un método en los observadores que sea llamado por el objeto observado mediante un bucle foreach.

Pero esto se puede mejorar…¿Como? El comportamiento que hemos visto es extensible para cualquier objeto sujeto a cambio y a sus observadores, por lo tanto, podemos abstraerlo en dos interfaces.

Esta generalización nos permite tener varios observadores u oyentes que hagan distintas cosas cuando el sujeto quiera notificar algo. Hemos desacoplando ambos objetos y ahora el sujeto ya no tiene que saber de qué tipo concreto es su oyente para notificarle un cambio.

¿Pero qué pasa con los observadores?

Como a lo mejor te habrás dado cuenta este diseño deja un poco desvalidas a las clases observadoras.  No controlan cuando deben dejar de serlo de una clase ni tienen control ninguno sobre cuándo quieren ser notificadas. No nos preocupemos, si queremos que las clases observadoras tengan el control de suscribirse y desuscribirse a eventos, solo necesitaremos hacer implementar una referencia a la clase que está observando para notificarle sus deseos.

¿Y si una clase solo quiere ser notificada en determinadas circunstancias? Bueno, aprovecharemos para explicar eso mientras vemos la implementación nativa de Java para este patrón en otra entrada.