• 4 heures
  • Facile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 15/02/2023

Gérez des objets avec des patterns de comportement

Design patterns de comportement

La dernière catégorie de design patterns est celle des patterns de comportement. Les design patterns de comportement correspondent à des modèles de communication partagés entre des objets.

Dans ce dernier chapitre, intéressons-nous à trois d'entre eux :

  • Observateur

  • Stratégie

  • État

Observateur (Observer)

Le premier pattern de comportement que nous allons voir est le pattern observateur. Ce pattern définit une dépendance un-à-plusieurs entre des objets. Lorsqu'un objet change d'état, tous ses objets dépendants sont informés et mis à jour automatiquement. 

Plusieurs objets observateurs observent un sujet spécifique (un autre objet) et veulent être notifiés lorsqu'une modification intervient au sein de ce sujet. S'ils ne souhaitent plus l'être, ils peuvent se désinscrire du sujet. 

Le diagramme de classe UML suivant illustre la structure d'un observateur :

Structure d'un pattern observateur
Structure d'un pattern observateur

Pour créer un pattern observateur, vous aurez besoin des éléments suivants :

  • Sujet : il connaît ses observateurs et fournit une interface permettant d'attacher et de détacher des objets observateurs.

  • SujetConcret : il permet de stocker l'état d'intérêt pour l'observateur concret et d'envoyer une notification à ses observateurs lorsque son état change. 

  • Observateur : il permet de définir une interface de mise à jour pour les objets souhaitant être notifiés des modifications d'un sujet.

  • ObservateurConcret : il permet de maintenir une référence à un objet SujetConcret. Il garde en mémoire l'état qui devrait rester cohérent avec le sujet. Enfin, il implémente l'interface de mise à jour de l'observateur pour assurer la cohérence de son état avec le sujet.

Dans quelles situations utiliser ce pattern ?

Vous pouvez l'utiliser chaque fois que votre application doit notifier quelqu'un ou quelque chose, par exemple, un blog. Un auteur peut écrire un article, et d'autres personnes peuvent s'abonner au blog pour savoir si de nouveaux articles ont été publiés.

Voyons comment créer un blog avec un pattern observateur. Tout d'abord, créons l'observateur, qui est l'interface  ILecteurBlog : 

public interface ILecteurBlog
{
   void NotificationRecue(string message);
}

Créons ensuite le sujet, qui attache les objets observateurs :

public interface IServiceNotification
{
   void AjouterLecteurs(ILecteurBlog lecteurBlog);
   void NotifierLecteurs(string messageNotification);
}

Ensuite, créons le sujet concret,  AuteurBlog , chargé de gérer les abonnés au blog : 

public class AuteurBlog : IServiceNotification
{
   List<ILecteurBlog> listeLecteurs = new List<ILecteurBlog>();

   public void AjouterLecteurs(ILecteurBlog lecteurBlog)
   {
      listeLecteurs.Add(lecteurBlog);
   }
   public void NotifierLecteurs(string messageNotification)
   {
      foreach(var lecteur in listeLecteurs)
      {
         lecteur.NotificationRecue(messageNotification);
      }
   }
}

Comme vous pouvez le voir, le service de notification ajoute le type des abonnés, qui implémentent l'interface  ILecteurBlog . Il ajoute donc des abonnés qui recevront des notifications via cette interface.

Enfin, la classe  LecteurBlog  est notre ObservateurConcret. C'est un abonné qui implémentera l'interface  ILecteurBlog  pour s'autogérer ou effectuer l'action de son choix lors de la réception de notifications.

public class LecteurBlog: ILecteurBlog
{
   private string _nomLecteur;

   public LecteurBlog(string nomLecteur)
   {
      _nomLecteur = nomLecteur;
   }

   public void NotificationRecue(string messageNotification)
   {
      Console.WriteLine(_nomLecteur + " " + messageNotification);
}
}

La configuration du blog est terminée.

Écrivons maintenant une application console pour permettre à un auteur de créer et publier des articles :

static void Main(string[] args)
{
   AuteurBlog auteurBlog = new AuteurBlog();

   LecteurBlog lecteur1 = new LecteurBlog("Julien");
   LecteurBlog lecteur2 = new LecteurBlog("Michael");

   //ajout de lecteurs du blog à la liste des abonnés
   auteurBlog.AjouterLecteurs(lecteur1);
   auteurBlog.AjouterLecteurs(lecteur2);

   //informer les lecteurs de la publication du premier article
   auteurBlog.NotifierLecteurs("j'ai publié un nouvel article sur les chats !");

   Console.WriteLine(Environment.NewLine);

   //informer les lecteurs de la publication du deuxième article
   auteurBlog.NotifierLecteurs("j'ai publié un nouvel article sur les chiens !");
}

