🎃 Grandes descuentos en libros en línea, eformaciones y vídeos*. Código CALABAZA30. Pulse aquí
¡Acceso ilimitado 24/7 a todos nuestros libros y vídeos! Descubra la Biblioteca Online ENI. Pulse aquí

Comunicación entre objetos

El evento: estar a la escucha

Igual que los sistemas operativos basados en eventos, puede ser que sus objetos tengan que escuchar a otros objetos. Por ejemplo, su formulario gráfico estará "atento" a las acciones del ratón para poder reaccionar inmediatamente a las peticiones del usuario. Su formulario no sabe cuándo el usuario va a hacer clic en este o aquel componente. De hecho, será el componente el que le llame para indicarle un cambio de estado.

Un objeto que capta la información la puede enviar a una lista de "clientes", que inicialmente se habrán suscrito a su lista de notificación.

La programación orientada a objetos es muy próxima a la realidad. Puede que sea suscriptor a su revista favorita, como miles de otros lectores. Los periodistas escriben artículos que se agrupan en ediciones del periódico. Este periódico se le envía cada cierto tiempo. En cualquier momento puede cancelar su suscripción o suscribirse a otro.

En programación es igual. Habitualmente se trata de establecer mecanismos de gestión de la lista de suscriptores y de las notificaciones.

El pattern Observador

Para este problema recurrente, el "grupo de los cuatro" ofrece un design pattern para resolverlo.

Como vamos a ver, esta solución contrastada se puede utilizar directamente en C#. Sin embargo, existe un sistema con muy buen rendimiento basado en el uso de tipos específicos, que son los delegate y los event, muy utilizados para gestionar las comunicaciones entre objetos en C#.

Pero volvamos al pattern Observador, que ofrece una solución de codificación basada en una relación entre uno (observado) y varios (observadores), utilizando el acoplamiento más débil posible. A continuación se muestra la representación UML de este tipo de relación.

images/RI07_1.png

La clase Observable y la interfaz IObservador se diseñan al mismo tiempo, cuando se desarrolla un objeto que puede comunicar sus cambios de estado.

La comunicación entre un objeto Observable, que se ha podido desarrollar más tarde que el resto, y un objeto Observador es posible gracias al polimorfismo del objeto Observador que implementa la interfaz IObservador.

De hecho, la clase Observable solo reconoce objetos que implementan la interfaz IObservador (vea los métodos de gestión de los suscriptores: esperan un objeto de tipo IObservador) y por tanto, cuando una clase implementa esta interfaz, se convierte en "compatible" con las notificaciones de la clase Observable.

A continuación se muestra la implementación en C# del lado de la notificación:


    // Interfaz que deberá implementar cualquier clase   
    // que quiera observar la clase... Observable :)  
    interfaz IObservador  
    {  
        void ObservableNotificaSusObservadores(object sender, object arg); 
    }  
  
    // La clase Observable  
    // durante su trabajo, puede notificar cambios  
    // a las clases que la observan.  
  ...

La solución C#: delegate y event

En lenguaje C, la comunicación entre módulos, por ejemplo entre un ejecutable y una DLL, se hace por medio de un puntero de función.

El puntero de función es una ubicación en memoria que contiene la dirección de la función a ejecutar. El sistema operativo permite al programa cambiar una DLL y después recuperar dinámicamente la dirección de la función que exporta a partir de su nombre. Después, gracias a una sintaxis adecuada, el programa llama a la función como si fuera "local".

El problema de esta solución es que un puntero de función no informa ni sobre el nombre, ni sobre el tipo de los argumentos, ni sobre el valor de retorno. Un puntero de función está compuesto por solo cuatro bytes, que es el origen de muchos funcionamientos incorrectos.

En el mundo de C#, el desarrollador dispone de un tipo especial llamado delegate que va a suprimir este defecto tan importante de C yendo mucho más lejos.

Un delegate es un objeto:

  • que define un prototipo de método de tipo static o instancia, con sus argumentos y su tipo de retorno.

  • que contiene una colección de referencias a los métodos compatibles con su prototipo.

  • que se puede pasar como argumento a otro método.

Sintaxis de un delegate:


visibilidad delegate tipoRetorno NombreDelegado([tipo 
argumento nombre argumento,...]);
 

Ejemplo:


using System;  
  
namespace DemoDelegado  
{  
  // Este delegate puede apuntar a cualquier  
  // método que reciba dos enteros como argumento  
  // y devuelva un entero  
  public delegate int Calcular(int x, int y );  
  
  // Esta clase contiene los métodos   
  // "compatibles" con el delegate Calcular   
  public class CalculoBasico  
  {  
    public static int Agregar(int x, int y )  
    { return x + y ; }  
    public static int Sustraer(int x, int y )  
    { return x - y ; }  
  }  
  
  class Program  
  {  
    static void Main(string[] args)  
    {  
      // Creación...

Llamadas síncronas, llamadas asíncronas

Una noción importante afecta al modo de ejecución de un método respecto al programa que lo utiliza. De hecho, determinados métodos pueden necesitar bastante tiempo para ejecutarse. Por supuesto, esta noción de tiempo es subjetiva y depende del contexto de uso. Por ejemplo, en un entorno de oficina, un tiempo de ejecución de medio segundo no se representativo; en el dominio industrial no es lo mismo en absoluto.

El funcionamiento síncrono es el modo de ejecución por defecto. El programa llamador permanece "bloqueado" durante la ejecución del método llamado.

A continuación se muestra la representación UML en forma de diagrama, de la secuencia de una llamada síncrona de una instancia Objeto1 a una instancia Objeto2.

images/RI07_13.png

Si la duración de la ejecución se convierte en crítica, hay que ejecutar el método de manera asíncrona. En este caso, la ejecución se "divide en dos", permitiendo que las dos ramas evolucionen en un modo "pseudo-paralelo".

A continuación se muestra la representación UML en forma de diagrama de secuencia de una llamada asíncrona de una instancia Objeto1 a una instancia Objeto2.

images/RI07_14.png

La programación de ejecuciones paralelas normalmente pasa por una programación multithread, que se presentará en el capítulo El multithreading. Sin embargo, los delegate ofrecen una alternativa sencilla para llamar a los métodos que tiene asociados de manera asíncrona, por lo que podemos aprovecharnos de ello.

En primer lugar, a continuación se muestra una clase que tiene un método que simula una ejecución durante un número definido de segundos gracias al método System.Threading.Thread.Sleep(xxx) que bloquea la ejecución durante un número definido de milisegundos. Este método es el que queremos ejecutar de manera asíncrona para la demonstración.


  class MiClaseTrt  
  {  
    public static string Job(int duracionEnSeg)  
    {  
      Thread.Sleep(duracionEnSeg * 1000);  
      return "He trabajado " + duracionEnSeg + " segundos";  
    }  
  }
 

A continuación...

Ejercicio

El siguiente ejercicio va a permitir manipular los event, descubriendo una clase muy práctica de .NET: System.IO.File.FileSystemWatcher.

Esta clase tiene la particularidad de enviar notificaciones a sus suscriptores cuando se pasan eventos a los archivos en una carpeta específica de una unidad de almacenamiento.

1. Enunciado

Vamos a utilizar FileSystemWatcher para monitorizar la escritura de archivos de extensión .TXT en una carpeta compartida, llamada por defecto C:\temp.

Se mostrará la lista de los archivos en un formulario y la selección de una entrada de la lista mostrará su contenido.

Una vez consultado el archivo, un botón Eliminar permitirá eliminar el archivo.

A continuación se muestra algo parecido al formulario:

images/RI07_16.png

2. Consejos para la realización

 Crear un proyecto de tipo Windows Forms.

 Crear un formulario en el asistente gráfico que contenga una listbox para mostrar los archivos detectados y una textbox multi-línea para mostrar el contenido de los archivos.

 Utilice el evento Load del formulario para instanciar y configurar un objeto FileSystemWatcher.

 Suscriba el formulario al evento Created del objeto FileSystemWatcher.

 Tan pronto como se detecta un archivo, añada su nombre a la lista. Para evitar un problema que se comentará en el siguiente capítulo, utilizará:


this.listBox1.Invoke((MethodInvoker)delegate  
                  {  
                      this.listBox1.Items.Add(e.FullPath);  
                  });
 

 Suscriba el formulario al evento SelectedIndexChanged de la listbox para realizar la visualización del contenido del archivo.

 Lea el archivo en un string con una sencilla llamada al método System.IO.File.ReadAllText.

 Suscriba el formulario al evento Click del botón Eliminar y borre el archivo llamando al método System.IO.File.Delete.

3. Corrección

Parte del código de la clase generada por Visual Studio:


namespace LabFileSystemWatcher  
{  
  partial class Form1  
  {  
    /// <summary>  
    /// Required designer variable.  
    /// </summary>  ...