Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Fathi Bellahcene

.Net m'a tuer!
LSP: Liskov Substitution Principle

Ce post fait partie d’une serie se proposant de présenter les principes SOLID. Vous  trouverez l’introducton ainsi que les liens vers les autres articles ici .

Avec le principe de substitution de Liskov, on s’attaque au principe le plus difficile à comprendre mais aussi le plus interressant et celui qui a le plus grand impact dans le code que l’on est amené à écrire.

Je vais donc tenter de l’expliquer le plus simplement possible à travers ce post tout en abordant le maximum de choses.

Comprendre l’objectif du principe

Le principe de substitution de Liskov nous dit (définition de Robert C. Martin) :

Les méthodes qui utilisent les objets d'une classe doivent pouvoir utiliser des objets dérivés de cette classe sans même le savoir.

En d’autres termes:

Une classe de base doit être substituable par ses classes dérivées sans que cela nécessite des modifications dans la méthode qui utilise ces classes.

Si on prend le problème dans l’autre sens, on a quelque chose d’un peu plus compréhensible:

Si je souhaite faire une classe A qui hérite d’une classe B, je dois m’assurer de ne pas introduire de modifications dans son fonctionnement qui rend inutilisable tout objet de type A utilisé comme un objet de type B.

toujours pas clair?…. c’est normal, prenons un exemple concret:

 

Ca ressemble à un canard, fait le même bruit qu’un canard mais ca a besoin de batteries, vous avez probablement la mauvaise abstraction!

L’idée qui se cache derrière cette image est que le canard électrique est une spécialisation du canard naturel (la classe canard électrique hérite de la classe canard)…et qae cela pose un problème parce que le canard électrique à un besoin  impératif de batterie pour fonctionner contrairement au canard naturelle.

Regardons le code maintenant:

   1: public class Duck
   2:    {
   3:        public DuckState State {get; private set;}
   4:  
   5:  
   6:        public virtual void Fly()
   7:        {
   8:            State = DuckState.Fly;
   9:        }
  10:    }
  11:  
  12:  
  13:    public enum DuckState
  14:    { 
  15:        Rest,
  16:        Fly
  17:    }

 

La classe canard possède une méthode Fly qui permet de positionner l’état du canard à Fly.

Regardons maintenant la classe Canard Electrique:

   1: public class ElectricDuck: Duck
   2:  {
   3:  
   4:      public bool SwitchOn { get; set; }
   5:      public override void Fly()
   6:      {
   7:          if(SwitchOn)
   8:              base.Fly();
   9:      }
  10:  }

Celle-ci hérite de la classe canard, possède une propriété SwitchOn qui correspond à l’allumage du canard et on modifie la méthode fly en vérifiant que le canard est bien allumé. Fonctionnellement cela est cohérent et techniquement l’héritage “semble” justifié.

