C#Observateur

Quelques raccourcis :

Ceci est un exemple implémentant (de manière simpliste) le schéma de conception Observateur en C#. Vous pourrez comparer par vous-mêmes avec des programmes équivalents dans les langages que vous connaissez.

Concept général (survol)

Le schéma de conception Observateur est un système d'abonnement :

Pour en savoir plus : ../Developpement/Schemas-conception.html#observateur

Notre exemple sera celui d'un petit serveur de touches lues au clavier : ce serveur lira les touches pressées et en informera les abonnés, qui agiront en conséquence. Certains abonnés feront écho à l'écran des touches pressées; un abonné particulier signalera la fin du programme sur pression de la touche 'q'.

Version avec interfaces

Dans cette version, les abonnés seront des classes implémentant une interface particulière. Le fournisseur conservera des références vers les abonnés à travers ces interfaces, et appellera les services de l'interface lorsque des événements surviendront.

L'interface RéactionClavier expose un service TouchePressée(). Ce service sera invoqué lors du rappel des abonnés.

using System;
using System.Collections.Generic;

namespace ObservateursInterface
{
   interface RéactionClavier
   {
      void TouchePressée(char c);
   }

Un cas particulier de RéactionClavier est un AfficheurTouche, qui affichera les touches pressées.

   class AfficheurTouche : RéactionClavier
   {
      public void TouchePressée(char c)
      {
         Console.WriteLine("Touche pressée: {0}", c);
      }
   }

Un Signaleur pourra signaler l'occurrence d'un « événement » (au sens large, pas dans une acception technique).

   interface Signaleur
   {
      void Signaler();
   }

Un Surveillant surveille une touche en particulier et, si cette touche est pressée, signale un Signaleur.

   class SignaleurManquantException : Exception {}
   class Surveillant : RéactionClavier
   {
      private Signaleur sign;
      private Signaleur Sign
      {
         get { return sign; }
         set
         {
            if (value == null)
               throw new SignaleurManquantException();
            sign = value;
         }
      }
      private char Touche { get; }
      public Surveillant(Signaleur sign, char c)
      {
         Sign = sign;
         Touche = Char.ToLower(c);
      }
      public void TouchePressée(char c)
      {
         if (Char.ToLower(c) == Touche)
            Sign.Signaler();
      }
   }

Un cas particulier de Signaleur est FinDeProgramme, dont le nom décrit bien la tâche.

   class FinDeProgramme : Signaleur
   {
      private bool Mort { get; set; }= false;
      public void Signaler()
      {
         Mort = true;
      }
      public bool Complété() => Mort;
   }

Un GestionnaireClavier est un singleton implémentant le schéma de conception Observateur. Il est capable de lire une touche et d'informer tous ses abonnées de la touche qui fut lue.

   class GestionnaireClavier
   {
      private List<RéactionClavier> Elems { get; } = new List<RéactionClavier>();
      public static GestionnaireClavier Instance { get; } = new GestionnaireClavier();
      private GestionnaireClavier()
      {
      }
      public void Abonner(RéactionClavier réac)
      {
         Elems.Add(réac);
      }
      public void Désabonner(RéactionClavier réac)
      {
         Elems.Remove(réac);
      }
      public void Exécuter()
      {
         char c = Console.ReadKey(true).KeyChar;
         foreach (RéactionClavier réac in Elems)
            réac.TouchePressée(c);
      }
   }

Le programme principal montre comment il est possible d'abonner quelques observateurs au gestionnaire et de lire, puis d'afficher des touches, de manière cyclique, jusqu'à ce que le signal de fin de programme ait été produit.

   class Program
   {
      static void Main(string[] args)
      {
         var ges = GestionnaireClavier.Instance;
         ges.Abonner(new AfficheurTouche());
         var fdp = new FinDeProgramme();
         ges.Abonner(new Surveillant(fdp, 'q'));
         while (!fdp.Complété())
            ges.Exécuter();
      }
   }
}

