• 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

Organisez des objets avec des patterns de structure

Design patterns de structure

Intéressons-nous maintenant à une nouvelle catégorie de design patterns, les design patterns de structure. Ces design patterns facilitent la conception d'une application en offrant une méthode simple pour établir des liens entre des entités.

Dans ce chapitre, nous allons étudier trois de ces design patterns :

  • Adaptateur

  • Composite

  • Décorateur 

Adaptateur (Adaptor)

Ce pattern est très simple à comprendre lorsqu'on fait l'analogie avec un adaptateur physique. Pensez aux adaptateurs électriques que vous avez chez vous. Votre smartphone dispose d'un adaptateur dont une extrémité est dotée d'une entrée USB et l'autre d'une fiche pour prise de courant murale. L'adaptateur a pour objectif de rendre ces deux formats compatibles.

Image d'un adaptateur
Image d'un adaptateur

L’adaptateur permet à des classes incompatibles d'interagir en convertissant l'interface d'une classe en une interface attendue par le client

Pour utiliser un adaptateur, vous avez besoin des éléments suivants :

  • Une interfacecible

  • Une classeadaptée

  • Une classe d'adaptation

  • Une applicationcliente

Prenons l'exemple d'une entreprise qui gère les dossiers RH pour différentes sociétés tierces. Chacune d'elle dispose de son propre système de gestion. Nous devons donc écrire un adaptateur permettant à l'entreprise de récupérer les dossiers des employés depuis tous ces systèmes.

Tout d'abord, créons une interface cible  ICible : 

public interface ICible
{
   List<string> RecupererEmployes();
}

Passons ensuite à la classe  adaptée  :

public class EmployeTiers
{
   public List<string> RecupererListeEmployes()
   {
      List<string> ListeEmployes = new List<string>();
      ListeEmployes.Add("Jean");
      ListeEmployes.Add("Pierre");

      return ListeEmployes;
   }
}

Créons maintenant la classe d'  adaptation  :

public class AdaptateurEmployes : EmployeTiers, ICible
{
   public List<string> RecupererEmployes()
   {
      return RecupererListeEmployes();
   }
}

Enfin, codons l'application  cliente  pour l'implémentation : 

public class Client
{
   static void Main(string[] args)
   {
      ICible adaptateur = new AdaptateurEmployes();

 foreach(string employe in adaptateur.RecupererEmployes())
      {
         Console.WriteLine(employe);
      }
   }
}

La sortie sera :

Jean

Pierre

Comme vous pouvez le voir dans l'application  cliente  , l'interface  ICible  appelle la fonctionnalité de la classe  adaptée .

Quels sont les avantages de cette stratégie ?

  • Vous bénéficiez d'une meilleure réutilisabilité et d'une meilleure flexibilité.

  • La classe cliente n'est pas compliquée par le recours forcé à une interface différente. 

Composite

Intéressons-nous maintenant à un autre design pattern de structure, le composite. Il décrit un groupe d'objets considéré comme formant une instance unique d'un même type d'objet. L'objectif est de placer les objets dans une structure arborescente afin de représenter des hiérarchies partielles et globales.

En d'autres termes, vous pouvez représenter tout ou partie d'une hiérarchie en la réduisant à ses composants communs.

Pour établir un composite, vous aurez besoin des éléments suivants :

  • Composant : déclare l'interface pour les objets et leur comportement commun.

  • Feuilles : cet élément représente le comportement des feuilles.

  • Composite : il définit le comportement des composants ayant des enfants (inverse des feuilles). 

  • Client : application principale.

Prenons l'exemple d'une cafetière haut de gamme.

Cafetière haut de gamme
Cafetière haut de gamme

Ce qui rend cette cafetière unique, c'est qu'elle vous invite à composer votre boisson en sélectionnant d'abord un type (expresso, latte, cappuccino...), puis une saveur.

Arborescence des options de café
Arborescence des options de café

Il s'agit de la hiérarchie (arborescence) dont nous avons parlé un peu plus haut. Modélisons cette arborescence et déterminons le nombre de calories associé à chaque saveur.

public class Cafe
{
 public int Calories { get; set; }
 public List<Cafe> Saveurs { get; set; }

