mardi 16 septembre 2008

Programmation événementielle Actes VI)

Le premier argument appelé sender permet à l'abonné de l'événement de connaître précisément l'instance de l'objet qui a émis l'événement. A quoi cela peut-il servir me direz-vous ?

Et bien imaginons un instant que je possède deux instances d'un même objet, Instance1 et Instance2, et que je souhaite m'abonner à l'événement NouveauButPourLOM. Comme dans mon cas le traitement que j'ai à faire dans cet événement côté abonné est identique pour les deux instances, je me dis que cela serait sympa de n'utiliser qu'une seule et même méthode pour traiter les deux événements. Et bien l'argument sender m'a justement me permettre de savoir si l'événement a été émis par Instance1 ou Instance2. A noter que si nous décidions que notre méthode CopieFichier était statique (static), ce premier argument n'aurait pas de sens et ne figurerait pas dans la signature.

Généralement, l'événement émis par une classe à destination d'une autre classe abonnée transporte avec lui un certain nombre d'informations qui pourraient être utiles à l'abonné. On pourrait être tenté de créer autant de paramètres à la signature de notre Delegate/Event que nécessaire. Ce n'est pas l'approche qui a été choisie par les développeurs du framework .NET.

Lorsqu'on souhaite déclarer un évenement qui n'a aucun paramètre particulier à transmettre, on utilise comme deuxième paramètre du Delegate/Event la classe System.EventArgs. Voici ce que la documentation indique sur cette classe :

EventArgs est la classe de base des classes contenant des données d'événement.Cette classe ne contient pas de données d'événement; elle est utilisée par des événements qui ne passent pas d'informations d'état à un gestionnaire d'événements lorsqu'un événement est déclenché. Si le gestionnaire d'événements nécessite des informations d'état, l'application doit dériver une classe à partir de cette classe pour contenir les données.

Et voilà tout est dit, dès qu'on veut passer un certain nombre d'informations à un Delegate/Event, on écrit une classe qui dérive de la classe EventArgs à laquelle on ajoute les informations supplémentaires. Vous avez noté que le nom de ces classes se terminent généralement par EventArgs.

Pour l'événement CopieDemarrage, ce dont on a besoin est simplement la taille du fichier à copier. Ainsi cela permettra d'être notifié de l'imminence du démarrage de la copie et de pouvoir ajuster les valeurs de notre gauge de progression en fonction de la taille du fichier à copier. De plus, il pourrait être utile de pouvoir récupérer les chemins complets des fichiers source et destination. Cela nous donne :

namespace Librairie_CS {

 public class CopieDemarrageEventArgs : System.EventArgs {

  private string fichierSource = string.Empty;
  public string FichierSource {

   get {

    return(this.fichierSource);
   }
  }

  private string fichierDestination = string.Empty;
  public string FichierDestination {

   get {

    return(this.fichierDestination);
   }
  }

  private long tailleFichier = 0;
  public long TailleFichier {

   get {

    return(this.tailleFichier);
   }
  }

  public CopieDemarrageEventArgs(string fichierSource, string fichierDestination, long tailleFichier) {

   this.fichierSource = fichierSource;
   this.fichierDestination = fichierDestination;
   this.tailleFichier = tailleFichier;
  }
 }
}

