El patrón de diseño del observador en Java

El patrón de diseño del observador es una solución generalizada para los sistemas controlados por eventos. En este artículo, implementaremos nuestro propio sistema desacoplado basado en eventos en Java.

Introducción

En este artículo, implementaremos el patrón de diseño Observer para resolver un problema común en el desarrollo de software orientado a objetos.

Los patrones de diseño son soluciones estandarizadas para problemas comunes en la industria del desarrollo de software. Al estar familiarizado con ellos, un desarrollador puede reconocer dónde debe implementarse cada uno y cómo ayudaría a resolver un problema de diseño en particular.

La prevención de desastres en el diseño temprano puede ahorrar una gran cantidad de tiempo y costos para un equipo que intenta sacar un producto.

Patrones de diseño de comportamiento

Patrones de diseño de comportamiento provide responsibility assignment between instances of classes. Also, they define types of relationships and communication between objects.

La idea principal es lograr algún comportamiento esperado de una aplicación y crear un diseño flexible al mismo tiempo.

Patrón de diseño de observador

El patrón de diseño del observador es una forma de diseñar un subsistema que permite que muchos objetos respondan automáticamente a los cambios de un objeto en particular que está siendo "observado".

Aborda la descomposición de un ‘Observable’ y ‘Observer’s, o un editor y suscriptores.

Para un objeto ‘Observable’, usamos el término Sujeto. Los objetos que están suscritos a los cambios del Sujeto se denominan Observadores. Un sujeto y los observadores suelen tener una dependencia de uno a muchos.

El patrón de diseño del observador también se conoce como patrón Suscriptor de eventos o Oyente.

Nota: Java tiene una implementación oficial de Observer Design Pattern y es la columna vertebral de JMS (Java Message Service). Por lo general, se usa para crear aplicaciones impulsadas por pares, aunque la implementación oficial no está muy extendida y muchas personas implementan el patrón de acuerdo con sus propios casos de uso.

Motivación

Probablemente el ejemplo más conocido es un detector de botones que realiza una acción al hacer clic en el botón. Este patrón, en general, es bastante común en los componentes de la GUI de Java. Es una forma de reaccionar a los eventos que suceden con objetos visuales.

Como usuario de las redes sociales, es posible que estés siguiendo a algunas personas. Podemos decir que eres un observador del feed de redes sociales de tu amigo (Sujeto de observación) y recibes notificaciones sobre sus nuevas publicaciones y eventos de la vida. Curiosamente, tu amigo también es un Observador de tu feed.

Agreguemos más complejidad y digamos que probablemente tiene varios o incluso cientos de observadores diferentes y pueden reaccionar de manera diferente a sus publicaciones. Es posible que un objeto pueda ser un sujeto de observación y un observador de otro sujeto. Incluso pueden tener esta relación entre ellos.

Como un ejemplo más real, una alarma contra incendios en un centro comercial debe notificar a todas las tiendas que se está produciendo un incendio. Estas tiendas observan la señal de alarma contra incendios y reaccionan a sus cambios.

Como puede ver, el problema está bastante extendido y, a menudo, no es trivial resolverlo con otros diseños.

Implementación

Supongamos que una cadena de tiendas quiere notificar a sus clientes leales sobre una venta en curso. El sistema enviaría un mensaje corto a todos los clientes suscritos cada vez que se activa una venta.

En este caso, nuestra tienda es objeto de observación y nuestros clientes la están observando. Definamos las interfaces Subject y Observer para que nuestros objetos las implementen:

1
2
3
4
5
public interface Subject {
    public void addSubscriber(Observer observer);
    public void removeSubscriber(Observer observer);
    public void notifySubscribers();
}

La interfaz Asunto es bastante sencilla. Proporciona métodos para agregar y eliminar suscriptores/observadores y notificarles de un cambio.

La interfaz Observer es aún más simple:

1
2
3
public interface Observer {
    public void update(String message);
}

Lo único que realmente necesita un ‘Observador’ es saber cuándo hay una actualización de su tema. Su comportamiento basado en esta actualización diferirá entre clases.

Con nuestras interfaces fuera del camino, implementemos la interfaz Subject a través de una tienda:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Store implements Subject {
    private List<Observer> customers = new ArrayList<>();

    @Override
    public void addSubscriber(Observer customer) {
        customers.add(customer);
    }
    @Override
    public void removeSubscriber(Observer customer) {
        customers.remove(customer);
    }
    @Override
    public void notifySubscribers() {
        System.out.println("A new item is on sale! Act fast before it sells out!");
        for(Observer customer: customers) {
            customer.update("Sale!");
        }
    }
}

