SOLID Partie II : Open/Closed Principle

Introduction

Dès lors qu’un logiciel est, avec le temps, amené à être modifié, que ce soit afin d’y ajouter des fonctionnalités ou de modifier certains comportements, des problèmes de régression se posent. Au fil des versions la qualité tend à diminuer et la maintenabilité aussi.

C’est en 1988 que Bertrand Meyer invente le principe Open/Closed dans Object-Oriented Software Construction en précisant qu’une fois une entité (classe) validée, l’on ne devrait plus la changer que pour corriger des problèmes, et que toute nouvelle fonctionnalité devrait impliquer l’écriture d’une nouvelle classe, héritant ou non de la première. En bref, pour changer quelque chose il faut ajouter du code, et le code existant ne devrait pas être modifié:

Software entities should be open for extension, but closed for modification

  • open for extension signifie que l’on peut modifier / étendre le comportement de l’entité
  • closed for modification signifie que l’on ne peut pas changer le code de l’entité.

En 1996, Robert C. Martin reprend cette notion à son compte et explicite le moyen d’appliquer l’Open/Closed Principle (OCP) en insistant notamment sur l’importance de l’abstraction.

Abstraction, abstraction, abstraction…

Quand un programme respecte l’OCP, il évite le phénomène de réécriture en cascade à chaque modification. Comme à chaque fois l’abstraction est la clé. En effet l’utilisation (abusive?) d’interfaces/de classes abstraites évite toute dépendance à l’implémentation, d’où une diminution drastique du couplage.

Exemple #1 : code non fermé car trop peu d’abstraction

class Forme;
class Cercle : public Forme {
 /*...*/
}
class Rectangle : public Forme {
 /*..*/
}

double calculerPerimetre (Forme * forme) {
  Cercle* c = dynamic_cast<Cercle*>(forme);
  if (c) {
    return M_PI * c->getRayon() * 2.;
  }
  Rectangle * r = dynamic_cast<Rectangle*>(forme);
  if (r) {
    return (r->getLargeur() + r->getLongueur()) * 2.;
  }
}

Ici il est très clair que si l’on rajoute une nouvelle Forme, on devra modifier la fonction calculerPerimetre: cette fonction n’est pas “fermée”. On peut rapidement rendre cet exemple conforme à l’OCP grâce à l’abstraction:

class Forme {
  public:
    virtual double calculerPerimetre () = 0;
}
class Cercle : public Forme {
 /*...*/
  public:
    virtual double calculerPerimetre () {
      return M_PI * r * 2.;
    }
}
class Rectangle : public Forme {
 /*..*/
  public:
    virtual double calculerPerimetre () {
      return (longueur + largeur) * 2;
    }
}

double calculerPerimetre (Forme * forme) {
  return forme->calculerPerimetre ();
}

Exemple #2 : Decorator

class Burger {
  /*...*/
  public:
    virtual double prix () = 0;
}

class SimpleBurger {
  /*..*/
  public:
    virtual double prix () {
      return pain->prix() * 2 + steak->prix();
    }
}

Dans cet exemple nous avons l’abstraction Burger qui nous permet de définir plusieurs type de burgers, et SimpleBurger, un burger tout simple. Notre but est de calculer le prix.
Si à présent l’on décide que les clients pourront prendre un supplément salade, ketchup et cornichon, l’implémentation suivante ne respecte pas l’OCP:

class Burger {
  /*...*/
  public:
    virtual double prix () = 0;
}

class SimpleBurger {
  /*...*/
  public:
    /*...*/
    virtual double prix () {
      double prix = pain->prix() * 2 + steak->prix();
      if (supplement_salade) {
        prix += 0.2;
      }
      if (supplement_ketchup) {
        prix += 0.3;
      }
      if (supplement_cornichon) {
        prix += 0.2;
      }
      return prix;
    }
}

int main() {
  /*...*/
  Burger * b = new SimpleBurger();
  b->addKetchup();
  b->addSalade();
  double prix = b->prix();
  /*...*/
}

Dès que l’on ajoute un supplément, l’on doit ajouter un cas dans la fonction prix, qui n’est donc pas fermée. En revanche on peut utiliser le Design Pattern Decorator, et rendre le programme conforme à l’OCP:

class Burger {
  /*...*/
  public:
    virtual double prix () = 0;
}
class BurgerSupplement : public Burger {}

class SimpleBurger {
  /*..*/
  public:
    virtual double prix () {
      return pain->prix() * 2 + steak->prix();
    }
}

class AvecSalade : public BurgerSupplement {
  private:
    Burger * decorated;
  public:
    SupplementSalade(Burger * decorated) : decorated(decorated) {}
    virtual double prix () {
      return decorated->prix() + 0.2;
    }
}

class AvecKetchup : public BurgerSupplement {
  private:
    Burger * decorated;
  public:
    SupplementKetchup(Burger * decorated) : decorated(decorated) {}
    virtual double prix () {
      return decorated->prix() + 0.3;
    }
}

class AvecCornichon : public BurgerSupplement {
  private:
    Burger * decorated;
  public:
    SupplementCornichon(Burger * decorated) : decorated(decorated) {}
    virtual double prix () {
      return decorated->prix() + 0.2;
    }
}

int main() {
  /*...*/
  Burger * b = new AvecKetchup( new AvecSalade( new SimpleBurger() ) );
  double prix = b->prix();
  /*...*/
}

Rajouter un supplément consiste à créer une nouvelle classe et le code existant n’est pas modifié.

Design Patterns

Les deux exemples précédents sont très simples et ne couvrent qu’une infime partie des cas que vous pouvez rencontrer.
Je vous invite à vous familiariser avec certains design patterns permettant de rendre un programme conforme à l’OCP:

  • Decorator : nous l’avons vu, il permet d’ajouter des fonctionnalités sans ni modifier le code ni créer une dépendance forte à l’implémentation existante, comme le ferait l’utilisation de l’héritage.
  • Strategy : permet de modifier le comportement d’une classe sans en modifier le code, en encapsulant un objet responsable de ce comportement.
  • Visitor : permet de séparer un algorithme de la structure de données sur laquelle il agit, rendant possible un changement du comportement sans modifier de code.
  • Chain of responsibility : permet de rajouter des “étapes” dans le traitement sans modifier le code, en créant un nouvel élément de la chaîne

Conclusion

L’Open/Closed Principle doit être considéré plus comme un ligne directrice que comme un but en soi. De plus, il doit être mis en regard d’un autre grand principe du développement logiciel : le you-ain’t-gonna-need-it principle. Ce principe nous rappelle de ne pas nous laisser tenter par l’écriture de code inutile pour le moment. Ainsi l’OCP ne sera en pratique souvent appliqué qu’au moment d'”ouvrir” : “Avant il n’y avait qu’un cas mais à présent il y en a deux : refactorons afin de faciliter cet ajout et les suivants.”

Pour aller plus loin

One thought on “SOLID Partie II : Open/Closed Principle

Leave a Reply