Pour l'événement CopieFin, ce dont on a besoin est simplement le statut final de la copie. On sait que la copie peut s'achever de trois manières différentes : Succes, Echec ou encore Annulation (par l'utilisateur). C'est un cas idéal de création d'une énumération :

namespace Librairie_CS {

 public enum StatutCopie {

  Aucun,
  Succes,
  Echec,
  Annulation
 }
}

De plus, il peut être utile d'indiquer dans cet événement en combien de temps s'est déroulé l'opération de copie. Cela nous donne finalement ceci:

namespace Librairie_CS {

 public class CopieFinEventArgs : System.EventArgs {

  private StatutCopie statutCopie = StatutCopie.Aucun;
  public StatutCopie StatutCopie {

   get {

    return(this.statutCopie);
   }
  }

  private int duree = 0;
  public int Duree {

   get {

    return(this.duree);
   }
  }

  public CopieFinEventArgs(StatutCopie statutCopie, int duree) {

   this.statutCopie = statutCopie;
   this.duree = duree;
  }
 }
}

Finalement, pour CopieEnCours, il faudrait fournir le nombre d'octets déjà copiés au moment de l'émission de l'événement. Aucune difficulté particulière ici. Par contre, on souhaite également donner la possibilité à l'abonné de pouvoir annuler la copie à tout le moment. L'utilisation de cet événement pour permettre l'annulation de l'opération est alors l'idéal car appelé très régulièrement durant le processus de copie. Voici ce que cela donne :

namespace Librairie_CS {

 public class CopieEnCoursEventArgs : System.EventArgs {
 
  private long position = 0;
  public long Position {

   get {

    return(this.position);
   }
  }

  private bool cancel = false;
  public bool Cancel {

   get {

    return(this.cancel);
   }
   set {

    this.cancel = value;
   }
  }

  public CopieEnCoursEventArgs(long position) {

   this.position = position;
  }
 }
}

Notez que dans ce cas, la propriété Cancel doit être en lecture ET écriture pour donner une chance à l'abonné de pouvoir modifier sa valeur.

A ce niveau là, on a fait le plus dur (et ce n'était pas bien dur, il faut bien l'avouer ;-) ). Il nous reste à modifier le code de la fonction CopieFichier pour déclencher l'émission des événements comme il faut. Cela donne ceci :

public void CopieFichier(string cheminFichierDestination, bool ecrasement) {

 // On vérifie si le fichier de destination n'existe pas déjà
 if (File.Exists(cheminFichierDestination)) {

  // Si demandé, on écrase le fichier de destination
  if (ecrasement) {

   File.Delete(cheminFichierDestination);
  }
  else {

   throw new IOException(string.Format("Le fichier de destination '{0}' existe déjà.", cheminFichierDestination));
  }
 }

 StatutCopie statutCopie = StatutCopie.Aucun;
 int dureeCopie = System.Environment.TickCount;

 try {

  statutCopie = StatutCopie.Succes;

  // On ouvre le fichier de destination en écriture
  using (FileStream fileStreamDestination = new FileStream(cheminFichierDestination, FileMode.Create, FileAccess.Write)) {

   // On ouvre le fichier d'origine en lecture
   using (FileStream fileStreamOriginal = new FileStream(this.cheminFichier, FileMode.Open, FileAccess.Read)) {

    // On informe l'abonné de l'imminence de la copie
    if (CopieDemarrage != null) {

     CopieDemarrage(this, new CopieDemarrageEventArgs(this.cheminFichier, cheminFichierDestination, fileStreamOriginal.Length));
    }

    // On lit le prochain octet
    int octet = fileStreamOriginal.ReadByte();
    while (octet != -1) {

     // On écrit l'octet
     fileStreamDestination.WriteByte((byte)octet);

     // On informe l'abonné de la progression de la copie
     if (CopieEnCours != null) {

      CopieEnCoursEventArgs copieEnCoursEventArgs = new CopieEnCoursEventArgs(fileStreamOriginal.Position);
      CopieEnCours(this, copieEnCoursEventArgs);

      // L'utilisateur a-t'il demandé l'annulation de la copie ?
      
if (copieEnCoursEventArgs.Cancel) {

       statutCopie = StatutCopie.Annulation;
       break;
      }
     }

     octet = fileStreamOriginal.ReadByte();
    }

    fileStreamOriginal.Close();
   }
   fileStreamDestination.Close();
  }
 }
 catch {

  // On renvoit telle quelle l'exception qui a lieu,
  
// l'objectif étant simplement de pouvoir indiquer
  
// correctement le statut de la copie
  statutCopie = StatutCopie.Echec;
  throw;
 }
 finally {

  dureeCopie = System.Environment.TickCount - dureeCopie;

  // On informe l'abonné de la fin de la copie
  if (CopieFin != null) {

   CopieFin(this, new CopieFinEventArgs(statutCopie, dureeCopie));
  }
 }
}

