Principes S.O.L.I.D - Open/closed principle (OCP)

Les problèmes de rigidité et de fragilité évoqués précédemment sont liés à une même cause : l'impact des changements sur de nombreuses parties de l'application. Chacun de ces changements oblige à modifier du code existant, ce qui est à la fois coûteux et risqué. Pour éviter cela, le principe d'ouverture/fermeture énoncé par Bertrand Meyer stipule que tout module (package, classe, méthode) doit être à la fois :

  • Ouvert aux extensions : le module peut être étendu pour proposer des comportements qui n'étaient pas prévus lors de sa création.
  • Fermé aux modifications : les extensions sont introduites sans modifier le code du module.
En d'autres termes, l'ajout de fonctionnalités doit se faire en ajoutant du code et non en éditant du code existant.

1. L'abstraction comme moyen d'ouverture/fermeture

L'ouverture/fermeture se pratique en faisant reposer le code "fixe" sur une abstraction du code amené à évoluer. En d'autres termes, l'OCP consiste à séparer le commun du variable, en faisant reposer le commun sur une définition stable du variable.

Voyons ceci sur un exemple :

Dans le schéma suivant, A gère les cas c1 et c2 dans les deux méthodes foo() et bar() dont le code est représenté à côté. Si un nouveau cas c3 doit être géré, il faut modifier le code de A en conséquence :



Le code de A peut être ouvert/fermé aux modifications en introduisant une interface I dont dérivent des classes C1 et C2 correspondant aux cas c1 et c2 :



Puisque A repose uniquement sur l'interface I, il devient possible d'ajouter un nouveau cas c3 sous la forme d'une classe C3 dérivée de I, sans avoir à modifier A :



2. L'abstraction comme moyen d'ouverture/fermeture

Soit la classe rectangle définie comme suit :



On cherche à calculer l’aire de plusieurs rectangles. On crée donc une autre classe AreaCalculator qui ne s’occupe que de ça. Elle a le code suivant :



Nous voilà avec une solution qui devrait bien fonctionner. Mais maintenant on souhaite ajouter la possibilité de calculer l'air de cercles en plus des rectangles.
On modifie alors la classe AreaCalculator et on ajoute une classe Circle :



Voilà, nous avons modifié notre code pour répondre aux nouvelles exigences mais cela pose certains problèmes…

En effet, on peut très justement se demander pourquoi se limiter aux rectangles et aux cercles et c’est pourquoi on demande d’ajouter la possibilité de calculer l’aire de triangles et d’autres formes.

Pour cela on va devoir modifier une énième fois la classe AreaCalculator au risque de casser ce qui fonctionne. La classe AreaCalculator n'est donc pas fermée à la modification.
Et pourtant il semblerait judicieux qu’elle le soit étant donné le nombre de modification déjà apporté et les éventuelles modifications à venir. C’est pourquoi on va appliquer l'OCP sur AreaCalculator.

Pour se faire, nous allons créer un Interface Shape avec une methode area() se chargeant de calculer l'air d'un Shape.



Maintenant on va faire en sorte que les classes Rectangle et Circle implémente l'interface Area :



A partir de ce modèle, on peut réaliser un algorithme de calcul d'air capable de faire completement abstraction du type de "Shape" :



Dorénavant l'ajout d'une nouvelle forme géométrique ne passe plus par la modification de l'algorithme du calcul d'air, mais uniquement par l'ajout d'une classe implémentant l'interface Shape ce qui rendra cette nouvelle classe utilisable par l'algorithme de calcul d'air.

AreaCalculator et sa methode de calcul d'air est alors fermée à la modification et ouverte à l’évolution.

3. L'application de l'OCP est un choix stratégique

L'OCP est un principe incontournable lorsque l'on parle de flexibilité du code. Par contre, une erreur classique consisterait à vouloir ouvrir/fermer toutes les classes de l'application en vue d'éventuels changements. Cela constitue une erreur dans la mesure où la mise en oeuvre de l'OCP impose une certaine complexité qui devient néfaste si la flexibilité recherchée n'est pas réellement exploitée.

Il convient donc d'identifier correctement les points d'ouverture/fermeture de l'application, en s'inspirant :
  • Des besoins d'évolutivité exprimés par le client,
  • Des besoins de flexibilité pressentis par les développeurs,
  • Des changements répétés constatés au cours du développement.
La mise en oeuvre de ce principe reste donc une affaire de bon sens, sachant que la meilleure heuristique reste la suivante : on n'applique l'OCP que lorsque cela simplifie le design.

4. L'OCP s'applique également dans une démarche itérative

En préconisant d'anticiper le changement, ce principe semble s'opposer à certains principes d'Extreme Programming ("You ain't gonna need it" et "Do the simplest thing that could possibly work"). La contradiction n'est cependant qu'apparente :
  • L'OCP a pour but de réduire le coût du changement dans le logiciel : il facilite donc en cela l'approche itérative/incrémentale.
  • L'ouverture/fermeture du code n'est pas obligatoirement faite dans un design "up-front" : elle peut (doit) au contraire n'être mise en place qu'au cours de l'activité permanente de refactoring, lorsque les cas concrets d'extension se présentent.