Les principes SOLID appliqués en C# (2/5)
Publié le 16/08/2022
Par  Christophe MOMMER

Avant toute chose, il faut savoir que chacun de ces principes peut également être découvert en vidéo. La vidéo pour le second principe se trouve ici : https://youtu.be/Uv15ZRFkUO0

O pour Open Closed Principle

Le second principe dans la liste : Open Closed Principle, ou, en français, le principe ouvert/fermé. Comment peut-on être ouvert et fermé à la fois ? Il suffira d'être ouvert à une chose et fermé à l'autre...

De quoi parle-t-on exactement ?

Le principe Open Closed est assez simple : votre code doit être ouvert à l'extension mais fermé à la modification. Bien sûr, pour ce dernier point, un petit bémol : la modification du code pour correction de bug est TOUJOURS autorisée... (n'allez pas utiliser ce billet de blog pour trouver une excuse à ne pas corriger votre code !)

En fait, ce principe s'applique au code métier/business. Il faut simplement considérer la chose suivante : votre code doit naturellement offrir des points d'extension pour que les nouveaux cas d'usages qui apparaissent puissent être ajoutés sans que vous ayez à modifier votre classe. Il existe beaucoup de principes dans la programmation orientée objet pour vous aider à cela, et dans la vidéo, je vous montre cela avec l'héritage. 

Prenons l'exemple suivant : j'ai une classe qui calcule la surface de formes géométriques, et j'ai deux formes de définies :

public class Square
{
    public int Side { get; set; }
}

public class Rectangle
{
    public int Height { get; set; }
    public int Width { get; set; }
}

public class AreaCalculator
{
    public int ComputeArea(object shape)
    {
        if(shape is Square s) return s.Side * s.Side;
        else if(shape is Rectangle r) return r.Height * r.Width;
        return 0;
    }
}

On peut voir une succession de if et else if dans la méthode pour gérer les différents cas de figure. Que se passe-t-il si une nouvelle forme fait son apparition ? Il sera nécessaire de modifier la méthode précédent afin de rajouter un else if. Cela devrait vous mettre la puce à l'oreille : on doit modifier notre code pour gérer un nouveau cas !

OK, mais que faire alors ?

Ici, la POO peut nous sauver grâce à l'héritage. Il faudra déplacer le comportement de calcul dans une classe commune à toutes les formes, et faire en sorte que la classe AreaCalculator soit la consommatrice de cette méthode. Ainsi, les nouvelles formes devront déclarer comment leur surface est calculée ! 

public abstract class Shape
{
    public abstract int ComputeArea();
}

public class Square : Shape
{
    public int Side { get; set; }
    public override int ComputeArea() => Side * Side;
}

public class Rectangle: Shape
{
    public int Height { get; set; }
    public int Width { get; set; }
    public override int ComputeArea() => Height * Width;
}

public class AreaCalculator
{
    public int ComputeArea(Shape shape)
    {
        return shape.ComputeArea();
    }
}

On voit déjà une amélioration rapide : plus d'utilisation de la classe object (et donc de boxing/unboxing avec des appels potentiellement faux) mais ici une utilisation d'une définition de base commune ! Finalement, ajouter une nouvelle forme n'aura aucun impact sur notre classe AreaCalculator, le cas sera automatiquement géré ! 

Et dans les applications réelles ?

Ici, effectivement, le cas est simple et la POO nous sauve... Ce n'est pas toujours le cas dans dans "la vie réelle". Le DDD (Domain Driven Design), grâce aux domain events et les pattern de messaging, permet notamment de faire du code naturellement extensible. Il suffira de brancher de nouveaux handlers pour gérer de nouveaux cas business. Cela dépasse le cadre de ce billet, mais je vous invite à creuser le sujet si cela vous intéresse !