Le principe Ouvert/Fermé
Second principe appartenant à la gamme SOLID, le principe ouvert/fermé (Open/Closed principle) encourage la conception de logiciels extensibles sans nécessité de modifications directs sur le code existant.
Le Principe Ouvert/Fermé est formulé par Bertrand Meyer et stipule : “Les entités logicielles (classes, modules, fonctions, etc.) doivent être ouvertes à l’extension, mais fermées à la modification”. En d’autres termes, cela signifie que vous devez concevoir votre code de manière à ce qu’il puisse être étendu pour répondre à de nouveaux besoins ou fonctionnalités, sans avoir à modifier directement le code existant.
Respecter ce principe est extrêmement puissant puisque les éléments de votre code deviennent interchangeables facilement et sans impact au-delà du composant qu’on ajoute. On peut voir ça comme le fait de remplacer une batterie de voiture par une plus performante, cela se fait sans avoir à démonter le moteur ni le moindre risque pour le reste des éléments… À condition de respecter les spécifications de la batterie.
Prenons un cas concret pour illustrer les avantages de ce principe. Imaginons que nous ayons un système qui représente une machine à café. Cette machine permet de réaliser les cafés suivants :
Voici maintenant le code de la machine à café :
On remarque que dans cette implémentation de la machine à café, on sera obligé de modifier cette classe à chaque modification concernant les cafés. Il faudra rajouter une clause dans la sélection pour introduire un nouveau café. Il faudra également modifier ce code si la façon de préparer un café change. Imaginons qu’on rajoute une étape à la préparation d’un cappuccino. Il faudra alors aller dans la classe machine à café pour introduire cette nouvelle étape.
Ici, la machine à café, n’est pas fermée aux modifications. C’est le seul moyen d’étendre son comportement. Elle est également couplée à la préparation de chaque café.
Voici maintenant un refactoring qui permet de suivre le principe ouvert/fermé en partant du même problème.
Tout d’abord, on introduit une interface, Coffee, dont l’objectif est de garantir un contrat unique avec la machine à café.
Maintenant, tout nos cafés vont implémenter cette interface et vont donc répondre au même contrat.
Vous remarquerez que seule la méthode qui vient de l’interface est publique. Les autres méthodes ont été passées en privé. En faisant cela, on encapsule les détails concernant chaque café dans leur propre classe.
Voici maintenant l’impact sur notre machine à café :
L’introduction de l’interface nous a permis de retirer toute la complexité de la préparation de chaque café de la classe. Maintenant, notre machine à café est protégée des modifications des autres classes. Je vous vois venir, vous allez me dire qu’on a toujours le bloc conditionnel pour la sélection d’un café. Donc, en ajoutant un nouveau type de café, on devra obligatoirement modifier le code de la machine à café.
Je ne vais pas vous mentir, dans un cas celui-là, on aura toujours, quelque part, besoin de faire le lien entre le paramètre de la sélection et le café à préparer. Il aura donc besoin d’un bout de code similaire à celui-là pour le faire.
Il y a néanmoins une amélioration intéressante à proposer pour la classe CoffeeMachine.
Ici, on extrait simplement la sélection du café dans une classe dédiée. On introduit ce sélecteur comme une dépendance de la machine à café. Maintenant, quel que soit le changement qu’on introduit, ajout d’un nouveau style de café ou modification dans la préparation d’un café, il n’y aura plus d’impact sur la classe CoffeeMachine. Cette classe respect bien le principe Open/Close. Elle est ouverte à l’extension, on peut lui passer un autre type de sélecteur ou introduire de nouveau café facilement. En revanche, elle est fermée aux modifications qui viennent de l’extérieur.
Notez au passage que ce refactoring nous a permis de respecter le Single Responsibility Principle dont je parle ici.
Pour finir sur le code voici un exemple de sélecteur de café. Cette classe sera toujours dépendante de l’introduction de nouveau type de café et devra être modifiée en conséquence. Pour ce genre de classe, c’est un compromis qu’on fait. Cela ne pose pas de problème tant que la seule responsabilité de cette classe, c’est de faire le mapping entre un paramètre et la classe qu’elle doit fournir.
Conclusion
Ce qu’il faut avoir en tête pour respecter ce principe, c’est qu’une entité logicielle doit pouvoir être impacté sans que son code n’ait besoin de changer. Pour ça, il faut que son comportement puisse être étendu. Dans le cas de notre machine à café, c’est l’introduction d’une interface commune à tous les cafés et d’un sélecteur qui nous permet d’étendre les capacités de notre machine sans avoir à modifier son propre code.
Ce qui est intéressant dans cet exemple, pourtant simple, c’est que pour rendre le code compatible avec le principe ouvert/fermé nous avons du faire appel à des techniques de programmation qui correspondent aux trois derniers principes de SOLID. Nous verrons cela dans les posts à suivre sur le sujet.