Regardons le code suivant:

   1: private static void Exemple1(List<Duck> myList )
   2:       {
   3:           
   4:           int nbDucks = 0;
   5:  
   6:           foreach (var duck in myList)
   7:           {
   8:               duck.Fly();
   9:               if (duck.State == DuckState.Fly)
  10:                   nbDucks++;
  11:           }
  12:  

13: Debug.Assert(nbDucks == myList.Count, "All ducks are not in

the sky!!");

  14:       }

Si on exécute cette méthode (on fait voler les canards et on vérifies bien qu’il y en a 2 qui sont en train de voler) avec en paramètre les objets suivant:

   1: List<Duck> myList = new List<Duck>() { new Duck(), new Duck() };
   2: Exemple1(myList);

tout marche bien et on est content.

Maintenant introduisons nos fameux canard électriques:

1: List<Duck> myList = new List<Duck>() { new Duck(), new ElectricDuck(),

new Duck() };

   2: Exemple1(myList);

Et la …ca nous envoi une erreur à l’exécution! Pourquoi? parce que l’on ne respecte pas le principe de Liskov. reprenons la définition et complétons la avec les acteurs de cet exemple:

Les méthodes [La méthode Exemple1]  qui utilisent les objets d'une classe  [La classe Duck] doivent pouvoir utiliser des objets dérivés de cette classe  [La classe ElectricDuck] sans même le savoir.

Sauf que pour le coup, ca marche pas: le canard electrique n’ayant pas pu décoller on a une erreir…donc le principe de Liskov est violé Triste

On pourrait proposer la correction suivante:

   1: private static void Exemple1(List<Duck> myList )
   2:        {
   3:            
   4:            int nbDucks = 0;
   5:  
   6:            foreach (var duck in myList)
   7:            {
   8:  
   9:                if (duck is ElectricDuck)
  10:                    ((ElectricDuck)duck).SwitchOn = true;
  11:                duck.Fly();
  12:  
  13:  
  14:                if (duck.State == DuckState.Fly)
  15:                    nbDucks++;
  16:            }
  17:  
  18:            Debug.Assert(nbDucks == myList.Count, "All ducks are not in the sky!!");
  19:        }

Ca fonctionne sauf que la méthode Exemple1 a besoin de connaitre le type réel des objets pour pouvoir les traiter correctement…donc on viole Liskov ET au passage le principe OCP!

Au final, faire hériter la classe ElectircDuck de Duck est une mauvaise idée.

Le principe de Liskov est donc un critère qui va nous permettre de dire si l’héritage est cohérent du point de vue fonctionnel des clients ou pas.c’est EXACTEMENT CA ET RIEN DE PLUS!

Comment détecter les violations?

He bien pour le coup, la mère Liskov (c’est une femme au passage) elle ne nous dit rien; débrouiller vous Sourire

On doit donc trouver un moyen technique qui va nous permettre de définir comment être sûr qu’une classe qui hérite de “moi” ne va pas me faire violer le principe…Evaluons les réponses suivantes:

Je doit tester toute ies classes sur du code qui utilise mes classes de bases!

>>sauf que je ne maitrise pas toujours l’utilisation de mes classes et encore moins dans le cadre du développement d’un framework

Je cèle mes classes (sealed) et pas de polymorphisme!

>>Je dois quand même respecter OCP …et faire de la POO Sourire

J’utilise Code Contracts parce que j’ai lu le (super) blog de Fathi Sourire

>> Exact! tu est un super développeur et tu ira loin!

Monsieur Bertrand Meyer [Zorro] est arrivé !!

La solution est apporté par Monsieur Bertrand Meyer: pour valider nos classes dérivés et les modifications faites dans les méthodes modifiées, on utilise le paradigme de programmation  Design by contract. Je vous invite a lire ce post ici pour comprendre un peu comment fonctionne l’API CodeContract qui est à mon sens une réponse parfaite a cette problématique.

L’utilisation de ce concept dans les programmes consiste à établir des contraintes fonctionnelles dans l’utilisation des objets, ces contraintes définissant les conditions correctes d’utilisation d’un objet.

Il existe trois types de contrat :

· Les pré-conditions : tester l’état des objets avant le traitement (tester les paramètres)

· Les post-conditions : tester l’état des objets à la fin du traitement (tester les objets modifiés ou les résultats produits par le traitement)

· Les invariants : s’assurer que certains objets ne changent jamais pendant un traitement

Les modifications effectuées dans les sous-types sont donc encadrées par le biais de contrats « fonctionnels ». Cependant, il faut s’assurer que les fonctionnalités dans les sous-types respectent toujours les mêmes contraintes. Dans le cas où l’on souhaite utiliser le polymorphisme pour faire évoluer les fonctionnalités, les règles suivantes doivent être respectées…sinon pas de Liskov! :

· Aucune modification des invariants dans une classe dérivée

· Aucun renforcement des pré-conditions dans une classe dérivée

· Aucun affaiblissement des post-conditions dans une classe dérivée

 

Revenons en à nos canards et adaptons notre code pour integrer certaines conditions:

   1: public class Duck
   2:     {
   3:         public DuckState State {get; private set;}
   4:  
   5:  
   6:         public virtual void Fly()
   7:         {
   8:             State = DuckState.Fly;
   9:  
  10:             Contract.Ensures(this.State == DuckState.Fly);
  11:         }
  12:     }
  13:  
  14:  
  15:     public enum DuckState
  16:     { 
  17:         Rest,
  18:         Fly
  19:     }

J’ai ajouté la ligne :

            Contract.Ensures(this.State == DuckState.Fly);

Qui signifie que fonctionnellement, a la fin de la méthode Fly, le canard doit être en train de voler!

Du coup, utiliser la classe ElectricDuck en l’état aboutirai a une exception si on refait le test ci-dessus (execution de la méthode methode1). Une exception qui t’indique que tu casse le principe de Liskov quelque partSourire.

Du coup, dans la classe ElectricDuck, je dois faire la modification suivante si je souhaite respecter ce principe:

   1: public class ElectricDuck: Duck
   2:     {
   3:  
   4:         public bool SwitchOn { get; set; }
   5:         public override void Fly()
   6:         {
   7:             if (!SwitchOn)
   8:                 SwitchOn = true;
   9:  
  10:             base.Fly();
  11:         }
  12:     }

Le canard c’est s’allumer tout seul…pas terrible hein! Il est donc préférable de ne pas hériter de la classe Duck tout court.

Pour en revenir à la règle sur les pré-conditions, je vous invite a regarder l’exemple très connu du carré qui hérite du rectangle proposé par Robert C. Martin dans son ouvrage et de réfléchir à quel type de contrat je dois ajouter à la classe Rectangle pour respecter Liskov.

Pour conclure

Le principe de Liskov apparait donc comme un complément du principe Ouvert-Fermé, il ajoute un aspect fonctionnel qui permet de s’assurer du bon fonctionnement des traitements.

Mais celui-ci va encore plus loin! car il vous permet de déterminer quel doit être la stratégie de gestion des exceptions dans vos applications (surement le sujet d’un post plus tard)…et je parle pas de la co-variance/contra-variance (peut être au cours d’une futur soirée Geek).

Et surtout, on a la puissance de l’API Code Contracts proposé par MS qui est un pur bijoux et permet de répondre parfaitement au défi proposé par Barbara Liskov, l’utiliser c’est réellement l’adopter!

Vous trouverez les sources liées aux posts sur SOLID sur codeplex:http://solidincsharp.codeplex.com/

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :
Posted: vendredi 9 décembre 2011 23:43 par fathi

Commentaires

Pas de commentaires

Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- Simuler facilement l’envoi de mail par Blog de Jérémy Jeanson le il y a 21 heures et 8 minutes

- ProcDump 6.0 : support du filtrage sur messages d'exceptions .NET, des filtres multiples et du ciblage par nom de service par CoqBlog le 05-20-2013, 14:50

- Votez pour le TOP 10 des influenceurs SharePoint francophones ! par Le blog de Patrick [MVP SharePoint] le 05-20-2013, 12:59

- [Conf’SharePoint] Dernier rappel ! :-) par Le blog de Patrick [MVP SharePoint] le 05-20-2013, 09:09

- [ #SharePoint 2013 ] les modèles de sites standards… par Le blog de Patrick [MVP SharePoint] le 05-20-2013, 09:03

- 10 erreurs de compréhension concernant SharePoint… par Le blog de Patrick [MVP SharePoint] le 05-20-2013, 08:27

- Conf’SharePoint : 10 bonnes raisons pour ne pas la rater par Le petit blog de Pierre / Pierre's little blog le 05-14-2013, 02:24

- [Event] Soirée de lancement Agile .NET France à Lyon par Blog Agile/ALM de Vincent THAVONEKHAM le 05-13-2013, 01:29

- .NET / Debug : inspection de la mémoire d'applications .NET (dump ou processus live) : première livraison d'une librairie .NET par Microsoft par CoqBlog le 05-11-2013, 22:21

- SharePoint : Incompatibilité avec Internet Explorer 10 (IE10) par Blog Technique de Romelard Fabrice le 05-08-2013, 16:29