   public Cafe(int calories)
   {
      Calories = calories;
      Saveurs = new List<Cafe>();
   }

   public void AfficherCalories()
      {
      Console.WriteLine(this.GetType().Name + ": " + this.Calories.ToString() + " calories.");

      foreach(var boisson in this.Saveurs)
      {
         boisson.AfficherCalories();
      }
   }
}

Avez-vous remarqué la méthode  AfficherCalories()  ? Il s'agit d'une méthode récursive permettant d'afficher les calories de l'ensemble des saveurs.

Maintenant, implémentons les feuilles des saveurs de café :

public class LatteVanille : Cafe
{
   public LatteVanille(int calories)
      : base(calories)
   { }
}

public class LatteCannelle : Cafe
{
   public LatteCannelle(int calories)
      : base(calories)
   { }
}

public class CappuccinoMenthePoivree : Cafe
{
       public CappuccinoMenthePoivree(int calories)
      : base(calories)
   { }
}

public class CappuccinoCaramel : Cafe
{
   public CappuccinoCaramel(int calories)
      : base(calories)
   { }
}

Ensuite, implémentons le composite, qui représente les objets de l'arborescence disposant d'enfants :

public class Expresso : Cafe
{
   public Expresso(int calories)
      : base(calories)
   { }
}

public class Cappuccino : Cafe
{
   public Cappuccino(int calories)
      : base(calories)
   { }
}
public class Latte : Cafe
{
   public Latte(int calories)
      : base(calories)
   { }
}

Enfin, nous avons besoin d'une classe composite supplémentaire pour le nœud racine de l'arborescence :

public class CafeChaud : Cafe
{
   public CafeChaud(int calories)
      : base(calories)
   { }
}

Les classes composites sont parfaitement identiques aux classes feuilles, et c'est tout à fait normal.

Désormais, nous pouvons initialiser dans la méthode  Main  de notre application une arborescence  Cafe  avec plusieurs saveurs, puis afficher les calories pour chacune de ces saveurs.

static void Main(string[] args)
{
   Latte latte = new Latte(350);
   latte.Saveurs.Add(new LatteCannelle(400));
   latte.Saveurs.Add(new LatteVanille(300));

   Expresso expresso = new Expresso(10);
   
   Cappuccino cappuccino = new Cappuccino(450);
   cappuccino.Saveurs.Add(new CappuccinoCaramel(500));
   cappuccino.Saveurs.Add(new CappuccinoMenthePoivree(425));

   Cafe cafe = new Cafe(100);
   cafe.Saveurs.Add(latte);
   cafe.Saveurs.Add(expresso);
   cafe.Saveurs.Add(cappuccino);

   cafe.AfficherCalories();
}

La sortie de ce code serait :

Cafe : 100 calories.
Latte : 350 calories.
LatteCannelle : 400 calories.
LatteVanille : 300 calories.
Expresso : 10 calories.
Cappuccino : 450 calories.
CappuccinoCaramel : 500 calories.
CappuccinoMenthePoivree : 425 calories.

Quels sont les avantages de cette stratégie ?

  • Les clients peuvent ainsi appliquer un traitement identique à différentes parties de la hiérarchie des objets :

    • Si on appelle une feuille, la requête est gérée directement.

    • Si on appelle un composite, celui-ci transfère la requête à ses composants enfants.

  • L'ajout de nouveaux types de composants est simplifié.

  • La structure est flexible grâce à une classe ou une interface gérable.

Décorateur (Decorator)

Le dernier design pattern de notre liste est le décorateur. Il vous permet d'ajouter un comportement à un objet sans modifier celui des autres objets de la même classe. Un décorateur peut être considéré comme une alternative à un héritage. 

Pour illustrer, imaginez que vous commandez un sundae. Ce dessert est principalement composé d'une glace. Vous pouvez le savourer avec une cuillère. Vous pouvez lui ajouter des éclats croustillants et des nappages. En d'autres termes, le décorer. Mais même décoré, vous continuez à le savourer avec une cuillère. L'interface reste identique, même si le sundae est devenu plus complexe.

