Résumé et application des principes SOLID.


L’objectif de cet article est de présenter les 5 principes SOLID en situation réelle, dans un seul programme. Pour cela, je baserais mon étude de cas sur une bibliothèque (sans UI) visant à catégoriser du matériel informatique.

Si vous trouvez qu’un de ces principes n’est pas clair ou que vous n’en voyez pas l’utilité, je vous invite à aller jeter un coup d’œil à son article correspondant, ou à me le faire savoir dans les commentaires tout en bas.


Single Responsability Principle (SRP)

Regroupe les choses qui changent pour la même raison. Sépare les choses qui changent pour des raisons différentes.
Robert C. Martin

Le principe de responsabilité unique dit qu’un module ne doit pas faire plus d’une chose à la fois. Si c’est le cas, alors il faut le diviser en plusieurs modules.

Cela a un impact majeur sur l’organisation et la lisibilité du code.

Si les modules sont rangés à leur place en étant relativement isolés du reste, alors il est bien plus facile de les retrouver et de les distinguer. Sinon, cela revient à chercher une aiguille dans une botte de foin.

Ça semble logique, mais il est très facile de se faire piéger, comme dans cette situation :

public class PersonalComputer 
{
    /// <summary>
    /// Desktop ou Laptop
    /// </summary>
    public static string Type { get; set; }

    /// <summary>
    /// Déplacement du material
    /// </summary>
    public void ChangeLocation()
    {
        if(Type == "Desktop")
        {
            // Fait quelque chose
        }
        else if(Type == "Laptop")
        {
            // Fait autre chose
        }
    }
}
La fonction adapte son comportement au type du PC, et donc joue le rôle de PC de bureau et PC portable.

Un moyen simple de détecter les violations du SRP est d’essayer de décrire le comportement d’une fonction sans utiliser les mots et, ou, si… Si vous y arrivez aisément, c’est très bon signe.

Aussi, méfiez-vous des conditions (if, switch…), des catégories (Type…) et des drapeaux. Une solution bien plus propre au problème ci-dessus serait :

public class PersonalComputer { }

public class DesktopComputer : PersonalComputer 
{ 
    public void ChangeLocation()
    {
        //...
    }
}

public class LaptopComputer : PersonalComputer 
{
    public void ChangeLocation()
    {
        //...
    }
}
Les parties PC de bureau et PC portable sont séparés dans deux classes différentes, mais gardent la même racine commune.

Open/Closed Principle (OCP)

Un module doit être ouvert à l’extension, mais fermé à la modification.
Robert C. Martin

Le principe ouvert/fermé dit qu’à partir du moment où un module est déployé en production, il ne faut plus jamais le modifier. Tout changement se ferra en étendant le module, c’est-à-dire en en ajoutant un autre à côté.

La raison, c’est que chaque modification sur un module existant est source d’erreur : on prend le risque que le module ne marche plus, ou devienne incompatible avec ses dépendances, et donc qu’il plante complètement le programme.

Or, si l’on ne fait qu’étendre le module, son cœur reste inchangé, on peut donc profiter des modifications tout en assurant la stabilité du programme à 100%

On peut arriver à ce résultat grâce à de l’abstraction. Lorsqu’un programme est correctement abstrait, il devient facile d’y insérer des nouveaux modules sans toucher aux précédents.

C’est comme construire un puzzle tout en laissant de la place pour d’éventuelles nouvelles pièces.

Ainsi, l’OCP pousse à écrire beaucoup de modules, mais qui sont légers, relativement indépendants, ultra-compatibles entre eux et qui respectent le SRP.

class Computer { }

class Server : Computer { }
class MobilePhone : Computer { }
class PersonalComputer : Computer { }

class DesktopComputer : PersonalComputer { }
class LaptopComputer : PersonalComputer { }
Avec ce modèle, il est très facile d'étendre le programme en ajoutant de nouvelles classes.

L’OCP est de très loin le principe SOLID le plus difficile à respecter à 100% tant il demande de rigueur. C’est un idéal.

Cependant, tout n’est pas en noir et blanc, et même si vous n’arrivez pas à l’appliquer totalement, gardez-le toujours dans un coin de votre tête, car c’est une des meilleures manières de rendre une codebase robuste et flexible.


Liskov Substitution Principle (LSP)

Un programme utilisant une interface ne doit pas être perturbé par une implémentation de cette interface.
Robert C. Martin

Le principe de substitution de Liskov dit que toutes les implémentations d’une interface doivent accomplir les mêmes changements d’état et les mêmes effets secondaires.

De cette manière, on n’a jamais de surprise lorsque l’on fait du polymorphisme, car le résultat est relativement prédictible, peu importe le type de l’instance appelée.