Ceci étant fait, il ne nous reste plus qu'à compiler l'ensemble de ce code dans une assembly (Librairie_CS.dll).

Enfin, pour illustrer l'utilisation de ces événements depuis une IHM, nous allons créer un projet de type Application Windows et déposer deux Label, deux TextBox, deux Button et une ProgressBar :

 

Puis voici le code client :

namespace ClientWindows_CS {

 public class Form1 : System.Windows.Forms.Form {

  ...
  

  private Librairie_CS.GestionFichiers gestionFichiers = null;
  private float taille = 0;
  private bool cancel = false;

  private void buttonCopie_Click(object sender, System.EventArgs e) {
  
   gestionFichiers = new Librairie_CS.GestionFichiers(textBoxFichierSource.Text);

   gestionFichiers.CopieDemarrage += new Librairie_CS.CopieDemarrageEventHandler(gestionFichiers_CopieDemarrage);
   gestionFichiers.CopieEnCours += new Librairie_CS.CopieEnCoursEventHandler(gestionFichiers_CopieEnCours);
   gestionFichiers.CopieFin += new Librairie_CS.CopieFinEventHandler(gestionFichiers_CopieFin);

   gestionFichiers.CopieFichier(textBoxFichierDestination.Text, true);
  }

  private void buttonAnnuler_Click(object sender, System.EventArgs e) {

   cancel = true;
   buttonAnnuler.Enabled = false;
   Application.DoEvents();
  }

  private void gestionFichiers_CopieDemarrage(object sender, Librairie_CS.CopieDemarrageEventArgs e) {

   cancel = false;
   taille = Convert.ToSingle(e.TailleFichier);
   progressBar1.Minimum = 0;
   progressBar1.Maximum = 100;
   progressBar1.Value = 0;
   buttonAnnuler.Enabled = true;

   Application.DoEvents();
  }

  private void gestionFichiers_CopieEnCours(object sender, Librairie_CS.CopieEnCoursEventArgs e) {

   e.Cancel = cancel;
   progressBar1.Value = Convert.ToInt32(Convert.ToSingle(e.Position) / taille * 100D);

   Application.DoEvents();
  }

  private void gestionFichiers_CopieFin(object sender, Librairie_CS.CopieFinEventArgs e) {

   progressBar1.Value = 0;
   buttonAnnuler.Enabled = false;
   MessageBox.Show(this, string.Format("Statut : {0} - La durée du traitement a été de {1}", e.StatutCopie.ToString(), (new TimeSpan(0, 0, 0, 0, e.Duree)).ToString()), "Copie finie");
  }

}

Les modèles de conception

Il est courant lors du développement d'un projet, d'utiliser des Modèles de conceptions (Design Patterns), pour adresser certains problèmes relatifs à la modélisation et à l'architecture de l'application. Cependant, la définition de ces modèles de conceptions, est souvent difficile à communiquer avec précision. Un bref retour aux origines et à l'histoire du concept s'impose.

L'origine informatique des modèles de conception est attribuée aux travaux de Christopher Alexander. En tant qu'architecte en bâtiment, Alexander a remarqué l'apparition de problèmes communs relatifs à un contexte donné. D'après Alexander, un modèle de conception possède 3 éléments essentiels ; Problème/solution/Conséquence ; qui permettent à un architecte de rapidement adresser les problèmes d'une manière connue et acceptée. Publié il y a 35 ans pour la première fois, A Pattern Language: Towns, Buildings, Construction (Alexander et al, Oxford University Press, 1977), il introduit plus de 250 Modèles de conception Architecturaux et fournit les bases pour l'introduction du concept dans le développement de logiciels.

C'est en 1995, que l'analogie entre les modèles de conception et la construction d'applications se répand largement dans l'industrie du logiciel. Les quatre auteurs Gamma, Helm, Johnson, et Vlissides (connus comme le gang des quatre, ou GoF), ont rapproché dans leurs travaux, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley Pub Co, 1995), les modèles de conception d'Alexander et l'émergence croissante du développement orienté objet.

