web-dev-qa-db-fra.com

Quelqu'un peut-il fournir un exemple du principe de substitution de Liskov (LSP) utilisant des véhicules?

Le principe de substitution de Liskov stipule qu'un sous-type doit être substituable à ce type (sans altérer l'exactitude du programme).

  • Quelqu'un peut-il donner un exemple de ce principe dans le domaine des véhicules (automobiles)?
  • Quelqu'un peut-il donner un exemple de violation de ce principe dans le domaine des véhicules?

J'ai lu l'exemple du carré/rectangle, mais je pense qu'un exemple avec des véhicules me donnera une meilleure compréhension du concept.

37
random512

Pour moi, ce Citation de 1996 de Oncle Bob ( Robert C Martin ) résume le mieux le LSP:

Les fonctions qui utilisent des pointeurs ou des références à des classes de base doivent pouvoir utiliser des objets de classes dérivées sans le savoir.

Ces derniers temps, comme alternative aux abstractions d'héritage basées sur la sous-classification d'une base/super classe (généralement abstraite), nous utilisons également souvent des interfaces pour l'abstraction polymorphe. Le LSP a des implications à la fois pour le consommateur et la mise en œuvre de l'abstraction:

  • Tout code consommant une abstraction de classe ou d'interface ne doit supposer rien d'autre sur la classe au-delà de l'abstraction définie;
  • Toute sous-classe d'une superclasse ou implémentation d'une abstraction doit respecter les exigences et les conventions de l'interface avec l'abstraction.

Conformité LSP