Le but étant de prévenir toutes les sources d’instabilité liées au sous-typage (implémentation d’interfaces, sous-classes, types génériques)…

Cet exemple est typique d’une violation du LSP, car même si le résultat est relativement le même, les effets secondaires ne le sont pas du tout :

public class PersonalComputer 
{ 
    public virtual void ChangeLocation()
    {
        //...
    }
}

public class DesktopComputer : PersonalComputer 
{ 
    public override void ChangeLocation()
    {
        // Eteindre l'ordinateur
        // Débrancher tout le matos
        // Déplacer l'ordinateur
        // Déplacer le matos
        // Rebrancher tout
        // Ralumer l'ordinateur
    }
}

public class LaptopComputer : PersonalComputer 
{
    public override void ChangeLocation()
    {
        // Changer de place
    }
}
Grosse source d’instabilité, car en fonction du type de PC, les effets secondaires ne sont pas les mêmes, car l’un nécessite une extinction + débranchement alors que l’autre rien de tout ça.

Autrement dit, pour une même API, l’impact sur le programme n’est pas du tout le même.

Pour prévenir de ce problème, il faut baser son sous-typage non pas sur la définition des API (signature de fonction), mais sur leur implémentation.

Dans l’exemple au-dessus, le sous-typage n’a pas lieu d’être, car les deux fonctions ne font clairement pas la même chose. Il est donc bien plus légitime d’enlever complètement cette couche d’abstraction :

public class PersonalComputer { }

public class DesktopComputer : PersonalComputer 
{ 
    public void ChangeLocation() { }
}

public class LaptopComputer : PersonalComputer 
{
    public void ChangeLocation() { }
}

Interface Segragation Principle (ISP)

Créer des interfaces courtes pour que les utilisateurs n’aient pas à dépendre de choses dont ils n’ont pas besoin.
Robert C. Martin

Le principe de ségrégation des interfaces dit qu’un programme ne devrait pas pouvoir utiliser des API donc il n’a pas besoin. Il faut donc filtrer les API dans plusieurs interfaces.

Le but est surtout de faciliter le travail des développeurs. Il est beaucoup plus facile de travailler avec une boite contenant 3 outils plutôt qu’une autre en contenant 10 000, surtout s’ils se ressemblent tous.

Pour appliquer ce principe, il faut donc définir les différents rôles du module, et créer une interface pour chaque rôle.

public class Server : IWebHost, IFileServer, IPrintServer { }

public interface IWebHost { }

public interface IFileServer { }

public interface IPrintServer { }

Dependancy Inversion Principle (DIP)

Les modules de haut-niveau et de bas-niveau doivent tout deux dépendre d’abstractions, et les abstractions ne doivent jamais dépendre de leurs implémentations concrètes.
Robert C. Martin

Le principe d’inversion des dépendances dit que deux modules ne doivent pas s’accoupler directement ensemble, mais plutôt dépendre d’une abstraction commune, elle-même indépendante de ses implémentations.

Le but est d’éviter une cohésion trop forte entre deux modules, car sinon, le programme risque de perdre en flexibilité.

Si deux modules sont interdépendants, alors le jour où on décide d’ajouter une nouvelle pièce au puzzle, on est obligé d’en enlever d’autres, et donc de violer l’OCP.

public class PersonalComputer
{ 
    public IntelCorei5 Processor { get; set; }
}

public class IntelCorei5
{
    public int CoreNumber { get; set; }
    public long Frequency { get; set; }
}
Si on décide d’ajouter une nouvelle gamme de processeur, il faut alors modifier la classe PersonalComputer, et donc prendre le risque de casser toutes ses dépendances.

Grâce au DIP, n’importe quel module pourra communiquer avec n’importe quel autre, tant que ceux-ci dépendent de la même couche d’abstraction (type abstrait, interface…).

Pour l’appliquer correctement, il faut être particulièrement attentif lorsque l’on fait de la composition d’objets, car c’est en général ici que se cachent les problèmes.

Une meilleure alternative au programme ci-dessus serait :

public class PersonalComputer
{ 
    public ICPU Processor { get; set; }
}

public interface ICPU
{
    int CoreNumber { get; set; }
    long Frequency { get; set; }
}

public class IntelCorei5 : ICPU
{
    public int CoreNumber { get; set; }
    public long Frequency { get; set; }
}

public class IntelCorei7 : ICPU
{
    public int CoreNumber { get; set; }
    public long Frequency { get; set; }
}
J’ai ajouté une couche d’abstraction entre PersonalComputer et les processeurs pour que les PC ne dépendent pas d’un seul en particulier.

Voilà pour ce résumé des principes SOLID. Si vous voulez plus de détails sur l’un d’entre eux, vous pouvez retrouver tous mes articles sur le sujet ici. À bientôt sur le blog des développeurs ultra-efficaces.