• 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

"O" pour Open/Closed Principe ou principe ouvert/fermé

Principe ouvert/fermé

Ce principe est considéré comme le plus important de la conception orientée objet. Il stipule que votre code (classes, modules, fonctions, etc.) doit être ouvert à l'extension, mais fermé à la modification

Mais qu'est-ce que cela veut dire exactement ? 

Cela signifie que vous devez pouvoir ajouter de nouvelles fonctionnalités sans modifier votre code existant. Pour cela, vous devez utiliser des interfaces permettant à votre code d'utiliser diverses implémentations interchangeables sans être modifié.

L'intérêt d'utiliser une interface réside dans l'ajout d'un niveau d'abstraction supplémentaire qui limite le couplage. Les interfaces d'implémentation sont indépendantes les unes des autres et n'ont pas de code en commun.

Exemple 1 : code non fermé à la modification

Prenons l'exemple d'un bloc de code permettant de calculer la superficie d'un rectangle. Votre rectangle dispose d'une largeur et d'une hauteur.

public class Rectangle
{
 public double Largeur { get; set; }
 public double Hauteur { get; set; }
}

Ensuite, nous devons créer une calculatrice de  Superficie  permettant de déterminer l'aire totale d'un ensemble de rectangles :

public class CalculSuperficie
{
   public double Superficie(Rectangle[] formes)
   {
      double superficie = 0;

      foreach (var forme in formes)
      {
         superficie += forme.Largeur * forme.Hauteur;
      }

      return superficie;
   }
}

Super ! Ça fonctionne ! Mais, que faire si vous vouliez calculer la superficie d'un cercle ? Voyons... Il faudrait modifier la méthode  Superficie  pour qu'elle prenne en charge les cercles. 

Vous pouvez donc modifier la méthode  Superficie  de sorte qu'elle accepte un ensemble d'objets et non pas le seul type  Rectangle  . Vous pouvez ensuite vérifier le type de chaque objet pour déterminer s'il s'agit d'un rectangle ou d'un cercle, puis appliquer la bonne formule de calcul.

public double Superficie(object[] formes)
{
   double superficie = 0;

   foreach (var forme in formes)
   {
      if (forme is Rectangle)
      {
         var rectangle = (Rectangle)forme;
         superficie += rectangle.Largeur * rectangle.Hauteur;
      }
      else
      {
         var cercle = (Cercle)forme;
         superficie += cercle.Rayon * cercle.Rayon * Math.PI;
      }
   }

   return superficie;
}

Très bien, ça marche. Mais, que faire si vous vouliez également calculer la superficie d'un triangle ? Pff... Il va falloir ENCORE modifier la méthode  Superficie  !

Comme vous pouvez le voir, la structure de ce code ne respecte pas le principe ouvert/fermé, qui voudrait que votre méthode soit fermée à la modification. Malheureusement, la méthode  Superficie  n'est ni fermée à la modification ni ouverte à l'extension.

Exemple 2 : utilisation de contrats

Vous pouvez résoudre ce problème en créant un contrat, qui impose à la classe qui en hérite d'utiliser tous les éléments définis. En C#, les contrats sont des interfaces et des classes abstraites. 

Les classes abstraites

Une classe abstraite vous permet de masquer ses détails internes pour en exposer uniquement la fonctionnalité.

  •     Une classe abstraite n'est jamais instanciée.

  •     Elle doit contenir au moins une méthode abstraite.

  •     Elle est généralement utilisée pour définir une classe de base dont héritent d'autres classes.

  •     Elle peut contenir des constructeurs.

  •     Elle peut implémenter des fonctions à l'aide de méthodes non abstraites.

  •     Elle ne prend pas en charge l'héritage multiple.

  •     Elle ne peut pas être statique.

Voici un exemple de programme en C# utilisant une classe abstraite :

using System;

public abstract class MaClasseAbstraite
{
   public abstract void monTest();
}

public class MaClasse1 : MaClasseAbstraite
{
   public override void monTest()
   {
      Console.WriteLine("Ceci est monTest de MaClasse1.");
   }
}

public class MaClasse2 : MaClasseAbstraite
{
   public override void monTest()
   {
      Console.WriteLine("Ceci est monTest de MaClasse2.");
   }
}
Les interfaces

Comme les classes, les interfaces peuvent disposer de méthodes, de propriétés, d'indexeurs et d'événements. Toutefois, elles ne contiennent que la signature de ces membres. L'implémentation des membres d'une interface est réalisée par la classe qui implémente cette interface.

Utilisons une interface pour illustrer ce fonctionnement.

Créons une interface pour la forme :

public interface IForme
{
   double Superficie();
}

Nos classes  Rectangle  et  Cercle  héritent désormais de l'interface  IForme :

public class Rectangle : IForme
{
 public double Largeur { get; set; }
 public double Hauteur { get; set; }
   public double Superficie()
   {
      return Largeur * Hauteur;
   }
}

public class Cercle : IForme
{
 public double Rayon { get; set; }
   public double Superficie()
   {
 return Rayon * Rayon * Math.PI;
   }
}

Avec cette approche, nous avons transféré la responsabilité du calcul de la superficie dans chaque classe, alors qu'elle appartenait précédemment à la méthode Superficie de CalculSuperficie.

Comment implémenter ces classes dans mon application ?

Il est possible de passer par l’injection de dépendances. L'idée est de donner (injecter) à un objet l'objet dont il a besoin plutôt que de le créer. Vous disposez donc d'un objet injecté et d'une méthode qui accepte une interface en tant que paramètre. Ainsi, une autre partie du code peut instancier un objet qui implémente l'interface, puis utiliser cet objet pour l'injection.

Dans notre application, au lieu d'instancier un des deux types acceptés par  IForme  , nous allons injecter celui qui nous intéresse. Supposons que notre application dispose d'un contrôleur MVC nommé FormeController. Nous pouvons ajouter un paramètre de type  IForme  au constructeur du contrôleur.  Ensuite, nous pouvons transmettre la bonne implémentation via ce paramètre.

public class FormeController : Controller
{
   private IForme _forme;

   public FormeController(IForme forme)
   {
      _forme = forme;
   }
}

Il reste encore un détail à régler. Nous devons créer l'objet  IForme  qu'il faudra injecter dans  FormeController  . Nous verrons comment le faire un peu plus loin dans ce cours. 

En résumé 

  • Le principe ouvert/fermé stipule que votre code (classes, modules, fonctions, etc.) doit être ouvert à l'extension, mais fermé à la modification.

  • Le recours à des contrats comme des interfaces vous permet d'adapter les fonctionnalités de votre application sans changer le code existant. 

  • Une classe abstraite ne peut pas être instanciée, mais peut implémenter des fonctions.

  • Une interface ne contient que la déclaration des méthodes, propriétés, indexeurs et événements. 

Passons maintenant à la troisième lettre de l'acronyme SOLID, le "L". Il renvoie au principe de substitution de Liskov.

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