Voici un exemple utilisant une interface IVehicle qui peut avoir plusieurs implémentations (alternativement, vous pouvez remplacer l'interface par une classe de base abstraite avec plusieurs sous-classes - même effet).

interface IVehicle
{
   void Drive(int miles);
   void FillUpWithFuel();
   int FuelRemaining {get; } // C# syntax for a readable property
}

Cette implémentation d'un consommateur de IVehicle reste dans les limites de LSP:

void MethodWhichUsesIVehicle(IVehicle aVehicle)
{
   ...
   // Knows only about the interface. Any IVehicle is supported
   aVehicle.Drive(50);
 }

Violation flagrante - Changement de type d'exécution

Voici un exemple de violation de LSP, en utilisant RTTI puis Downcasting - Oncle Bob appelle cela une "violation flagrante":

void MethodWhichViolatesLSP(IVehicle aVehicle)
{
   if (aVehicle is Car)
   {
      var car = aVehicle as Car;
      // Do something special for car - this method is not on the IVehicle interface
      car.ChangeGear();
    }
    // etc.
 }

La méthode de violation va au-delà de l'interface contractée IVehicle et pirate un chemin spécifique pour une implémentation connue de l'interface (ou une sous-classe, si vous utilisez l'héritage au lieu des interfaces). L'oncle Bob explique également que les violations de LSP utilisant un comportement de changement de type violent généralement aussi le principe ouvert et fermé , car une modification continue de la fonction sera nécessaire afin d'accueillir de nouvelles sous-classes.

Violation - La condition préalable est renforcée par un sous-type

Un autre exemple de violation serait où une "la condition préalable est renforcée par un sous-type" :

public abstract class Vehicle
{
    public virtual void Drive(int miles)
    {
        Assert(miles > 0 && miles < 300); // Consumers see this as the contract
    }
 }

 public class Scooter : Vehicle
 {
     public override void Drive(int miles)
     {
         Assert(miles > 0 && miles < 50); // ** Violation
         base.Drive(miles);
     }
 }

Ici, la sous-classe Scooter tente de violer le LSP en essayant de renforcer (contraindre davantage) la condition préalable de la méthode de classe de base Drive qui miles < 300, Jusqu'à maintenant un maximum de moins de 50 miles. Ceci n'est pas valide, car la définition du contrat de Vehicle autorise 300 miles.

De même, les conditions de publication ne peuvent pas être affaiblies (c'est-à-dire détendues) par un sous-type.

(Les utilisateurs de Contrats de code en C # noteront que les conditions préalables et postconditions DOIVENT être placées sur l'interface via un ContractClassFor classe, et ne peut pas être placé dans les classes d'implémentation, évitant ainsi la violation)

Violation subtile - Abus d'une implémentation d'interface par une sous-classe

Une violation de more subtle (Également la terminologie de l'oncle Bob) peut être montrée avec une classe dérivée douteuse qui implémente l'interface:

class ToyCar : IVehicle
{
    public void Drive(int miles) { /* Show flashy lights, make random sounds */ }
    public void FillUpWithFuel() {/* Again, more silly lights and noises*/}
    public int FuelRemaining {get {return 0;}}
}

Ici, quelle que soit la distance parcourue par le ToyCar, le carburant restant sera toujours nul, ce qui surprendra les utilisateurs de l'interface IVehicle (c'est-à-dire la consommation infinie de MPG - mouvement perpétuel?). Dans ce cas, le problème est que malgré le fait que ToyCar ait implémenté toutes les exigences de l'interface, ToyCar n'est pas intrinsèquement un vrai IVehicle et juste des "tampons en caoutchouc" L'interface.

Une façon d'empêcher vos interfaces ou classes de base abstraites d'être abusées de cette manière est de vous assurer qu'un bon ensemble de tests unitaires est disponible sur la classe de base interface/abstraite pour tester que toutes les implémentations répondent aux attentes (et à toutes les hypothèses). Les tests unitaires sont également excellents pour documenter l'utilisation typique. par exemple. ce NUnit Theory empêchera ToyCar d'en faire votre base de code de production:

[Theory]
void EnsureThatIVehicleConsumesFuelWhenDriven(IVehicle vehicle)
{
    vehicle.FillUpWithFuel();
    Assert.IsTrue(vehicle.FuelRemaining > 0);
    int fuelBeforeDrive = vehicle.FuelRemaining;
    vehicle.Drive(20); // Fuel consumption is expected.
    Assert.IsTrue(vehicle.FuelRemaining < fuelBeforeDrive);
}

Edit, Re: OpenDoor

L'ouverture des portes sonne comme une préoccupation complètement différente, il faut donc les séparer en conséquence (c'est-à-dire "S" et "I" en SOLID), par ex.

  • sur une nouvelle interface IVehicleWithDoors, qui pourrait hériter de IVehicle
  • ou IMO encore mieux, sur une interface distincte IDoor, puis des véhicules comme Car et Truck implémenteraient les deux interfaces IVehicle et IDoor , mais Scooter et Motorcycle ne le feraient pas.
  • ou même 3 interfaces, IVehicle (Drive()), IDoor (Open()) et IVehicleWithDoors qui hérite des deux.

Dans tous les cas, pour éviter de violer LSP, le code qui nécessitait des objets de ces interfaces ne devrait pas abaisser l'interface pour accéder à des fonctionnalités supplémentaires. Le code doit sélectionner l'interface/la (super) classe minimale appropriée dont il a besoin, et s'en tenir uniquement aux fonctionnalités contractées sur cette interface.

58
StuartLC

Image Je veux louer une voiture lorsque je déménage. Je téléphone à la société de location et je leur demande quels modèles ils ont. Ils me disent cependant que je vais juste recevoir la prochaine voiture disponible:

public class CarHireService {
    public Car hireCar() {
        return availableCarPool.getNextCar();
    }
}

Mais ils m'ont donné une brochure qui me dit que tous leurs modèles sont livrés avec ces fonctionnalités:

public interface Car {
    public void drive();
    public void playRadio();
    public void addLuggage();
}

Cela correspond exactement à ce que je recherche, alors je réserve une voiture et je m'en vais heureux. Le jour du déménagement, une voiture de Formule 1 apparaît devant ma maison:

public class FormulaOneCar implements Car {
    public void drive() {
        //Code to make it go super fast
    }

    public void addLuggage() {
        throw new NotSupportedException("No room to carry luggage, sorry."); 
    }

    public void playRadio() {
        throw new NotSupportedException("Too heavy, none included."); 
    }
}

Je ne suis pas content, car leur brochure m'a essentiellement menti - peu importe si la voiture de Formule 1 a un faux coffre qui ressemble à il peut contenir des bagages mais ne s'ouvre pas, inutile pour déménager!

Si on me dit que "ce sont les choses que font toutes nos voitures", alors n'importe quelle voiture qu'on me donne devrait se comporter de cette façon. Si je ne peux pas faire confiance aux détails de leur brochure, c'est inutile. C'est l'essence du Liskov Substitution Principle.

25
anotherdave

Le principe de substitution de Liskov stipule qu'un objet avec une certaine interface peut être remplacé par un objet différent qui implémente cette même interface tout en conservant l'exactitude du programme d'origine. Cela signifie que non seulement l'interface doit avoir exactement les mêmes types, mais que le comportement doit également rester correct.

Dans un véhicule, vous devriez pouvoir remplacer une pièce par une autre et la voiture continuera de fonctionner. Disons que votre ancienne radio n'a pas de tuner numérique, mais que vous souhaitez écouter la radio HD, vous achetez donc une nouvelle radio dotée d'un récepteur HD. Vous devriez pouvoir retirer l'ancienne radio et brancher la nouvelle radio, tant qu'elle a la même interface. En surface, cela signifie que la prise électrique qui relie la radio à la voiture doit avoir la même forme sur la nouvelle radio que sur l'ancienne radio. Si la fiche de la voiture est rectangulaire et possède 15 broches, alors la prise de la nouvelle radio doit être rectangulaire et avoir également 15 broches.

Mais il y a d'autres considérations en plus de l'ajustement mécanique: le comportement électrique sur la prise doit également être le même. Si la broche 1 du connecteur de l'ancienne radio est de + 12V, la broche 1 du connecteur de la nouvelle radio doit également être de + 12V. Si la broche 1 de la nouvelle radio était la broche "sortie haut-parleur gauche", la radio pourrait court-circuiter ou faire sauter un fusible. Ce serait une violation claire du LSP.

Vous pouvez également envisager une situation de déclassement: disons que votre radio coûteuse meurt et que vous ne pouvez vous permettre qu'une radio AM. Il n'a pas de sortie stéréo, mais il a le même connecteur que votre radio existante. Disons que la spécification a la broche 3 étant sortie haut-parleur gauche et la broche 4 étant sortie haut-parleur droit. Si votre radio AM diffuse le signal monophonique à la fois sur les broches 3 et 4, vous pourriez dire que son comportement est cohérent, et ce serait une substitution acceptable. Mais si votre nouvelle radio AM lit uniquement l'audio sur la broche 3, et rien sur la broche 4, le son serait déséquilibré, et ce ne serait probablement pas une substitution acceptable. Cette situation violerait également le LSP, car bien que vous puissiez entendre des sons et qu'aucun fusible ne saute, la radio ne répond pas aux spécifications complètes de l'interface.

3
John Deters

Tout d'abord, vous devez définir ce qu'est un véhicule et une automobile. Selon Google (définitions peu complètes):

Véhicule:
une chose utilisée pour le transport de personnes ou de marchandises, esp. sur terre, comme une voiture, un camion ou une charrette.

Voiture:
un véhicule routier, généralement à quatre roues, propulsé par un moteur à combustion interne ou électrique
moteur et capable de transporter un petit nombre de personnes

Donc, une automobile est un véhicule, mais un véhicule n'est pas une automobile.

2
user2810910