Le principe de substitution de Liskov stipule qu'un sous-type doit être substituable à ce type (sans altérer l'exactitude du programme).
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.
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:
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.
IVehicleWithDoors
, qui pourrait hériter de IVehicle
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.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.
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.
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.
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.