mardi 16 septembre 2008

Programmation événementielle Acte VII)

Modèle observateur dans le Framework .NET

Maintenant que nous avons certaines bases sur le modèle observateur, regardons l'utilisation de ce modèle dans le framework .NET. Pour ceux familier avec la FCL, auront sans doute remarqué que les types IObserver, IObservable, ou ObservableImpl ne sont pas présents dans le Framework. La raison première de leur absence vient du fait que la CLR les rend d'une certaine manière obsolète. Bien que vous puissiez utiliser ces constructions dans une application .NET, l'introduction des délégués (delegate) et des évènements (event), fournit un nouveau moyen très puissant pour l'implémentation du modèle Observateur sans être obligé de lui créer des types dédiés. En fait, comme les délégués et les évènements sont des classes de base de la CLR, les fondations de ce modèle se retrouvent incorporées dans le cœur du Framework .NET. La structure de la FCL en est un parfait exemple, car elle utilise le modèle observateur intensivement.

On a beaucoup écrit sur les délégués et les évènements, on ne va donc pas s'y étendre ici. Il suffit de savoir qu'un délégué est l'équivalent orienté objet (et type-safe) d'un pointeur de fonction. Une instance d'un délégué possède une référence à une ou plusieurs méthodes qu'elles soient statiques ou non.

Les évènements sont des constructions spéciales déclarées dans une classe qui exposent l'état des objets au Run time. Un évènement représente une abstraction des méthodes d'inscription, désinscription et de notification que nous venons d'implémenter dans le modèle Observateur. Les évènements utilisent en arrière plan des délégués qui seront invoqués lorsque un évènement sera levé. Pour de plus amples informations sur les évènements et les délégués reportez-vous à l'article suivant An Introduction to Delegates.

Pour rapprocher le modèle Observateur et la CLR, la classe implémentant l'évènement est le sujet. Plus besoin pour la classe sujet d'implémenter l'interface IObservable et ou de dériver de la classe ObservableImpl, il lui suffit d'implémenter un évènement et rien de plus. La création de l'observateur est légèrement plus sophistiquée, mais cependant plus flexible (nous en rediscuterons plus tard). Plutôt que d'implémenter l'interface IObserver et s'inscrire auprès du sujet, un Observateur doit créer une instance spécifique d'un délégué et inscrire ce délégué auprès de l'évènement du sujet. Il doit, sous peine que l'inscription échoue, utiliser un délégué du type spécifié dans la déclaration de l'évènement. Passons directement à  nos exemples qui sont beaucoup plus simples à implémenter qu'il ne parait au premier abord.

A noter dans l'exemple C# et Visual Basic .NET ci-dessous l'absence de classe de base ou interface utilisées par les classes Stock ou StockDisplay pour supporter le Modèle observateur.

Observateur utilisant des délégués et des évènements (C#)
public class Stock {
 
 //Déclaration du délégué pour l'évènement
 public delegate void AskPriceDelegate(object aPrice);
 //Déclaration de l'évènement utilisant le délégué
 public event AskPriceDelegate AskPriceChanged;
 
 //variable prix
 object _askPrice;
 
 //propriété pour le prix
 public object AskPrice {
 
 set { 
 //Modifie le prix
 _askPrice=value; 
 
 //lève l'évènement
 AskPriceChanged(_askPrice); 
 
 }
 
 }//AskPrice property
 
}//Stock class
 
//Représente l'interface utilisateur
public class StockDisplay {
 
 public void AskPriceChanged(object aPrice) {
 Console.Write("Le nouveau prix est :" + aPrice 
+ "\r\n"); }
 
}//StockDispslay class
 
public class MainClass {
 
 public static void Main(){
 
 StockDisplay stockDisplay=new 
StockDisplay();
 Stock stock=new Stock();
 
 //création d'une nouvelle instance du délégué 
et lie le avec la 
 //méthode askpricechanged de l'observateur
 Stock.AskPriceDelegate aDelegate=new
 
Stock.AskPriceDelegate(stockDisplay.AskPriceChanged);
 
 //Ajout du délégué à l'évènement
 stock.AskPriceChanged+=aDelegate;
 
 //boucle 100 fois et modifie le prix
 for(int looper=0;looper < 100;looper++) 
{
 stock.AskPrice=looper;
 }
 
 //Dés inscrit le délégué de l'évènement
 stock.AskPriceChanged-=aDelegate;
 
 }//Main
 
}//MainClass

Une fois familier avec les délégués et les événements leur force deviendra évidente. L'utilisation des délégués et des évènements, réduit par rapport à l'utilisation des interfaces IObserver, IObservable et de la classe ObservableImpl, la quantité de travail à entreprendre pour implémenter ce modèle. La CLR fournit les bases de la gestion du conteneur, ainsi qu'une convention commune d'appel pour s'inscrire, se désinscrire et notifier l'observateur. De plus, l'un des plus grand bénéfice des délégués est leur possibilité intrinsèque d'invoquer n'importe quelle méthode (pour peut qu'elle se conforme à la même signature). N'importe quelle classe qui utilise ces mécanismes peut alors agir en tant qu'observateur indépendamment de l'implémentation d'interfaces spécialisées.

Bien que l'utilisation des interfaces IObserver et IObservable réduise le couplage entre l'observateur et le sujet, l'utilisation des délégués l'élimine complètement.

Le Modèle évènement (Event Pattern)

Basée sur les évènements et les délégués, la FCL utilise intensivement le modèle Observateur. Les concepteurs de la FCL ont pleinement réalisé la puissance de ce modèle, et l'ont appliquée aussi bien aux classes qui gère l'interface utilisateur, que celles qui sont indépendantes de l'interface utilisateur. Le modèle évènement inventé par les concepteurs du Framework, est une variation du modèle Observateur.

