mercredi 29 mai 2013

Le « O » de SOLID : Open/Closed Principle (OCP)


Au sein de la programmation orientée objet, le principe « Ouvert/Fermé » est le deuxième principe de SOLID, et s'applique aux différents concepts manipulés : Assembly, Class, Method. Selon la définition de Bertrand Meyer :

« Software entities should be open for extension, but closed for modification ».


On retrouve dans cette phrase un paradoxe intéressant : les entités doivent être « ouvertes » pour étendre leur comportement, tout en étant « fermées » à la modification. Sous entendu, une entité autorise la modification de son comportement, sans en altérer le code source originel. Comment résoudre ce paradoxe ? Comme l'explique notre cher B.M, l'abstraction est la clé.

Lorsqu'on souhaite effectuer des modifications sur un code existant pour ajouter de la fonctionnalité, on « étend » le concept, en ajoutant du nouveau code, par héritage et polymorphisme. On ne modifie en aucun cas le concept originel pour arriver à notre besoin. Si on le faisait, on risquerait de le rendre :
  • fragile : remplacer du code déjà éprouvé par du nouveau code non éprouvé fragilise le concept
  • rigide : l'ajout de code spécifique rigidifie le concept et peut même ne plus respecter le SRP
  • non prévisible : des effets de bords importants peuvent apparaître
  • non réutilisable : on risque de spécialiser le concept et de le rendre moins voir non réutilisable

Exemple

Soit une classe voiture, « Car », qui hérite de la classe véhicule, « Vehicule ». Il existe une méthode abstraite « Drive() » de la classe « Vehicule » à surcharger dans les classes dérivées.


Ci-dessous un exemple d’implémentation, la classe Vehicule :
public abstract class Vehicule
{
    public abstract void Drive();
}

Classe Car :
public class Car : Vehicule
{
    public enum CarType
    {
        Fuel,
        Gazole,
        Electric
    }

    public CarType Type { get; set; }
    public int LiquidInReservoir { get; set; }
    public int Kms { get; set; }

    public override void Drive()
    {
        if (Type != CarType.Electric) {
            LiquidInReservoir--;
        }
        Kms++;
    }
}

Comme vous vous en doutez, cette implémentation de la classe « Car » ne suit pas le principe Ouvert/Fermé. En effet, si l’on souhaite ajouter un nouveau type de voiture, comme par exemple, le type « Hydrogen », qui, de la même manière que pour les véhicules électriques, ne consomme pas de carburant liquide, nous sommes obligés de « modifier » le code source de la classe « Car ».

La condition deviendrait :
if (Type != CarType.Electric && Type != CarType.Hydrogen) {
    LiquidInReservoir--;
}

Si la classe était implémentée au sein d’une librairie externe, nous n’aurions pas eu d’autres choix que de la recoder entièrement pour qu’elle corresponde à notre nouveau besoin. En utilisant l’héritage et le polymorphisme sur le concept de « Car », nous pouvons changer son code afin qu’elle respecte le principe OCP :

La classe « Car » devient abstraite, et requiert l’implémentation d’une nouvelle méthode « ConsumeEnergy ». L’énumération « CarType » n’a plus lieu d’exister, et est remplacée par « FuelType », uniquement pour les voitures à essence, « FuelCar ». Le code suivant illustre une implémentation possible :

La classe de base Vehicule :
public abstract class Vehicule
{
    public abstract void Drive();
}

La classe abstraite Car :
public abstract class Car : Vehicule
{
    public int Kms { get; set; }

    public override void Drive()
    {
        ConsumeEnergy();
        Kms++;
    }
    public abstract void ConsumeEnergy();
}

La classe FuelCar :
public class FuelCar : Car
{
    public enum FuelType
    {
        Fuel,
        Gazole
    }

    public int LiquidInReservoir { get; set; }
    public FuelType Type { get; set; }

    public override void ConsumeEnergy()
    {
        LiquidInReservoir--;
    }
}

La classe ElectricCar :
public class ElectricCar : Car
{
    public int BatteryCapacity { get; set; }

    public override void ConsumeEnergy()
    {
        BatteryCapacity--;
    }
}

Le code pour une voiture à hydrogène serait :
public class HydrogenCar : Car
{
    public int HydrogenCapacity { get; set; }

    public override void ConsumeEnergy()
    {
        HydrogenCapacity--;
    }
}

Le principe OCP est bien respecté, l’ajout de nouveaux véhicules, afin d’« étendre » le périmètre fonctionnel, ne « modifie » pas le code source de la classe de base « Car ».


Conclusion

Ce principe est très important sur le point de vue de la réutilisabilité du code. Il aide le développeur à encapsuler les comportements au sein d’une hiérarchie logique de classe. Les classes de base se voient attribuer un périmètre fonctionnel faible, ce qui facilite leur écriture et garantit leur robustesse. Le code qui les compose devient vite mature, testé, et fiable.

OCP et SRP sont essentiels pour appréhender l’architecture logicielle : ils constituent des fondements solides à responsabilité unique et permettent de faire évoluer très facilement les concepts grâce à l’extension.


Prochain article : L pour Liskov Substitution Principle



4 commentaires:

  1. C'est d'ailleurs pour ça que certains programmeurs crisses des dents lorsqu'ils voient un switch/case dans du code, car en règle générale c'est un indicateur montrant que la classe peut potentiellement violer le principe OCP ! :p

    RépondreSupprimer
  2. Exactement. C'est réel indicateur ! De même que les "if", "else if", en cascade sur 100 lignes.

    Un autre bon moyen pour détecter les ruptures avec OCP, est de se restreindre à faire du "Clean Code" (Merci Robert C. Martin) avec la règle des 5 lignes par méthode. La segmentation des responsabilités en méthode fait naturellement émerger du polymorphisme.

    RépondreSupprimer
  3. Autant je suis tout à fait d'accord, autant le problème que j'ai toujours eu avec OCP (moins qu'avec DRY mais quand même :)) est que "On ne modifie en aucun cas le concept originel pour arriver à notre besoin" est souvent détourné en "On ne doit pas toucher du code déjà en prod". Alors que l'amélioration de la qualité du code nécessite de refactorer le code existant :) Et amélioration du code != ajout d'une fonctionnalité dans le code.

    RépondreSupprimer
    Réponses
    1. C'est vrai que le sens premier est souvent détourné.
      Mais modifier du code existant déployé en production pour injecter de l'OCP afin d'améliorer la lisibilité en vue d'ajouter une autre fonctionnalité est très risqué. Sur de la fonctionnalité neuve, c'est plus facile de penser les choses en amont pour préparer OCP. Dans les autres cas, la seule solution consiste souvent à ré-écrire une bonne partie du code et c'est là que la testabilité devient indispensable. Pensons "E.T.R.A" !

      Supprimer