Pour créer un pattern décorateur, on utilise des éléments suivants :

  1. Composant : il déclare l'interface des objets auxquels il est possible d'ajouter dynamiquement des responsabilités. 

  2. ComposantConcret : objet auquel des responsabilités supplémentaires seront ajoutées.

  3. Décorateur : pour garder une référence à l'objet composant et définir une interface qui se conforme à celle du composant.

  4. DécorateurConcret : permet d'ajouter des responsabilités au composant. 

Prenons l'exemple d'une pizza que nous allons décorer avec différentes garnitures.

Commençons par la classe du composant, nommée  Pizza  :

public abstract class Pizza
{
   protected string description = "Pizza simple";

   public string RecupererDescription()
   {
      return description;
   }

   public abstract int RecupererCout();
}

Créons ensuite les classes concrètes de  Pizza  , qui correspondent aux types de pizzas :

public class Pepperoni : Pizza
{
   public Pepperoni()
   {
      description = "Pepperoni";
   }

   public override int RecupererCout()
   {
      return 10;
   }
}

public class Saucisse : Pizza
{
   public Saucisse()
   {
      description = "Saucisse";
   }

   public override  int RecupererCout()
   {
      return 14;
   }
}

Ajoutons maintenant la classe  Decorateur  , qui étend la classe  Pizza  :

public abstract class DecorateurGarnitures : Pizza
{
   public abstract string RecupererDescription();
}

Ajoutons ensuite les décorateurs concrets, à savoir les garnitures des pizzas :

public class Fromage : DecorateurGarnitures
{
   Pizza pizza;

   public Fromage(Pizza pizza)
   {
      this.pizza = pizza;
   }

   public override string RecupererDescription()
   {
      return pizza.RecupererDescription() + ", Fromage";
   }

   public override int RecupererCout()
   {
      return 2 + pizza.RecupererCout();
   }
}

public class Oignon : DecorateurGarnitures
{
   Pizza pizza;

   public Oignon(Pizza pizza)
   {
      this.pizza = pizza;
   }

   public override string RecupererDescription()
   {
      return pizza.RecupererDescription() + ", Oignon";
   }

   public override int RecupererCout()
   {
      return 3 + pizza.RecupererCout();
   }
}

Pour finir, rassemblons tout ce code dans l'application cliente :

public static void Main(string[] args)
{
   //création d'une nouvelle pizza aux pepperonis
   Pizza pizzaPepperoni = new Pepperoni();
   Console.WriteLine(pizzaPepperoni.RecupererDescription() + " Coût : " + pizzaPepperoni.RecupererCout());

   //création d'une nouvelle pizza à la saucisse
   Pizza pizzaSaucisse = new Saucisse();
   //décoration avec une garniture au fromage
   pizzaSaucisse = new Fromage(pizzaSaucisse);

   //décoration avec une garniture aux oignons
   pizzaSaucisse = new Oignon(pizzaSaucisse);

   Console.WriteLine(pizzaSaucisse.RecupererDescription() + " Coût :" + pizzaSaucisse.RecupererCout());
}

Voici la sortie de ce code :

Pepperoni Coût : 10 €.

Saucisse, Fromage, Oignon Coût : 19 €.

Comme vous le constatez, vous pouvez ajouter et supprimer des pizzas et des garnitures sans modifier le reste de votre code.

Quels sont les avantages de cette stratégie ?

  • Elle offre une option plus simple en cas d'héritage complexe (sur trop de niveaux).

  • Elle est utile lorsque vous devez ajouter des comportements supplémentaires à vos objets au moment de l'exécution. 

  • Elle est utile lorsque différentes instances d'un même objet peuvent se comporter différemment. 

En résumé

  • L’adaptateur permet à des classes incompatibles d'interagir en convertissant l'interface d'une classe en une interface attendue par le client. 

  • Le composite décrit un groupe d'objets considérés comme formant une instance unique d'un même type d'objet. 

  • Le décorateur vous permet d'ajouter un comportement à un objet sans modifier celui des autres objets de la même classe. 

Maintenant que nous avons vu quelques design patterns de structure, étudions un dernier groupe de patterns : les patterns de comportement.

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