Bien qu'il n'y ait pas obligation, Microsoft recommande que toutes les applications et les Frameworks qui utilisent les évènements et les délégués adoptent ce modèle.

Le premier, et peut-être le plus important, est l'utilisation du nom de l'évènement exposé par le sujet. Il doit être évident par lui-même et indiquer sur quoi il agit. Gardez à l'esprit que cette convention (aussi bien que les autres) est de nature subjective. L'intention première étant de fournir de la clarté à ceux qui utilisent l'évènement.

Retournons à notre exemple et examinons l'impacte de cette convention sur la classe Stock. Un nom convenable d'évènement serait comme préfix le nom du champ qui est modifié dans la classe sujet. Comme le nom de ce champ est _askPrice il est raisonnable de penser que le nom pourrait être AskPriceChanged. Il est évident que ce nom est de loin plus descriptif que celui qui dit StateChangedInStockClass. AskPriceChanged adhère donc à notre première convention

La seconde convention du modèle évènement repose également sur le nom du délégué, mais aussi sur sa signature. Le nom du délégué doit être composé du nom de l'évènement (voir 1er convention) avec le mot Handler ajouté. Le modèle appelle le délégué en lui spécifiant deux paramètres. Le 1er est une référence sur l'objet qui lève l'évènement et le second fournis des informations contextuelles à l'observateur. Le nom de 1er paramètre est tout simplement sender de type System.Object. Le faite que l'on passe un System.Object est du au fait que le délégué peut être lié à n'importe quelle méthode de n'importe quelle classe. Le nom du second paramètre est plus simple que le premier c'est e. Il doit être d'un type System.EventArgs ou de tous types dérivés. Bien que le type de retour dépende essentiellement de vos besoins, la plupart des délégués qui implémentent ce modèle ne retourne rien.

Regardons plus en détails le paramètre e. Ce paramètre permet au sujet de passer des informations à l'observateur. Si aucune information n'est nécessaire, une instance de System.EvenetArgs suffit largement. Par contre si vous avez besoins de passer des informations, une classe dérivant de System.EventArgs implémentant des champs supplémentaires, nécessaires au passage de l'information doit être construite. Le nom cette classe doit être représenté, du nom de l'évènement ajouté de EventArgs.

Dans notre classe Stock, le nom du délégué devient alors AskPriceChangedHandler, et le second paramètre devient AskPriceChangedEventArgs.

Puisque nous devons passer le nouveau prix à l'observateur, nous devons créer une classe AskPriceChangedEventArgs qui dérive de System.EventArgs en fournissant l'implémentation du passage d'informations.

Le dernier élément du modèle évènement est le nom et l'utilisation de la méthode de la classe Sujet qui lève l'évènement. Le nom de cette méthode doit être composé du nom de l'évènement avec le préfix On. Elle doit être de plus marquée comme étant protected. Cette convention s'applique seulement aux classes non sellées.

Appliquons cette dernière convention sur notre classe Stock. La classe n'étant pas sellée (sealed en C#, NotInheritable en VB.NET), on ajoute la méthode OnAskPriceChanged qui lève notre évènement.

L'exemple C# et Visual basic .NET montre une vue complète du modèle évènement appliquée à la classe Stock.

Event Pattern Example (C#)
public class Stock {
 
 public delegate void 
AskPriceChangedHandler(object sender, 
 
AskPriceChangedEventArgs e);
public event AskPriceChangedHandler AskPriceChanged;
 
 object _askPrice;
 
 public object AskPrice {
 
 set { 
_askPrice=value; 
 
//fire the event
OnAskPriceChanged(); 
 }
 
 }//AskPrice property
 
 
  
//method to fire event delegate with proper name
 protected void OnAskPriceChanged() {
 
 AskPriceChanged(this,new 
AskPriceChangedEventArgs(_askPrice));
 
 }//AskPriceChanged
 
 }//Stock class
 
 //specialized event class for the askpricechanged event
 public class AskPriceChangedEventArgs:EventArgs {
 
 //instance variable to store the ask price
 private object _askPrice;
 
 //constructor that sets askprice
 public AskPriceChangedEventArgs(object 
askPrice) { _askPrice=askPrice; }
 
 //public property for the ask price
 public object AskPrice { get { return 
_askPrice; } }
 
 }//AskPriceChangedEventArgs

Conclusion

En regard de ce que nous venons de voir sur le modèle Observateur, il devient évident que ce modèle fournit un mécanisme idéal pour assurer « the crisp boundaries » entre objet d'une application quelque soit leur fonction (Interface utilisateur ou autre). Bien qu'il soit relativement simple de l'implémenter via les fonctions de rappel (utilisation des interfaces IObserver et IObservable), les concepts des délégués et des évènements de la CLR facilite leur implémentation et réduit le couplage entre le sujet et l'observateur. La bonne utilisation de ce modèle assure réellement que l'application puisse évoluer au fil du temps. Ce modèle permet d'apporter des changements sur votre interface graphique aussi bien que sur votre logique métier, sans pour cela que soit aussi difficile que cela ne parait.

Les modèles de conception sont un puissant outil de développement d'application modulable, si utilisés efficacement. Dans cette article on a pu démontrer la solidité de l'approche par modèles, aussi bien que les modèles utilisés dans le Framework .NET. Dans l'article suivant les modèles de fabrique de classe, on expose d'autres modèles de la FCL qui permettent de construire des objets .NET efficacement.

 



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

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