Basé sur leur expérience collective des frameworks objets existant, le GoF a fournit 23 Modèles de conception qui adressent les problèmes et solutions rencontrés lors de la modélisation d'applications. Après la publication, ce concept n'a cessé d'augmenter en popularité en résolvant de nombreux problèmes rencontrés dans le domaine des logiciels. Due à cette popularité, la notion d'anti-modèles (anti-Pattern) a émergé, et il est communément acquis que les solutions sans modèles sont un frein à l'évolution d'un projet de grande envergure.

 

Pourquoi les modèles de conception ?

Bien qu'il n'y ait rien de magique dans les modèles de conception, ce sont des outils extrêmement puissants pour développeurs ou architectes qui s'embarquent dans le développement de projet. Les modèles de conception garantissent que les problèmes soient adressés par des solutions reconnues et acceptées de tous.

La force fondamentale des modèles reste dans le fait, que la plupart des problèmes ont vraisemblablement été rencontré et résolu par un ensemble d'individus et par la communauté des développeurs. En d'autres termes, les modèles fournissent un mécanisme de partage de solutions qui fonctionnent entre les développeurs et les organisations. Peu importe l'origine des modèles, c'est un formidable levier de l'expérience collective. Ils garantissent, l'implémentation rapide d'un code robuste, et la réduction des erreurs lors de la conception ou de l'implémentation. De plus, les modèles de conceptions offrent une sémantique commune entre chaque membre d'une équipe. Ceux qui ont été impliqués dans un projet un temps soit peu important, savent, qu'avoir un jeu commun de modèles, de syntaxes et de principes de développement, est critique pour l'achèvement du projet. Les modèles de conception, s'ils sont utilisés judicieusement, peuvent faire gagner un temps précieux.

Les modèles du Framework .NET

Les modèles de conception ne sont pas inféodés à un langage ou à une plateforme de développement particulier (bien que le GoF ait limité leurs exemples à du C++ ou du SmallTalk) ; l'arrivée du Framework .NET de Microsoft en est l'illustration parfaite. Elle permet désormais à des développeurs utilisant à l'origine un langage non orienté objet comme Microsoft Visual Basic, de développer selon les principes Objets. Pendant le développement des Librairies de classes du Framework (FCL), Microsoft a appliqué plusieurs modèles introduits par le GoF en 1994. Vu l'ampleur des fonctionnalités exposées par le Framework .NET, Microsoft a également introduit de nouveaux modèles.

Dans cette série d'articles, nous examinerons en détail plusieurs modèles disponibles dans la FCL. La structure générale et les bénéfices de chaque modèle seront étudiés, suivie d'un examen de leur implémentation spécifique dans la FCL. Bien que la plupart des modèles que nous examinerons trouvent leurs origines dans le GoF, le Framework .NET offre un nombre de fonctionnalités innovantes, pour lesquelles il y a très peu ou pas du tout de guide disponible actuellement. Les modèles utilisés pour ces nouvelles fonctionnalités seront également examinés. Nous débutons ici avec le modèle Observateur.

Le modèle Observateur

L'un des principes primordiaux du développement orienté objet, est la bonne répartition des tâches dans une application donnée. Chaque objet du système doit se focaliser sur sa propre implémentation dans le domaine qu'il couvre et rien de plus. Pour faire cours, un objet doit faire une seule chose et la faire bien. Cette approche assure qu'un couplage faible existe entre les objets, permettant une meilleure réutilisation, et un système plus maintenable.

Une des zones où la bonne répartition des tâches est des plus importantes, est l'interaction entre l'interface utilisateur, et la logique métier. Au cours du développement d'une application, il est souvent courant que l'interface utilisateur soit amenée à changer rapidement, sans pour cela impacter le reste de l'application. L'inverse est vrai aussi. Il est probable que les besoins métiers changent sans que cela en affecte l'interface utilisateur. Chacun ayant une expérience dans le développement, sait, que dans la plupart des cas, il y aura des modifications à apporter dans ces deux zones. Sans la séparation de l'IU avec le reste de l'application, la modification d'une portion de l'interface peut impacter l'intégralité de l'application.

