El patrón de diseño Observer y una forma de implementarlo en Java

Escrito por el , actualizado el .
java planeta-codigo programacion
Enlace permanente Comentarios

Otro de los patrones de diseño que en algún momento nos puede ser útil es el patrón Observer. Podemos usar este patrón si tenemos la necesidad de realizar acciones como consecuencia del cambio de estado o cierta circunstancia de un objeto. El patrón Observer nos permite mantener desacoplados el objeto que emite el evento y el objeto que recibe el evento e independizar al objeto observable del número de observadores que tenga.

Java

En Java este patrón podemos implementarlo usando una clase, la clase Observable, y una interfaz, la interfaz Observer proporcionadas en el propio JDK. La clase que queremos que reciba los eventos deberá implementar la interfaz Observer y el objeto que queremos que produzca los eventos debe extender o contener una propiedad de tipo Observable. La interfaz Observer contiene un único método de nombre update, que recibe dos parámetros que son la instancia del objeto observable sobre la que se ha producido el evento y un Object a modo de argumento que el objeto observable envía. La clase Observable contiene métodos para añadir y eliminar observadores que queremos que sean notificados, obtener un contador con el número de observadores y unos métodos para conocer y establecer si un objeto ha cambiado con el método hasChanged.

El patrón Observer proporciona algunas propiedades de los sistemas que se comunican mediante mensajes:

  • No hay que estar monitorizando un objeto en búsqueda de cambios, se hace la notificación cuando un objeto cambia.
  • Permite agregar nuevos observadores para proporcionar otro tipo de funcionalidad sin cambiar el objeto observador.
  • Bajo acoplamiento entre observable y observador, un cambio en el observable u observador no afecta al otro.

Esta funcionalidad es la ofrecida en el propio JDK, los sistemas que permiten comunicar las aplicaciones usando mensajes como RabbitMQ o JMS proporcionan estas características deseables además de otras propiedades como funcionamiento asíncrono.

Implementar el patrón no es muy complicado basta con extender de una clase (Observable) e implementar una interfaz (Observer), sin embargo, tener que extender de la clase Observable puede ser un problema si la clase que queremos observar ya extiende de otra clase, esto es así porque en Java no hay herencia múltiple de varias clases, si es posible implementar varias interfaces. ¿Que podemos en este caso? La solución es usar composición en vez de herencia aunque con composición no tendremos acceso a los métodos clearChanged ni setChanged si nos son necesarios.

Supongamos que tenemos una clase Producto con un precio y queremos emitir un mensaje en la terminal cuando un producto cambie de precio. Para implementar este patrón Producto debería extender de Observable y otra clase hacer que implemente la interfaz Observer, sin embargo, si no queremos o no podemos hacer que nuestra clase extienda de Observable para no limitarnos en nuestra jerarquía de clases o porque ya extiende de otra podemos usar composición, por otro lado, si no queremos registrar el observador en cada instancia de Producto sino observar cualquier instancia que se cree podemos implementar el Observer de forma estática en la clase Producto. El observable ProductoObservable amplia la visibilidad del método setChanged para poder hacer uso de él usando composición, deberemos invocarlo para que los observadores sean notificados. A continuación pondré el código usando composición e implementándolo de forma estática.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package io.github.picodotdev.blogbitix.patronobserver;

import java.math.BigDecimal;
import java.util.Observable;

public class Producto {

    public class PrecioEvent {
        
        private Producto producto;
        private BigDecimal precioAntiguo;
        private BigDecimal precioNuevo;
        
        public PrecioEvent(Producto producto,  BigDecimal precioAntiguo, BigDecimal precioNuevo) {
            this.producto = producto;
            this.precioAntiguo = precioAntiguo;
            this.precioNuevo = precioNuevo;
        }
        
        public Producto getProducto() {
            return producto;
        }
        
        public BigDecimal getPrecioAntiguo() {
            return precioAntiguo;
        }
        
        public BigDecimal getPrecioNuevo() {
            return precioNuevo;
        }        
    }

    private static final ProductoObservable OBSERVABLE;

    private String nombre;
    private BigDecimal precio;

    static {
        OBSERVABLE = new ProductoObservable();
    }

    public static Observable getObservable() {
        return OBSERVABLE;
    }

    public Producto(String nombre, BigDecimal precio) {
        this.nombre = nombre;
        this.precio = precio;
    }

    public String getNombre() {
        return nombre;
    }

    public BigDecimal getPrecio() {
        return precio;
    }

    public void setPrecio(BigDecimal precio) {
        PrecioEvent event = new PrecioEvent(this, this.precio, precio);

        this.precio = precio;

        synchronized (OBSERVABLE) {
            OBSERVABLE.setChanged();
            OBSERVABLE.notifyObservers(event);            
        }
    }
    
    private static class ProductoObservable extends Observable {
        @Override
        public synchronized void setChanged() {
            super.setChanged();
        }
    }
}
Producto.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package io.github.picodotdev.blogbitix.patronobserver;

import java.util.Observable;
import java.util.Observer;

import io.github.picodotdev.blogbitix.patronobserver.Producto.PrecioEvent;

public class ProductoObserver implements Observer {

    @Override
    @SuppressWarnings("unchecked")
    public void update(Observable observable, Object args) {
        if (args instanceof PrecioEvent) {
            PrecioEvent evento = (PrecioEvent) args;
            System.out.printf("El producto %s ha cambiado de precio de %s a %s%n", evento.getProducto().getNombre(), evento.getPrecioAntiguo(), evento.getPrecioNuevo());
        }
    }
}
ProductoObserver.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package io.github.picodotdev.blogbitix.patronobserver;

import java.math.BigDecimal;
import java.util.Observer;

public class Main {
    public static void main(String[] args) {
        Producto p1 = new Producto("Libro", new BigDecimal("3.99"));
        Producto p2 = new Producto("Lector libros electrónico", new BigDecimal("129"));

        Observer o1 = new ProductoObserver();
        Producto.getObservable().addObserver(o1);

        p1.setPrecio(new BigDecimal("4.99"));
        p2.setPrecio(new BigDecimal("119"));
    }
}
Main.java
1
2
El producto Libro ha cambiado de precio de 3,99 a 4,99
El producto Lector libros electrónico ha cambiado de precio de 129 a 119
System.out

Conocer los patrones de diseño, conocer sus beneficios y desventajas y saber cuando aplicarlos probablemente nos sea de provecho en los casos reales que nos encontremos. En el libro Head First Design Patterns explican bastante bien este y otros patrones, este libro lo considero como lectura recomendada junto a otros 8+ libros para mejorar como programadores.

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos alojado en GitHub y probarlo ejecutando el comando ./gradlew run.


Comparte el artículo: