Patrón Observer (II) – Métodos Pull y Push para el intercambio de información entre sujeto y observador

Seguimos con el ejemplo donde aplicabamos el patrón de diseño Observer a un código que realizaba la autenticación de usuarios. En este ejemplo vimos que la manera de desacoplar los componentes era mediante el uso de este patrón.

En el ejemplo, los componentes observadores reciben la información necesaria para realizar su labor cuando reciben la notificación junto con esta en forma de parámetros de entrada del método update. Cuando junto con la notificación, los observadores reciben la información de estado del sujeto, aquella que necesitan para llevar a cabo su tarea, a esta forma de comunicación de la información se la denomina «Push», dado que el sujeto «empuja» la información de su estado junto con la propia notificación.

El mayor inconveniente que puede tener el método push, tal y como lo hemos implementado, para hacer llegar la información de estado desde el sujeto a los observadores que la requieran es que no es flexible ante el cambio. Si las especificaciones futuras hicieran cambiar la información de estado, por ejemplo, ampliandola para informar adicionalmente sobre el password introducido, número de intentos de autenticación realizados, etc. ello implicaria modificar la firma del método update() en la interfaz Observer, lo que provocaría la rotura del código de todos los componentes observadores actualmente implementados, puesto que la firma de la implementación de sus métodos update() ya no coincidiría con la firma tal como quedara redefinida en la interfaz.

Método Pull

Ante esto, existe la posibilidad de no comunicar la información de estado del sujeto junto con la notificación, sino que la notificación simplemente sirva para indicar al observador que ahora es el momento de pedirle la información de estado al sujeto porque acaba de realizar la acción. Es entonces el observador, quien mediante una referencia al sujeto, invocará los métodos necesarios para recuperar la información de estado del sujeto. El observador «estira» de la información por eso el método se denomina Pull.

En el método Pull el sujeto debe de proporcionar una interfaz (una serie de métodos) para que los observadores puedan obtener la información de estado que necesitan. En el ejemplo, esto mismo es llevado a cabo mediante la creación de dos «getters» getUser() y isSuccess() de ambito público. El observador, mediante un referencia al sujeto, puede invocar estos métodos y «estirar» de la información.

El observador necesita contar con una referencia al sujeto al cual está observando que pueda untilizar dentro de su implementación del método update(). Existen varias aproximaciones para que el observador cuente con esa referencia. Podemos bien pasársela en el constructor, o bien crear un setter para establecerla, o como última posiblidad que se reciba como un parámetro del propio método update() así: void update(Subject source). Hacerlo en el constructor es muy rígido porque implica que el obervador no puede cambiar de sujeto durante toda su vida. Hacerlo mediante un setter si permite cambiar de sujeto, pero como ya veremos más adelante, implica que vamos a observar un solo sujeto a la vez. Por último, si decidimos que la referencia al sujeto nos llegue como parámetro de entrada en el método update, podremos registrar el componente para que observe varios sujetos al mismo tiempo ya que gracias a este parámetro se podrá identificar que instacia de sujeto concretamente está notificando. En el ejemplo solo existe un objeto sujeto, la instancia de AuthenticationManager, por tanto tiene sentido definir un setter.
Este setter (implementado de una forma perfecta y realista) deberia comprobar si el componente ya está actualmente observando otro sujeto y desregistrarse de su lista de observadores y a continuación registrarse para observar el nuevo sujeto.

public void setSubject(Subject subject) {
	if(this.subject != null) this.subject.unregister(this);
	this.subject = subject;
	subject.register(this);
}

Dentro del método update el inconveniente que se nos presenta es que no podemos obtener la información de estado del sujeto a no ser que convirtamos con éxito la referencia de tipo Subject más abstracta a una más concreta del tipo concreto del sujeto AuthenticationManager.
Para asegurarnos de que el downcasting tiene exito podemos comprobar si la referencia almacenada dentro el campo subject es de tipo AutheticationManager haciendo uso del operador instanceof de java.

if (subject instanceof AuthenticationManager) {
	AuthenticationManager authMng = (AuthenticationManager) subject;
        
        //Aqui código que recupera informacion de estado del sujeto concreto
}

Este es el mayor inconveniente que presenta el método pull, a parte de obligar al observador a solicitar la información de estado que necesita, este debe conocer el tipo concreto del sujeto o tratar de averiguarlo, con lo cual estará acoplado con una implementación concreta, rompiendo el principio de diseño de programar para una interfaz y no para una implementación.

Método Push

Por otra parte tenemos en método push, que ya hemos comentado basa su estrategia en propagar la información de estado junto con la notificación. El problema como hemos apuntado es que la información de estado en si puede ser motivo de cambio en futuras ampliaciones, lo que conlleva cambios en la firma del método update. No obstante podemos solventar este problema si encapsulamos toda la información de estado que vamos a propagar en una instancia de un objeto diseñado precisamente para encapsular toda esta información de estado. Con esto, la firma del método update solamente necesita contar con un parámetro de este tipo. En el ejemplo este papel lo desempeña la interfaz NotificationInfo.

