Cette article est la suite d’une série de plusieurs articles sur les principes SOLID (je vous invite à relire l’article Conception d’applications SOLID pour en savoir plus). L’article précédent était consacré à SRP (Single Responsability Principle).
Principe d’ouvert/fermé (OCP: The Open/Closed Principle)
Présentation d’OCP
Le principe s’énonce de la manière suivante : Une classe doit être extensible sans modifier son code.
En appliquant ce principe, les modules développées ont deux attributs principaux selon Robert C. Martin :
« 1 – Ils sont « ouverts pour l’extension ». Cela signifie que le comportement du module peut être étendu, que l’on peut faire se comporter ce module de façons nouvelles et différentes si les exigences de l’application sont modifiées, ou pour remplir les besoins d’une autre application.
2 – Ils sont « Fermés à la modification ». Le code source d’un tel module ne peut pas être modifié. Personne n’est autorisé à y apporter des modifications. »
Le but de ce principe est de mettre en place des classes pour lesquelles il sera possible d’ajouter de nouveaux comportements sans modifier le code de la classe elle-même.
En effet, l’impact d’un changement dans le code est souvent risqué et coûteux.
Comment mettre en pratique OCP ?
L’utilisation d’OCP est possible grâce à plusieurs mécanismes :
- L’abstraction
- L’utilisation de delegates
- Les génériques
- Les design patterns
La mise en place du principe d’ouvert/fermé avec l’abstraction
Le principe est de séparer votre code en deux parties :
- une partie abstraite qui regroupe le code commun
- une partie concrète qui propose une implémentation spécifique pour un comportement donné
En C#, la technique la plus simple est d’utiliser des classes « abstract » avec des méthodes « virtual » ou « abstract » qui pourront être surchargées. Les méthodes « abstract » devront être surchargées par le développeur.
Vous pouvez également utiliser des interfaces qui sont plus simples à rédiger.
La mise en place du principe d’ouvert/fermé avec les delegates
Une autre manière de proposer un comportement extensible est l’utilisation de delegates.
En C#, les delegates permettent de définir des références vers des méthodes qui pourront être écrites en dehors de la classe. Ceci correspond donc bien au principe d’OCP.
La mise en place du principe d’ouvert/fermé avec les generics
Les generics ou classes paramétrées sont un autre moyen pour permettre de faire évoluer le comportement d’une classe sans en modifier son code.
L’exemple le plus utilisé dans le framework sont les collections génériques.
L’utilisation de List
La mise en place du principe d’ouvert/fermé avec les design patterns
Plusieurs designs patterns permettent de mettre en oeuvre le principe d’ouvert/fermé, comme par exemple :
- Strategy (Plus d’infos sur Wikipedia) : le code d’origine est ouvert/fermé. Il permet l’ajout de nouveaux algorithmes grâce à l’implémentation d’une classe qui respecte un contrat décrit par une interface commune (par exemple IStrategie dans l’exemple sur Wikipedia). Le comportement de la classe d’origine pourra évoluer en fonction des nouveaux algorithmes.
- Abstract factory (Plus d’infos sur Wikipedia) : le code de la classe factory est ouvert/fermé à la création de nouveaux objets implémentant tous une interface commune.
- Visitor (Plus d’infos sur Wikipedia) : la structure parcourue est ouverte/fermée à l’ajout de nouveaux algorithmes de traitement.
Un exemple concret du principe d’ouvert/fermé
Je vous propose de partir sur un exemple très simple mais qui permet d’illustrer le principe.
Nous allons travailler sur des rectangles pour commencer.
Voici la déclaration d’une classe qui contient la taille d’un rectangle :
{
public class Rectangle
{
public int Width { get; set; }
public int Height { get; set; }
}
}
[/csharp]
Notre client nous demande de mettre en place une méthode pour calculer la surface d’un rectangle.
Naturellement, nous pouvons créer une classe qui s’occupe de faire le calcul :
{
public class AreaCalculator
{
public int ComputeArea(Rectangle r)
{
return r.Width * r.Height;
}
}
}
[/csharp]
Très simple, n’est-ce pas !
Notre client revient nous voir et désire désormais que l’on puisse calculer la surface d’une liste de rectangle. Pas de problème, il suffit de faire la somme.
Nous modifions la classe AreaCalcultator en ajoutant une nouvelle méthode donc voici le code :
[csharp] public int ComputeArea(List<Rectangle> rectangles){
int total = 0;
foreach (var oneRectangle in rectangles)
{
total = total + ComputeArea(oneRectangle);
}
return total;
}
[/csharp]
Un peu plus tard dans le projet, notre client nous demande de pouvoir faire les calculs sur des disques.
Nous ajoutons donc une classe Disc :
[csharp] namespace OcpDemo{
public class Disc
{
// Rayon du disque
public int Radius { get; set; }
}
}
[/csharp]
Et nous ajoutons également une méthode pour calculer la surface : S = PI * R².
[csharp] public int ComputeArea(Disc d){
return (int)(Math.PI * d.Radius * d.Radius);
}
[/csharp]
Bien sûr, il faut aussi pouvoir calculer la surface d’une liste de disques. Là, vous vous dites qu’il est sûrement possible de factoriser du code.
Si on applique le principe OCP (ouvert aux évolutions, fermé aux modifications), il faudrait que la classe AreaCalculator puisse calculer d’autres surfaces sans être modifiée dans le futur.
Nous allons donc faire du refactoring pour simplifier et améliorer tout ça.
Refactoring 1: Réduire le couplage entre les classes
Je vous ai parlé récemment du principe SRP (Responsabilité unique – une seule raison de changer). Actuellement, il y a un couplage fort entre Rectangle (ou Disc) et AreaCalculator.
Le fait de modifier la classe Rectangle demandera également de modifier AreaCalculator.
Pour réduire ce couplage, il suffit de déplacer la méthode de calcul dans l’objet sur lequel le calcul est fait.
Le code devient alors ceci pour le rectangle (nous allons faire la même chose pour Disc) :
[csharp] namespace OcpDemo{
public class Rectangle
{
public int Width { get; set; }
public int Height { get; set; }
public int ComputeArea()
{
return Width * Height;
}
}
}
[/csharp]
Refactoring 2 : Utilisation de l’abstraction pour ouvrir aux évolutions
Actuellement, notre classe AreaCalculator sait effectuer des calculs sur des rectangles et des disques. Mais comment pouvons nous la faire évoluer pour supporter d’autres formes dans l’avenir ?
Très simplement grâce à l’abstraction.
Nous allons donc mettre en place une classe abstraite « Shape » (forme) qui contient une seule méthode « ComputeArea ». En C#, nous pouvons faire ceci avec une interface.
Voici le code de l’interface :
[csharp] namespace OcpDemo{
public interface IShape
{
int ComputeArea();
}
}
[/csharp]
Nous adaptons ensuite Rectangle et Disc pour implémenter cette interface. La classe AreaCalculator est elle-aussi adaptée pour être ouverte à de nouvelles formes :
[csharp] public int ComputeArea(List<IShape> shapes){
int total = 0;
foreach (var aShape in shapes)
{
total = total + aShape.ComputeArea();
}
return total;
}
[/csharp]
Conclusion
Au travers d’un exemple très simple, vous avez pu découvrir comment mettre en place le principe OCP. Je vous invite donc à essayer vous aussi à utiliser OCP car il est très simple à utiliser au quotidien et vous apportera plus de flexibilité et une meilleure évolutivité dans votre code.
Le projet de démonstration est téléchargeable ici : OcpDemo.
Pour continuer dans cette série, je vous invite à lire le prochaine article sur LSP (Liskov Substitution Principle).