Le besoin de séparer l'interface utilisateur et la logique métier, est un problème récurrent dans le développement d'applications. De ce faite, les frameworks orienté objet développés depuis l'apparition des interfaces utilisateur graphique (GUI) ont été construit dans ce sens. Il n'est donc pas surprenant que la plupart ait adopté un modèle similaire pour fournir cette fonctionnalité. Ce modèle est connu sous le nom d'Observateur et permet de faire la distinction précise entre chaque objet du système. Comme la plupart des modèles, son utilisation s'étend au delà des limites de ses intentions primaires et il est assez courant de voir cette solution utilisée dans des segments de l'application qui ne soient pas liés à l'interface utilisateur.

Model logique

Bien qu'il existe un certain nombre de variations du modèle observateur, la base du modèle est constituée de deux acteurs primaires l'observateur et le sujet (Pour ceux familier de Smaltalk et du modèle MVC, reconnaîtront dans ces deux termes, respectivement View et Model).

Dans l'interface utilisateur, l'observateur est l'objet responsable d'afficher les données, alors que le sujet, représente la couche métier.

Comme vous pouvez le constater dans la Figure 1 une association logique existe entre l'observateur et le sujet. Lorsqu'un changement survient dans l'objet sujet (ex : Modification d'une variable d'instance), l'observateur observe et met à jour son affichage en conséquence.

Figure 1. Relation entre l'Observateur et le Sujet

Par exemple, supposons que nous ayons une application simple, qui suive quotidiennement le prix d'une action cotée en bourse. Nous avons d'un coté une classe Stock qui représente différentes actions du NASDAQ. Cette classe contient une variable d'instance, qui représente la fluctuation du prix courrant (la manière dont il change n'est pas importante ici). De l'autre coté, nous avons la classe SockDisplay qui affiche cette information à l'utilisateur, en écrivant sur la sortie standard (Console). Dans cette application, une instance de la classe Stock agit en tant que Sujet et une instance de la classe StockDisplay agit en tant que Observateur. Si le prix change, la variable d'instance change et comme StockDisplayobserveStock, alors l'affichage change.