La tienda contiene una lista de observadores (clientes) e implementa los métodos para agregar y eliminar clientes de la lista.

El método notifySubscribers() simplemente recorre la lista de ellos y les envía una actualización.

Podemos tener tantas implementaciones de Observer como queramos. Es natural que las personas reaccionen de manera diferente a una venta. Es probable que un adicto a las compras salte de alegría, mientras que un cliente pasivo probablemente tome nota de la venta y la recuerde para más adelante.

Avancemos e implementemos estos dos tipos de clientes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class ShopaholicCustomer implements Observer {
    @Override
    public void update(String message) {
        processMessage(message);
    }
    private void processMessage(String message) {
        System.out.println("Shopaholic customer is interested in buying the product on sale!");
        // A complex psychologic response to a sale by a shopaholic
    }
}

public class PassiveCustomer implements Observer {
    @Override
    public void update(String message) {
        System.out.println("Passive customer made note of the sale.");
        // Passive customer does not react to the message too much
    }
}

Y finalmente, echemos un vistazo al patrón de diseño del observador en acción activando una venta en una tienda que está siendo observada por algunos clientes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
    // Initialization
    Subject fashionChainStores = new ChainStores();
    Observer customer1 = new PassiveCustomer();
    Observer customer2 = new ShopaholicCustomer();
    Observer customer3 = new ShopaholicCustomer();

    // Adding two customers to the newsletter
    fashionChainStores.addSubscriber(customer1);
    fashionChainStores.addSubscriber(customer2);

    // Notifying customers (observers)
    fashionChainStores.notifySubscribers();

    // A customer has decided not to continue following the newsletter
    fashionChainStores.removeSubscriber(customer1);

    // customer2 told customer3 that a sale is going on
    fashionChainStores.addSubscriber(customer3);

    // Notifying the updated list of customers
    fashionChainStores.notifySubscribers();
}

Y ejecutar este fragmento de código producirá:

1
2
3
4
5
6
A new item is on sale! Act fast before it sells out!
Passive customer made note of the sale.
Shopaholic customer is interested in buying the product on sale!
A new item is on sale! Act fast before it sells out!
Shopaholic customer is interested in buying the product on sale!
Shopaholic customer is interested in buying the product on sale!

Cambiar el estado de la tienda da como resultado el cambio de estado de los clientes suscritos. Este mismo principio se aplicaría a una alarma contra incendios o a una fuente de noticias. Tan pronto como alguien publica una publicación, todos los observadores son notificados y toman alguna acción según su responsabilidad/interés.

Podemos modificar la lista de observadores de un sujeto en cualquier momento. Además, podemos agregar cualquier implementación de la interfaz Observer. Esto nos brinda la capacidad de construir un sistema robusto basado en eventos que envía actualizaciones a los observadores y actualiza todo el sistema en función de los cambios en un solo objeto en particular.

Ventajas y desventajas

The Observer Design Pattern es una gran contribución al apoyo del Principio de diseño abierto/cerrado. Nos ayuda a construir diseños con alta cohesión pero poco acoplamiento.

En otras palabras, el Observador y el Sujeto tienen una misión estrictamente especificada. El sujeto actualiza un observador con cierta información y no conoce la implementación del observador. Esta característica nos da flexibilidad.

Este patrón nos permite agregar y eliminar Observadores en cualquier momento. No necesita modificar ni Sujeto ni Observador para ello.

Sin embargo, hay un problema en el patrón de diseño del observador.

El orden de las notificaciones no está bajo nuestro control. No hay prioridad entre los suscriptores durante la notificación.

Esto significa que si la ejecución de un observador depende de la ejecución previa de otro observador, no hay garantía de que estos dos se ejecuten en ese orden.

Sin embargo, es valioso entender que un patrón es una descripción de alto nivel de una solución particular. Cuando aplicamos un patrón a dos aplicaciones diferentes, nuestro código será diferente. Podemos ordenar a nuestros Observadores y recibirán una notificación en el orden esperado. Esta no es una característica del patrón de diseño del observador, pero es algo que podemos hacer.

Conclusión

Cuando conoce los patrones de diseño, algunos problemas complejos se pueden reducir a soluciones simples comprobadas.

El patrón de diseño del observador es realmente útil en sistemas controlados por eventos donde muchos objetos pueden depender del estado de otro objeto. Si se implementa de manera deficiente, esto dará como resultado una aplicación acoplada y rígida en la que es realmente difícil probar objetos individualmente y actualizar el código será una molestia.

En este artículo, exploramos cómo podemos resolver este problema y crear una solución flexible y desacoplada que su equipo agradecerá.