public interface NotificationInfo {	
	String getUser();	
	boolean isSuccess(); 
}

Normalmente y en previsión de que un observador puede observar más de una instancia de tipo sujeto al mismo tiempo, se establece otro parámetro en la firma del método update de la interfaz Observer para, en caso de estar observando varios sujetos, poder identificar de que sujeto se trata, en el caso de que debamos interaccionar con él. Quedando, por tanto, la interfaz Observer de la siguiente manera:

public interface Observer {	
	void update(Subject source, NotificationInfo state);
}

El código que implementa el método update en una clase observador puede acceder a la información de estado, en el ejemplo el usuario y el resultado de la validación mediante el parámetro state.

En definitiva, si en lugar de colocar cada pieza de información asociada como un parámetro independiente en la lista de parámetros del método update los encapsulamos en un nuevo tipo de objeto que cuente con una interfaz para acceder a todos ellos, no será necesario modificar nada en el código que ya tuvieramos desarrollado si posteriormente hubiera que añadir más información de estado, como sucedia con el método pull. Pero, al contrario que en el método pull, el observador no está obligado a conocer y trabajar con referencias del tipo concreto de sujeto. Por este motivo, casi todos los sistemas utilizan esta aproximación basada en push más un objeto que encapsula la información de estado.

Aqui teneis el video donde podeis ver el desarrollo de ambos ejemplos.

Patrón Observer – Ejemplo de aplicación del patrón en Java

El patrón de diseño Observer se caracteriza principalmente por la reducción en el acoplamiento entre distintos componentes de una aplicación. Si nos encontramos en una situación en la que uno de estos componentes se acopla con el resto de ellos, como en el ejemplo de código que vamos a ver, siguiendo el patrón de diseño Observer daremos a este componente el rol de sujeto, el resto serán observadores.

La ventaja que proporciona el uso del patrón observador es que el código de la clase que hace de sujeto, que es la que tiene la información de cuando y por qué se deben hacer ciertas «tareas», quedará liberada de llamar directamente a los componentes que realizan estas «tareas». Dado que, llamarlos directamente implica conocer el tipo concreto de estos componentes y su interfaz concreta, se produce el acoplamiento con estos.

En lugar de esto, la clase sujeto notificará a los componentes cuando lo estime conveniente porque se da el motivo para ello. El patrón Obervador facilita que la notificación se produzca a través de una interfaz bien definida y común a todos estos componentes que se va a notificar. Esta interfaz común viene dada mediante la interfaz Observer que cuenta con un método que se suele denominar update().

Es tarea del sujeto mantener una lista de objetos interesados en ser notificados, con una serie métodos (register y unregister) para que los componentes observadores se registeren y se den de baja en esta lista. El sujeto pues, ya no necesita conocer el tipo concreto de los componentes, el tipo de las referencias con las que trabaja para notificar a los componentes es del tipo abstracto Observer. Tampoco depende de cuantos componentes estén registrados y si reciben la notificación ni de como la procesan.

En conclusión, todo esto permite que al programa al que aplicamos el patrón observador, podamos añadir nuevos componentes adicionales que amplien la funcionalidad con nuevas tareas en futuras modificaciones del código sin que sea necesario hacer cambios en el código de la clase notificadora o sujeto. Bastará simplemente con instanciar un objeto de este nuevo componente y registrarlo para que observe al sujeto.

En el ejemplo, tenemos una clase para autenticar usuarios, su responsabilidad es comprobar unas credenciales (usuario y contraseña) y validarlas, concluyendo si son correctas o no. Por otro lado, existen tareas adicionales, dependientes de si la validación ha tenido exito o no, son variadas y abarcan desde la carga de archivos del perfil, el registro de auditoria, la monitorización de ataques, etc.

Si todo esto lo hacemos en el código de la clase de autenticación estamos rompiendo el principio de responsabilidad única, y lo peor, estamos creando una dependencia en esta clase por cada uno de estos componentes que maneja directamente. Ampliaciones en este sistema para que trabaje con un nuevo compomente implica cambios en el código de la clase de autenticación. Los cambios en los componentes existentes tambien pueden afectar al código de la clase de autenticación.

Daremos a la clase que autentica el rol de sujeto, porque es la que tiene conocimiento de CUANDO suceden la acción que da lugar a que sea necesario escribir una entrada en el registro de eventos, cargar perfiles, activar la monitización de ataques, etc. y el rol de observadores al resto de componentes porque son los que tienen el conocimiento de COMO registrar, cargar, monitorizar, etc. Estos se registran en la lista de observadores del sujeto y este les noficará cuando deben realizar su labor, además de la información adicional necesaria.

Aqui os dejo el video tutorial.

El asunto de la comunicación adicional que el observador necesita para realizar su trabajo no es una cuestión menor y existen varias posibilidades. Estas variantes del patrón Obsever en función de como se facilita la información adicional a los observadores se denominan Push y Pull. En el próximo post analizaremos cada una de ellas para ver sus ventajas e inconvenientes.