L'exemple ci-dessus montre que nous avons ajouté des abonnés à la liste, puis appelé la méthode  NotifierLecteurs  en indiquant le message de notification.

Voici la sortie :

"Julien, j'ai publié un nouvel article sur les chats !"

"Michael, j'ai publié un nouvel article sur les chats !"

"Julien, j'ai publié un nouvel article sur les chiens !"

"Michael, j'ai publié un nouvel article sur les chiens !"

Quels sont les avantages de cette stratégie ?

  • Elle offre un couplage faible entre des objets qui interagissent les uns avec les autres.

  • Elle permet d'envoyer des données à des objets sans modifier les classes des sujets ou observateurs. 

  • Elle permet d'ajouter et de supprimer des observateurs à tout moment. 

Stratégie (Strategy)

Le prochain design pattern de comportement est le pattern stratégie. Il permet de définir plusieurs algorithmes et les rendre interchangeables en les encapsulant chacun dans un objet.

Il s'agit d'encapsuler les comportements dans des objets pour choisir ensuite les objets à utiliser. Le comportement à implémenter est donc basé sur une entrée externe.

Dans quelles situations utiliser le pattern stratégie ?

Ce pattern est utile lorsque vous avez différentes logiques possibles en fonction d'une ou de plusieurs conditions. Par exemple, si vous avez trop de blocs  if  ou  switch  et avez besoin d'une structure plus propre.

Le diagramme de classe UML suivant illustre la structure du pattern stratégie :

Structure du pattern stratégie
Structure du pattern stratégie

Pour créer un pattern stratégie, vous aurez besoin des éléments suivants :

  1. Stratégie : déclare une interface commune à tous les algorithmes pris en charge.

  2. StrategieConcrete : permet d'implémenter un algorithme à l'aide de l'interface de stratégie.

  3. Contexte : configuré avec l'objet StrategieConcrete, il maintient une référence à un objet de stratégie et peut définir une interface permettant à la stratégie d'accéder à ses données.

Pour illustrer ce pattern stratégie, pensons à différents modes de cuisson des aliments. Vous pouvez faire griller votre nourriture, la faire cuire à l'huile ou la mettre au four. Chacune de ces méthodes permet de faire cuire vos aliments, mais le processus utilisé sera différent. Dans notre exemple, chacun de ces processus correspondra à une classe à part entière.

Codons une calculatrice très simple proposant seulement quatre opérations : addition, soustraction, multiplication et division.

Commençons par créer une nouvelle interface pour nos opérations mathématiques, la stratégie :

public interface IOperateurMath
{
   int Operation(int a, int b);
}

Ensuite, une StrategieConcrete nous permet d'implémenter l'interface de chaque algorithme. Pour l'instant, intéressons-nous seulement à l'addition :

public class AdditionMath: IOperateurMath
{
   public int Operation(int a, int b)
   {
      return a + b;
   }
}

//implémentation des autres classes

Ensuite, mettons-la en contexte. Pour ce faire, nous utilisons l'interface  IOperateurMath  , dans le code :

Dictionary<string, IOperateurMath> strategies = new Dictionary<string, IOperateurMath>();
strategies.Add("+", new AdditionMath() );
strategies.Add("-", new SoustractionMath() );
strategies.Add("*", new MultiplicationMath() );
strategies.Add("/", new DivisionMath() ); 

Imaginons que l'utilisateur saisisse l'opération suivante : +, 5, 2:

IOperateurMath strategieSelectionnee = strategies["+"];
int resultat = strategieSelectionnee.Operation(5,2);
return resultat;

La sortie sera : 7.

Comme vous pouvez le voir, chaque opération correspond à une classe à part entière. Cela nous évite d'utiliser une classe plus vaste et remplie d'événements conditionnels. De plus, elle offre une meilleure séparation des responsabilités.

Quels sont les avantages de cette stratégie ?

  • Chaque algorithme est placé dans son propre fichier. 

  • Il décide du comportement à suivre en fonction des entrées externes.

  • Il respecte le principe de responsabilité unique, car chaque classe dispose d'une seule responsabilité. Il n'y a pas une classe unique contenant de multiples événements conditionnels. 

État (State)

Le dernier pattern de comportement est le pattern État. Il vise à autoriser un objet à changer de comportement lorsque son état interne change. 

