Patrón Observador en .NET - pattern Observer
.gif)
Patrón Observador en .NET - pattern Observer
Introducción
El patrón observador (pattern Observer) es uno de los
patrones que más me gustan, motivo principal por el cual me he animado a
escribir esta entrada para que lo muestre de una forma práctica, agradable y
útil para en primer lugar entenderlo, y posteriormente ser implementado en
nuestros desarrollos de .NET.
Espero que esta entrada os ayude a entender mejor el
funcionamiento de este patrón y os brinde la posibilidad de valorarlo por si os
interesa utilizarlo en vuestros desarrollos, o implementar un híbrido que os
resulte útil para abordar determinados problemas con los que os podáis
encontrar durante vuestros desarrollos Software.
Visión general del patrón
Este patrón se define como un patrón de tipo relacional de
uno a muchos.

De esta forma, un subscriptor (también denominado en algunas
ocasiones como observador) es el encargado de subscribirse a un publicador.
El publicador es a su vez el encargado de notificar que ha
sucedido “algo” que requiere la atención de sus subscritores.
En este punto, el publicador estará relacionado con todos
sus subscriptores, y cuando ocurra “algo”, todos los subscriptores se darán por
enterados.
Lógicamente, debe haber un mecanismo que permita agregar un
subscriptor y eliminar al subscriptor, tanto para que reciba notificaciones
como para que deje de recibirlas.
Aquí tampoco se está analizando la casuística de que con
determinados sucesos, el publicador sólo comunique que ha sucedido algo a unos
determinados subscriptores y a otros no. Simplemente vamos a analizar el caso
general de este patrón en el que todos los subscriptores reciben la
comunicación de aviso (notificación) por parte del publicador.
Aproximación 1
Vamos a empezar por lo tanto a preparar nuestra primera
implementación del patrón Observador.
Se trata de una implementación generalista.
Es decir, partiremos de la base de crear una biblioteca de
clases cuyo proyecto denominaré PatternObserver
para crear en él las clases que permitan comprender y utilizar este patrón.
También crearé un proyecto adicional de tipo WinForms que utilizaré para probar la
implementación del patrón y demostrar su funcionamiento en esta primera
aproximación.
PatternObserver
El diagrama general de lo que vamos a crear es lo que se
puede observar en la siguiente imagen:

Aquí podemos observar un proyecto PatternObserver que
contendrá las siguientes clases:
-
IObserver
-
ISubject
-
Subject
La explicación de cada una de estas clases es la siguiente:
IObserver es la
interfaz que define qué debe notificarse a cada uno de los subscriptores u
observadores para indicarles que ha sucedido “algo” y que requiere de su atención.
Más adelante veremos cómo funciona esto realmente, pero lo que tenemos que
tener en cuenta es que contiene un método llamado UpdateState() que se
encarga de actualizar ese estado o hecho.
ISubject es la
interfaz que define las acciones que pueden acometerse con respecto a cada uno
de sus observadores. Es decir, conoce a sus observadores y le permite
subscribirse, desubscribirse, y notificar a todos y cada uno de sus
subscriptores la notificación correspondiente (en nuestro caso, llamar a UpdateState()).
Subject es para
esta primera aproximación una clase que implementa ISubject y que es
consumida por la aplicación de ejemplo y demostración del funcionamiento y uso
de este patrón. Como vemos, para esta primera aproximación he querido crear un
proyecto que contenga esta clase también como clase que contenga esta
funcionalidad y que muestre como generarla a partir de ISubject, algo que
haremos en la segunda aproximación para mostrar las dos opciones generales.
El diagrama general de clases del proyecto es el que se
indica a continuación:

El código de nuestras clases es el que se indica a
continuación:
IObserver
namespace PatternObserver
{
/// <summary>
/// Interface del patrón Observer.
/// </summary>
public interface IObserver
{
/// <summary>
/// Método encargado de indicar que el estado del proceso debe actualizarse para que
/// indique a los observadores "algo".
/// </summary>
/// <param name="sender">Indicamos la interfaz a quién se le envía la notificación.</param>
void UpdateState(ISubject sender);
} // IObserver
} // PatternObserver
ISubject
namespace PatternObserver
{
/// <summary>
/// Interface del patrón Observer.
/// </summary>
public interface ISubject
{
/// <summary>
/// Método encargado de notificar a todos y cada uno de los observadores que ha
/// sucedido "algo".
/// Esto se realiza recorriendo todos los observadores subscritos y ejecutando por
/// cada uno de ellos el método UpdateState() implementado de IObserver.
/// </summary>
void Notify();
/// <summary>
/// Método encargado de subscribir un observador para que reciba las notificaciones.
/// </summary>
/// <param name="observer">Interfaz IObserver que indica el observador.</param>
void Subscribe(IObserver observer);
/// <summary>
/// Método encargado de desubscribir un observador para que no reciba más
/// notificaciones.
/// </summary>
/// <param name="observer">Interfaz IObserver que indica el observador.</param>
void Unsubscribe(IObserver observer);
} // ISubject
} // PatternObserver
Subject
namespace PatternObserver
{
using System.Collections.Generic;
/// <summary>
/// Clase encargada de implementar la interfaz ISubject para permitir registrar un
/// observador, eliminar el registro de un observador y notificarle que ha sucedido "algo".
/// </summary>
public class Subject : ISubject
{
#region CONSTRUCTORS
/// <summary>
/// Constructor de la clase.
/// </summary>
public Subject()
{
// Instanciamos la colección de observadores.
this.Observers = new List<object>();
} // Subject Constructor
#endregion
#region PROPERTIES
/// <summary>
/// Propiedad privada encargada de contener todos los subscriptores.
/// </summary>
private List<object> Observers { get; set; }
#endregion
#region METHODS
/// <summary>
/// Método encargado de notificar al subscriptor que ha sucedido un evento que
/// requiere su atención.
/// </summary>
public void Notify()
{
// Recorremos cada uno de los observadores para notificarles el evento.
foreach (IObserver observer in this.Observers)
{
// Indicamos a cada uno de los subscriptores la actualización del
// estado (evento) producido.
observer.UpdateState(this);
}
} // Notify
/// <summary>
/// Método encargado de agregar un observador para que el subscriptor le
/// pueda notificar al subscriptor el evento.
/// </summary>
/// <param name="observer">Interfaz IObserver que indica el observador.</param>
public void Subscribe(IObserver observer)
{
// Agregamos el subscriptor a la lista de subscriptores del publicador.
this.Observers.Add(observer);
} // Subscribe
/// <summary>
/// Método encargado de eliminar un observador para que el subscriptor no le
/// notifique ningún evento más al que era su subscriptor.
/// </summary>
/// <param name="observer">Interfaz IObserver que indica el observador.</param>
public void Unsubscribe(IObserver observer)
{
// Eliminamos el subscriptor de la lista de subscriptores del publicador.
this.Observers.Remove(observer);
} // Unsubscribe
#endregion
} // Subject
} // PatternObserver
En este caso, he creado una clase (Subject) que implementa
la interfaz ISubject y que tiene varias partes diferenciadoras.
En primer lugar he creado una propiedad (Observers)
que contendrá una lista de todos y cada uno de los observadores. Lógicamente,
habrá que crear la vía o forma de registrar y eliminar del registro a un
observador determinado. Esto lo haremos de acuerdo a la implementación de los
métodos de la interfaz.
Por lo tanto, en la implementación de la interfaz
prepararemos los tres métodos de la misma. El método Subscribe se encarga de
registrar o insertar en la lista de observadores a un observador concreto,
mientras que el método Unsubscribe se encarga de eliminar
de la lista de observadores al observador en cuestión.
Indudablemente, tendremos que trabajar en el método Notify
de una manera particular, y es que dentro de este método tendremos la
obligación de recorrer todos y cada uno de los observadores registrados y
llamar al método UpdateState() que se encargará de realizar la acción concreta
que tenga que hacer y que implementaremos y veremos más adelante.
PatternObserverProject
Sobre el proyecto de aplicación Windows de ejemplo (PatternObserverProject),
este está formado por las siguientes clases:
-
MainForm
-
SampleObserver
La explicación de cada una de estas clases es la siguiente:
MainForm es la
clase formulario con diferentes controles de usuario y las acciones generales
que forman parte de la demostración de uso de este patrón.
SampleObserver es
la clase que implementa la interfaz IObserver y determina qué acción o
acciones realiza en realidad UpdateState().
El diagrama general de clases del proyecto es el que se
indica a continuación:

Para esta primera aproximación, he preparado una interfaz de
usuario similar a la que se indica en la siguiente imagen:

Este ejemplo tiene tres botones que se encargan cada uno de
ellos de añadir un observador a la lista de observadores. Al hacer clic sobre
uno de estos botones se deshabilitará su control Button y se habilitará el homónimo pero para eliminar el observador
correspondiente.
Finalmente, el botón de lanzar el evento, es el encargado de
simular que ocurre “algo” y que los observadores deberán darse cuenta de que
han sido informados de que ha pasado algo que requiere su atención.
Como es un ejemplo de demostración, ese “algo” es un mensaje
en pantalla que nos va a indicar que observador es. Si registramos los tres
observadores y pulsamos el botón de lanzar el evento, aparecerán tres mensajes
en pantalla, uno por cada observador.
El código de nuestras clases es el que se indica a
continuación:
MainForm
namespace PatternObserverProject
{
using System;
using System.Windows.Forms;
/// <summary>
/// Clase formulario de ejemplo y demostración del patrón Observer.
/// </summary>
public partial class MainForm : Form
{
#region CONSTRUCTORS
/// <summary>
/// Clase principal del formulario.
/// </summary>
public MainForm()
{
InitializeComponent();
} // MainForm
#endregion
#region EVENTS
/// <summary>
/// Evento MainForm_Load encargado de ejecutarse al cargar el formulario
/// Windows de prueba para el patrón Observer.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MainForm_Load(object sender, EventArgs e)
{
// Instanciamos a PatternObserver.Subject.
this.SubjectDemo = new PatternObserver.Subject();
} // MainForm_Load
/// <summary>
/// Evento encargado de subscribirse al publicador.
/// Este evento simula al primer subscriptor.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnAddFirst_Click(object sender, EventArgs e)
{
this.btnRemoveFirst.Enabled = this.btnAddFirst.Enabled;
this.btnAddFirst.Enabled = !this.btnAddFirst.Enabled;
// Inicializamos la clase Observer correspondiente.
// Simulamos la subscripción a Observer.
this.Observer1 = new SampleObserver("Observador 1");
this.SubjectDemo.Subscribe(this.Observer1);
} // btnAddFirst_Click
/// <summary>
/// Evento encargado de eliminar la subscripción al publicador.
/// Este evento simula al primer subscriptor.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnRemoveFirst_Click(object sender, EventArgs e)
{
this.btnRemoveFirst.Enabled = this.btnAddFirst.Enabled;
this.btnAddFirst.Enabled = !this.btnRemoveFirst.Enabled;
// Simulamos la eliminación de la subscripción a Observer.
this.SubjectDemo.Unsubscribe(this.Observer1);
} // btnRemoveFirst_Click
/// <summary>
/// Evento encargado de subscribirse al publicador.
/// Este evento simula al segundo subscriptor.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnAddSecond_Click(object sender, EventArgs e)
{
this.btnRemoveSecond.Enabled = this.btnAddSecond.Enabled;
this.btnAddSecond.Enabled = !this.btnAddSecond.Enabled;
// Inicializamos la clase Observer correspondiente.
// Simulamos la subscripción a Observer.
this.Observer2 = new SampleObserver("Observador 2");
this.SubjectDemo.Subscribe(this.Observer2);
} // btnAddSecond_Click
/// <summary>
/// Evento encargado de eliminar la subscripción al publicador.
/// Este evento simula al segundo subscriptor.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnRemoveSecond_Click(object sender, EventArgs e)
{
this.btnRemoveSecond.Enabled = this.btnAddSecond.Enabled;
this.btnAddSecond.Enabled = !this.btnRemoveSecond.Enabled;
// Simulamos la eliminación de la subscripción a Observer.
this.SubjectDemo.Unsubscribe(this.Observer2);
} // btnRemoveSecond_Click
/// <summary>
/// Evento encargado de subscribirse al publicador.
/// Este evento simula al tercer subscriptor.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnAddThird_Click(object sender, EventArgs e)
{
this.btnRemoveThird.Enabled = this.btnAddThird.Enabled;
this.btnAddThird.Enabled = !this.btnAddThird.Enabled;
// Inicializamos la clase Observer correspondiente.
// Simulamos la subscripción a Observer.
this.Observer3 = new SampleObserver("Observador 3");
this.SubjectDemo.Subscribe(this.Observer3);
} // btnAddThird_Click
/// <summary>
/// Evento encargado de eliminar la subscripción al publicador.
/// Este evento simula al tercer subscriptor.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnRemoveThird_Click(object sender, EventArgs e)
{
this.btnRemoveThird.Enabled = this.btnAddThird.Enabled;
this.btnAddThird.Enabled = !this.btnRemoveThird.Enabled;
// Simulamos la eliminación de la subscripción a Observer.
this.SubjectDemo.Unsubscribe(this.Observer3);
} // btnRemoveThird_Click
/// <summary>
/// Evento encargado de simular una llamada al proceso que desencadena que los
/// subscriptores asociados al publicador reciban el evento de que se ha
/// desencadenado la acción.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnExecuteEvent_Click(object sender, EventArgs e)
{
// Lanzamos la notificación del proceso.
this.SubjectDemo.Notify();
} // btnExecuteEvent_Click
#endregion
#region PROPERTIES
/// <summary>
/// Propiedad encargada de instanciar a PatternObserver.Subject.
/// </summary>
private PatternObserver.Subject SubjectDemo { get; set; }
/// <summary>
/// Propiedad que utiliza la clase que implementa a Observer.
/// </summary>
private SampleObserver Observer1 { get; set; }
/// <summary>
/// Propiedad que utiliza la clase que implementa a Observer.
/// </summary>
private SampleObserver Observer2 { get; set; }
/// <summary>
/// Propiedad que utiliza la clase que implementa a Observer.
/// </summary>
private SampleObserver Observer3 { get; set; }
#endregion
} // MainForm
} // PatternObserverProject
SampleObserver
namespace PatternObserverProject
{
/// <summary>
/// Clase que implementa el patrón Observer.
/// </summary>
public class SampleObserver : PatternObserver.IObserver
{
#region CONSTRUCTORS
/// <summary>
/// Constructor de la clase.
/// </summary>
/// <param name="text">Parámetro utilizado para demostrar la implementación
/// correcta del patrón Observer.</param>
public SampleObserver(string text)
{
// Indicamos el valor de la propiedad Text.
this.Text = text;
} // SampleObserver Constructor
#endregion
#region PROPERTIES
/// <summary>
/// Propiedad que es utilizada para saber qué instancia del subscriptor es lanzada
/// en la notificación.
/// </summary>
private string Text { get; set; }
#endregion
#region IObserver Members
/// <summary>
/// Método utilizado para ser llamado desde el notificador ante un determinado
/// evento que ha sucedido en un momento dado y que requiere la atención por
/// todos y cada uno de los observadores o subscriptores.
/// </summary>
/// <param name="sender">Indicamos la interfaz a quién se le envía la notificación.</param>
public void UpdateState(PatternObserver.ISubject sender)
{
System.Windows.Forms.MessageBox.Show(this.Text);
} // UpdateState
#endregion
} // SampleObserver
} // PatternObserverProject
Esta última clase implementa la interfaz IObserver.
De esta manera, implementamos el método UpdateState(). Como
podemos apreciar, este método llama a MessageBox.Show() para mostrar un
mensaje en pantalla con el valor de la propiedad Text.
Como podemos apreciar es un ejemplo muy sencillo.
Si ejecutamos nuestra aplicación, veremos que ésta se
comporta tal y como esperamos. Es decir, hacemos clic en los botones que
queramos para agregar observadores a la lista de observadores, y posteriormente
hacemos clic en el botón de lanzar evento.
Aproximación 2
La segunda aproximación es prácticamente banal una vez vista
la aproximación anterior.
En la aproximación anterior hemos creado las interfaces y la
clase Subject como implementación general para mostrar una forma
particular y global del uso del patrón.
Si queremos implementar nuestra propia aproximación, quizás
deberíamos tirar directamente de la interfaz.
PatternObserver
El diagrama general de lo que vamos a crear es lo que se
puede observar en la siguiente imagen:

Esta imagen muestra claramente lo que comentaba
anteriormente.
El proyecto de biblioteca de clases PatternObserver contiene
únicamente las interfaces IObserver
e ISubject.
El diagrama general de clases del proyecto es el que se
indica a continuación:

El código y las explicaciones que daba anteriormente son las
mismas.
PatternObserverProject
El proyecto de aplicación Windows (PatternObserverProject)
en este caso, estará formado por las siguientes clases:
-
MainForm
-
SampleObserver
-
SampleSubject
La explicación de estas clases también la hemos comentado
anteriormente.
La única cosa a tener en cuenta es que SampleSubject es una clase del proyecto de prueba que implementa a ISubject.
Para hacer este ejemplo un poco diferente, he modificado
levemente esta clase. En lugar de utilizar un List<object> para
registrar los observadores, he utilizado un ArrayList.
El diagrama general de clases del proyecto es el que se
indica a continuación:

Para esta segunda aproximación, he incluido seis controles Button
más a la interfaz de usuario tal y como se indica en la siguiente imagen:

Finalmente, incluiremos el código de nuestras clases:
MainForm
...
/// <summary>
/// Evento encargado de simular una llamada al proceso que desencadena que los
/// subscriptores asociados al publicador reciban el evento de que se ha
/// desencadenado la acción.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnExecuteEvent_Click(object sender, EventArgs e)
{
// Lanzamos la notificación del proceso.
this.SubjectDemo.Notify();
// Lanzamos la notificación del proceso.
this.InternalSubjectDemo.Notify();
} // btnExecuteEvent_Click
...
/// <summary>
/// Evento MainForm_Load encargado de ejecutarse al cargar el formulario
/// Windows de prueba para el patrón Observer.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MainForm_Load(object sender, EventArgs e)
{
// Instanciamos a PatternObserver.Subject.
this.SubjectDemo = new PatternObserver.Subject();
// Instanciamos a SampleSubject que implementa la interfaz ISubject.
this.InternalSubjectDemo = new SampleSubject();
} // MainForm_Load
...
/// <summary>
/// Propiedad que utiliza la clase que implementa a Observer.
/// </summary>
private SampleObserver ObserverInternal1 { get; set; }
/// <summary>
/// Propiedad que utiliza la clase que implementa a Observer.
/// </summary>
private SampleObserver ObserverInternal2 { get; set; }
/// <summary>
/// Propiedad encargada de instanciar la clase SampleSubject que implementa
/// PatternObserver.ISubject.
/// </summary>
private SampleSubject InternalSubjectDemo { get; set; }
...
private void btnAddOne_Click(object sender, EventArgs e)
{
this.btnRemoveOne.Enabled = this.btnAddOne.Enabled;
this.btnAddOne.Enabled = !this.btnAddOne.Enabled;
// Inicializamos la clase Observer correspondiente.
// Simulamos la subscripción a Observer.
this.Observer1 = new SampleObserver("Observador 1 - Internal");
this.InternalSubjectDemo.Subscribe(this.Observer1);
}
private void btnRemoveOne_Click(object sender, EventArgs e)
{
this.btnAddOne.Enabled = this.btnRemoveOne.Enabled;
this.btnRemoveOne.Enabled = !this.btnRemoveOne.Enabled;
// Simulamos la eliminación de la subscripción a Observer.
this.InternalSubjectDemo.Unsubscribe(this.Observer1);
}
private void btnAddTwo_Click(object sender, EventArgs e)
{
this.btnRemoveTwo.Enabled = this.btnAddTwo.Enabled;
this.btnAddTwo.Enabled = !this.btnAddTwo.Enabled;
// Inicializamos la clase Observer correspondiente.
// Simulamos la subscripción a Observer.
this.Observer2 = new SampleObserver("Observador 2 - Internal");
this.InternalSubjectDemo.Subscribe(this.Observer2);
}
private void btnRemoveTwo_Click(object sender, EventArgs e)
{
this.btnAddTwo.Enabled = this.btnRemoveTwo.Enabled;
this.btnRemoveTwo.Enabled = !this.btnRemoveTwo.Enabled;
// Simulamos la eliminación de la subscripción a Observer.
this.InternalSubjectDemo.Unsubscribe(this.Observer2);
}
...
Aquí únicamente indico aquellas partes que se crean y
modifican en esta clase.
SampleSubject
namespace PatternObserverProject
{
using System.Collections;
/// <summary>
/// Clase que implementa en este caso la clase ya preparada denominada Subject.
/// Podríamos implementar ISubject y a partir de ella preparar todos sus métodos y
/// acciones, pero esta clase define como reutilizar una clase ya preparada para el
/// propósito del patrón Observer.
/// </summary>
public class SampleSubject : PatternObserver.ISubject
{
#region CONSTRUCTORS
/// <summary>
/// Constructor de la clase.
/// </summary>
public SampleSubject()
{
// Instanciamos la colección de observadores.
this.Observers = new ArrayList();
} // SampleSubject Constructor
#endregion
#region PROPERTIES
/// <summary>
/// Propiedad privada encargada de contener todos los subscriptores.
/// </summary>
private ArrayList Observers {get; set;}
#endregion
#region ISubject Members
/// <summary>
/// Método encargado de notificar al subscriptor que ha sucedido un evento que
/// requiere su atención.
/// </summary>
public void Notify()
{
// Recorremos cada uno de los observadores para notificarles el evento.
foreach (PatternObserver.IObserver observer in this.Observers)
{
// Indicamos a cada uno de los subscriptores la actualización del
// estado (evento) producido.
observer.UpdateState(this);
}
} // Notify
/// <summary>
/// Método encargado de agregar un observador para que el subscriptor le
/// pueda notificar al subscriptor el evento.
/// </summary>
/// <param name="observer"></param>
public void Subscribe(PatternObserver.IObserver observer)
{
// Agregamos el subscriptor a la lista de subscriptores del publicador.
this.Observers.Add(observer);
} // Subscribe
/// <summary>
/// Método encargado de eliminar un observador para que el subscriptor no le
/// notifique ningún evento más al que era su subscriptor.
/// </summary>
/// <param name="observer"></param>
public void Unsubscribe(PatternObserver.IObserver observer)
{
// Eliminamos el subscriptor de la lista de subscriptores del publicador.
this.Observers.Remove(observer);
} // Unsubscribe
#endregion
} // SampleSubject
} // PatternObserverProject
En este caso, esta clase implementa la interfaz ISubject e implementa sus métodos
correspondientes.
Conclusiones
En esta entrada hemos visto cómo implementar en .NET el
patrón Observador (pattern Observer) y como funciona.
A partir de aquí, es cuestión de analizar si este
patrón nos resulta útil e incluso la posibilidad de utilizar un patrón
Observador modificado que satisfaga nuestras necesidades. Sin embargo, he
creído interesante y útil hablar de este patrón que muchos desconocen u
olvidan.
Descarga del proyecto en Visual Studio 2010 (PatternObserverProject.zip - 31 Kb).