Le défaut de cette approche est qu'elle est intrusive : les abonnés doivent implémenter une interface particulière, ce qui demande une adaptation de leur structure interne, et ce qui mène à du code quelque peu verbeux.

Version avec délégués

Un délégué est une abstraction de certains langages (en particulier, C# et Delphi, les deux étant l'oeuvre du même concepteur, Anders Hejlsberg) permettant de réaliser du polymorphisme sur la base de la signature d'une fonction, plutôt que sur la base d'une interface.

Le délégué RéagirClavier correspond à une fonction void acceptant un char en paramètre; on pourra faire pointer un RéagirClavier sur n'importe quelle méthode (d'instance ou de classe) respectant cette signature. Ceci dictera la signature du service qui sera invoqué lors du rappel des abonnés.

using System;
using System.Collections.Generic;

namespace ObservateursDélégués
{
   delegate void RéagirClavier(char c);

Un cas particulier de RéagirClavier est la méthode TouchePressée d'un AfficheurTouche, qui affichera les touches pressées.

Notez que la classe AfficheurTouche n'est plus forcée d'implémenter une interface particulière. Nous avons réduit le couplage dans notre code, ce qui est en général vu comme une avancée.

   class AfficheurTouche
   {
      public void TouchePressée(char c)
      {
         Console.WriteLine("Touche pressée: {0}", c);
      }
   }

Le délégué Signaler pourra signaler l'occurrence d'un « événement » (au sens large, pas dans une acception technique).

   delegate void Signaler();

Un Surveillant surveille une touche en particulier et, si cette touche est pressée, le signale à travers un délégué de type Signaler.

   class SignaleurManquantException : Exception { }
   class Surveillant
   {
      private Signaler sign;
      private Signaler Sign
      {
         get { return sign; }
         set
         {
            if (value == null)
               throw new SignaleurManquantException();
            sign = value;
         }
      }
      private char Touche { get; }
      public Surveillant(Signaler sign, char c)
      {
         Sign = sign;
         Touche = Char.ToLower(c);
      }
      public void TouchePressée(char c)
      {
         if (Char.ToLower(c) == Touche)
            Sign();
      }
   }

Un cas particulier de délégué de type Signaler est la méthode Signaler d'une instance de FinDeProgramme.

   class FinDeProgramme
   {
      private bool Mort { get; set; } = false;
      public void Signaler()
      {
         Mort = true;
      }
      public bool Complété() => Mort;
   }

Un GestionnaireClavier est un singleton implémentant le schéma de conception Observateur. Il est capable de lire une touche et d'informer tous ses abonnées de la touche qui fut lue. Portez attention aux rappels faits dans la méthode Exécuter(), qui sont plus simples sur le plan syntaxique que dans la version par interfaces.

   class GestionnaireClavier
   {
      private List Elems { get; } = new List();
      public static GestionnaireClavier Instance { get; } = new GestionnaireClavier();
      private GestionnaireClavier()
      {
      }
      public void Abonner(RéagirClavier réac)
      {
         Elems.Add(réac);
      }
      public void Désabonner(RéagirClavier réac)
      {
         Elems.Remove(réac);
      }
      public void Exécuter()
      {
         char c = Console.ReadKey(true).KeyChar;
         foreach (RéagirClavier réac in Elems)
            réac(c);
      }
   }

Le programme principal montre comment il est possible d'abonner quelques observateurs au gestionnaire et de lire, puis d'afficher des touches, de manière cyclique, jusqu'à ce que le signal de fin de programme ait été produit.

Vous remarquerez que l'on abonne des méthodes plutôt que des instances dans cette version.

   class Program
   {
      static void Main(string[] args)
      {
         var ges = GestionnaireClavier.Instance;
         ges.Abonner(new AfficheurTouche().TouchePressée);
         var fdp = new FinDeProgramme();
         ges.Abonner(new Surveillant(fdp.Signaler, 'q').TouchePressée);
         while (!fdp.Complété())
            ges.Exécuter();
      }
   }
}

Voilà!


Valid XHTML 1.0 Transitional

CSS Valide !