Prenez un distributeur de billets. L'état interne de la machine est "Carte de débit non insérée". Dans ce cas, quelles opérations pouvez-vous effectuer ?

  • Vous pouvez insérer votre carte bancaire.

  • Vous ne pouvez pas éjecter la carte, car elle n'est pas insérée dans la machine.

  • Vous ne pouvez pas saisir votre code secret et retirer de l'argent pour la même raison.

Vous avez fini par insérer votre carte. Désormais, les opérations disponibles sont différentes :

  • Vous ne pouvez pas insérer de carte bancaire, car il y en a déjà une.

  • Vous pouvez éjecter la carte.

  • Vous pouvez saisir votre code secret et retirer de l'argent.

Comme vous le voyez, de nombreuses opérations ou comportements dépendent de l'état de la carte bancaire (insérée ou non insérée).

Le diagramme de classe UML suivant illustre la structure du pattern d'état :

Structure du pattern état
Structure du pattern état

Pour créer un pattern état, vous aurez besoin des éléments suivants :

  • Contexte : définit l'interface pour les clients.

  • État : définit une interface encapsulant le comportement associé à un état donné du contexte.

  • EtatConcret : chaque sous-classe implémente un comportement associé à un état du contexte.

Reprenons l'exemple du distributeur de billets. Tout d'abord, créons l'interface état : 

public interface IEtatDab
{
   void CarteInseree();
   void EjecterCarte();
   void SaisirCode();
   void RetirerArgent();
}

Gardez à l'esprit que le distributeur de billets dispose de deux états : carte insérée et carte non insérée. Code des EtatConcret :

public class EtatCarteInseree: IEtatDab
{
   public void CarteInseree()
   {
      Console.WriteLine("Vous ne pouvez pas insérer de carte, car il y en a déjà une.")
   }

   public void SaisirCode()
   {
      Console.WriteLine("Code saisi.");
   }

   //Implémentation des autres méthodes
}

public class CarteNonInseree: IEtatDab
{
   public void CarteInseree()
   {
      Console.WriteLine("Carte insérée.");
   }

   public void SaisirCode()
   {
      Console.WriteLine("Vous ne pouvez pas saisir votre code secret, car aucune carte n'est insérée."); 
   }
}

Comme vous le voyez, chaque état concret fournit sa propre implémentation pour une requête. Par conséquent, lorsque l'état de l'objet du contexte change, son comportement change également.

Créons maintenant une classe de contexte pouvant disposer de divers états internes. Nous allons prendre l'exemple de  InsererCarte()  et  SaisirCode()  :

public class MachineDab : IEtatDab
{
 public IEtatDab EtatMachineDab { get; set; }

   public MachineDab()
   {
      EtatMachineDab = new CarteNonInseree();
   }

   public void InsererCarte()
   {
      EtatMachineDab.InsererCarte();

      if(EtatMachineDab is CarteNonInseree)
      {
         EtatMachineDab = new EtatCarteInseree();

         Console.WriteLine("L'état du DAB est désormais Carte insérée.");
      }
   }

   …

Dans l'application console principale, implémentons le distributeur de billets :

static void Main(string[] args)
{
   MachineDab machineDab = new MachineDab();

   Console.WriteLine("L'état du DAB est  " + machineDab.EtatMachineDab.GetType().Name);

   machineDab.SaisirCode();

   //implémentation des opérations restantes
}

Sortie :

L'état du distributeur de billets est CarteNonInseree.

Vous ne pouvez pas saisir votre code secret, car aucune carte n'est insérée.

Quels sont les avantages de ce pattern ?

  • Il limite la complexité générée par les structures conditionnelles.

  • Il facilite l'ajout d'autres états.

  • Il améliore la cohésion, car les comportements spécifiques à chaque état sont regroupés dans les classes EtatConcret.

En résumé

  • Le pattern Observateur définit une dépendance un-à-plusieurs entre des objets. Lorsqu'un objet change d'état, tous ses objets dépendants sont informés et mis à jour automatiquement. 

  • Le pattern Stratégie permet de définir plusieurs algorithmes et les rendre interchangeables en les encapsulant chacun dans un objet.

  • Le pattern État vise à autoriser un objet à changer de comportement lorsque son état interne change. 

Super ! Vous connaissez désormais les trois groupes de design patterns. Prenons quelques minutes pour récapituler tout ce que nous avons vu dans ce cours.

Exemple de certificat de réussite
Exemple de certificat de réussite