Avec ce processus on s'assure qu'une frontière existe entre les classes Stock et StockDisplay. Supposons maintenant que les besoins de l'application change, et qu'un formulaire d'affichage soit nécessaire. Il suffit simplement alors de construire une nouvelle classe StockForm qui agira en tant qu'observateur. Pas besoin ici de modifier la classe Stock. D'ailleurs elle n'est même pas au courant qu'une modification a été faite. De plus, si on a besoin d'utiliser une autre source pour retrouver le prix (Par exemple un service Web plutôt qu'une Base de données), la classe StockDisplay ne change pas elle continue à observer ignorant tous changements.


Modèle physique

Comme dans la plupart des solutions, c'est dans les détails que les difficultés surviennent. Il n'y a pas d'exception avec le modèle Observateur. En effet, bien qu'il soit fait état dans le modèle logique que l'observateur observe le sujet, lorsqu'on passe à l'implémentation cette logique ne calque pas tout à fait à la réalité. Plus exactement, l'observateur doit s'inscrire au près du sujet, en précisant qu'il va observer. Lorsqu'un changement survient, le sujet notifie l'observateur. Lorsque l'observateur ne souhaite plus observer il se désinscrit du sujet. Ces étapes sont connues respectivement comme inscription, notification, et désinscription.

La plupart des framework implémente ces mécanismes via des fonctions de rappel (callbacks). Le diagramme UML des Figures 2, 3, et 4 montre l'utilisation de cette approche.

Pour ceux qui ne seraient pas familier avec ce type de diagramme, les rectangles représentent les objets tandis que les flèches représentent les appels de méthodes.

Figure 2. Enregistrement de l'Observateur

Dans la Figure 2, l'observateur invoque la méthode Register du sujet, ce passant lui-même en tant qu'argument (pour le rappel). Une fois que le sujet reçoit cette référence, il la stocke pour pouvoir notifier l'observateur lorsqu'un changement survient. Plutôt que de stocker la référence à l'observateur directement dans une variable d'instance, la plupart des implémentations du modèle observateur, délèguent cette mission à un objet distinct, le Conteneur (Container). Nous verrons plus tard les bénéfices qu'apporte l'utilisation de ce conteneur. L'action suivante, est le stockage de la référence de l'observateur à l'aide de la méthode Add du conteneur.

Figure 3. Notification à l'Observateur

Dans la Figure 3 on peut noter, que quand un changement apparaît (AskPriceChanged), le sujet retrouve tous les observateurs en invoquant la méthode GetObservers du conteneur. Ensuite, le sujet notifie les observateurs qu'un changement est intervenu, en appelant la méthode Notify de chaque Observateur.

Figure 4. Désenregistrement de l'Observateur

Figure 4 la séquence de désenregistrement est exécutée lorsque l'observateur n'a plus besoin d'observer le sujet. Un appel à la méthode UnRegister est faite qui délègue le travail au conteneur via la méthode Remove.

Revenons à notre application, et examinons les impacts de ce processus d'inscription et désinscription. Lors du démarrage de l'application, une instance de la classe StockDiplay s'inscrit auprès d'une instance de la classe Stock, ce passant elle-même en tant qu'argument à la méthode Register. L'instance de la classe Stock maintient une référence d'une instance de la classe StockDisplay (dans le conteneur). Lorsque le prix change, Stock notifie StockDisplay du changement, en appelant la méthode Notify. Lorsque l'application se termine, le processus inverse doit s'effectuer. StockDisplay invoque la méthode UnRegister de Stock et la relation entre les deux se termine ainsi. Examinons maintenant les bénéfices d'utilisation du Conteneur, plutôt que d'une variable d'instance pour le stockage d'une référence à l'observateur. Imaginons que nous ayons besoin en plus de StockDisplay, d'un graphique en temps réel de la fluctuation des prix. Pour cela, nous allons créer une nouvelle classe StochGraph qui affiche le prix sur l'axe des x et les heures sur l'axe des y. Lorsque l'application démarre elle inscrit les instances des classes StockDisplay et StockGraph avec une seule instance de la classe Stock. Puisque le sujet stocke les observateurs dans un conteneur, lorsque le prix change, l'instance de stock peut notifier simplement les deux observateurs ou un nombre infini d'observateur en opposition à un nombre fini si on devait passer par des variables d'instances. On ne sait jamais à l'avance combien d'observateurs vont venir s'inscrire. En utilisant un conteneur on ajoute de la flexibilité à notre solution.

Bien que cela ne soit pas obligatoire, la plupart des frameworks fournissent un jeu d'interfaces que doivent implémenter les observateurs et les sujets. Dans nos exemples C# et VB.NET ci-dessus, l'interface IObserver expose une méthode publique Notify. Cette interfacedoit être implémentée par toutes les classes qui ont l'intention d'agir en tant qu'observateur. L'interface IObservable qui expose les méthodes Register et UnRegister, doit quand à elle être implémentée par toutes les classes qui souhaitent agir en tant que sujet. Ces interfaces prennent généralement la forme de classe abstraite virtuelle ou true interfaces si le langage que vous utilisez gère cette dernière option.

L'utilisation de ces interfaces IObserver et IObservable, introduit un couplage faible entre l'observateur et le sujet et permettent des opérations indépendantes de l'implémentation.

Interfaces IObserver et IObservable (C#)

//Toutes les classes observateurs doivent implémenter ces interfaces
public interface IObserver {
 
 void Notify(object anObject);
 
}//IObserver
 
public interface IObservable {
 
 void Register(IObserver anObserver);
 void UnRegister(IObserver anObserver);
 
}//IObservable

Retournons à notre exemple d'application. Nous savons que la classe Stock agit en tant que sujet, elle doit donc implémenter l'interface IObservable, alors que la classe StockDisplay doit implémenter l'interface IObserver.

Puisque toutes les opérations sont définies par l'interface, plutôt que par une classe spécifique, la classe Stock n'est pas liée à la classe StockDisplay et vice versa. Cela nous permet de changer rapidement l'implémentation de l'observateur ou du sujet sans impacter le reste de l'application (Remplacement ou ajout d'un observateur différent à StockDisplay).

Il n'est pas rare de voir également une classe de base commune dont les sujets dériveront. L'utilisation de cette classe de base réduit l'effort requis pour implémenter le modèle observateur. C'est elle qui implémente l'interface IObservable et fournit l'infrastructure requise pour le stockage des instances des observateurs. L'exemple en C# et VB.NET ci-dessous de la classe ObservableImpl esquisse ce que pourrait être cette classe de base. On remarquera dans les méthodes Register et UnRegister qu'elle délègue le stockage de l'observateur à une table de hachage (HashTable). Bien évidement n'importe quel conteneur peut convenir (Par soucis de simplicité, nous utilisons ici une table de hachage comme conteneur). A noter également la méthode NotifyObservers qui est utilisée pour notifier tous les observateurs stockés dans la table de hachage. Lorsque cette méthode est appelée, elle parcourt la table de hachage et invoque la méthode Notify de chaque observateur.

Classe ObservableImpl (C#)
//Classe helper qui implémente les interfaces IObservable
protected Hashtable _observerContainer=new Hashtable();
public class ObservableImpl:IObservable {
 
 // Conteneur pour stocker les instances des observateurs
 protected Hashtable _observerContainer=new Hashtable();
 
 //inscrit l'observateur
 public void Register(IObserver anObserver){
 _observerContainer.Add(anObserver,anObserver); 
 
 }//Register
 
 //des-inscrit l'observateur
 public void UnRegister(IObserver anObserver){
 _observerContainer.Remove(anObserver); 
 }//UnRegister
 
 //Méthode commune pour notifier tous les observateurs
 public void NotifyObservers(object anObject) { 
 
 //Parcours les observateurs et invoque leur 
méthode Notify
 foreach(IObserver anObserver in 
_observerContainer.Keys) { 
 
 anObserver.Notify(anObject); 
 
 }//foreach
 
 }//NotifyObservers
 
}//ObservableImpl

Plutôt que de fournir sa propre implémentation de IObservable, la classe Stock bénéficie de cette infrastructure en dérivant de la classe ObservableImpl. Puisque la classe ObservableImpl implémente l'interface IObservable, aucune modification n'est requise dans la classe StockDisplay. Cette approche simplifie réellement l'implémentation du modèle Observateur, permettant à de multiples sujets de réutiliser la même fonctionnalité tout en maintenant un couplage faible entre les classes impliquées.

L'exemple C#  d'observateur ci-dessous montre l'utilisation des interfaces IObservable et IObserver et de la classe ObservableImpl.

Cet exemple utilise également MainClass  pour associer un Observateur à un sujet et modifier la propriété AskPrice d'une instance de la classe Stock. Cette propriété est responsable d'invoquer la méthode NotifyObservers de la classe de base.

.
Exemple d'Observateur (C#)
//Classe stock
public class Stock:ObservableImpl {
 
 // Variable d'instance pour le prix
 object _askPrice;
 
 //Propriété
 public object AskPrice {
 
 set { _askPrice=value;
 
base.NotifyObservers(_askPrice);
 
}//set
 
 }//PropriétéAskPrice
 
}//Stock
 
//Représente l'interface utilisateur
public class StockDisplay:IObserver {
 
 public void Notify(object anObject){ 
 Console.WriteLine("le nouveau prix est:" 
+ anObject); 
 }//Notify
 
}//StockDisplay
 
public class MainClass{
 
 public static void Main() {
 
 //Création d'instances
 StockDisplay stockDisplay=new 
StockDisplay();
 Stock stock=new Stock();
 
 //Inscription de la grille
 stock.Register(stockDisplay);
 
 //Boucle 100 fois pour modifier le prix
 for(int looper=0;looper < 100;looper++) 
{
 stock.AskPrice=looper;
 }
 
 //Désinscription
 stock.UnRegister(stockDisplay);
 
 }//Main
 
}//MainClass


--
Alain Lompo
Excelta - Conseils et services informatiques
MCT
MCSD For Microsoft .Net
MVP Windows Systems Server / Biztalk Server
Certifié ITIL et Microsoft Biztalk Server

Aucun commentaire: