web-dev-qa-db-fra.com

Principe de substitution de Liskov - Renforcement des conditions préalables

Je suis un peu confus quant à ce que cela signifie vraiment. Dans les questions connexes (--- est-ce une violation du principe de substitution de Liskov? ), on a dit que l'exemple viole clairement le LSP.

Mais je me demande, s'il n'y a pas de nouvelle exception projetée, serait-ce toujours une violation? N'est-ce pas simplement polymorphisme alors? C'est à dire:

public class Task
{
     public Status Status { get; set; }

     public virtual void Close()
     {
         Status = Status.Closed;
     }
}

public class ProjectTask : Task
{
     public override void Close()
     {
          if (Status == Status.Started) 
          {
              base.Close(); 
          }
     }
}
7
John V

ça dépend.

Pour valider le LSP, vous devez connaître le contrat précis de la fonction Close. Si le code ressemble à ceci

public class Task
{
     // after a call to this method, the status must become "Closed"
     public virtual void Close()
     //...
}

ensuite, une classe dérivée qui ignore ce commentaire viole le LSP. Si, cependant, le code ressemble à ceci

public class Task
{
     // tries to close the the task 
     // (check Status afterwards to find out if it has worked)
     public virtual void Close()
     //...
}

alors ProjectTask ne violait pas le LSP.

Cependant, au cas où il n'y a pas de commentaire, un nom de fonction comme Close donne à IMHO un appelant une attente assez claire de définir le statut sur "fermé" et si la fonction ne fonctionne pas de cette façon, elle ne fonctionnerait pas. être au moins une violation du "principe du moins d'étonnement".

Notez également que certaines langues de programmation comme Eiffel ont des prises en charge dans une langue intégrée pour les contrats, il n'est donc pas nécessairement nécessaire de fier des commentaires. Voir Cet article Wikipedia pour une liste.

6
Doc Brown

Vous ne pouvez pas décider si un code viole le LSP par le code lui-même. Vous devez connaître le contrat ​​que chaque méthode est à remplir.

Dans l'exemple, il y a pas de contrat explicite donné, nous devons donc deviner le contrat prévu de la méthode Close() pourrait être.

En regardant la méthode de la classe de base de la méthode Fermer (), le seul effet de cette méthode est que, par la suite, le Status est Status.Closed. Ma meilleure hypothèse d'un contrat pour cette méthode se lit comme suit:

Faites tout ce qui est nécessaire pour rendre le Status devient Status.Closed.

Mais c'est juste une devinière plausible. Personne ne peut être sûr de cela s'il n'est pas explicitement écrit.

Prenons ma supposition pour acquis.

Est-ce que la méthode ultérieure Close() est également remplit ce contrat? Il y a deux possibilités après avoir exécuté cette méthode que nous avons Status.Closed:

  • Nous avons déjà eu Status.Closed avant d'appeler la méthode.
  • Nous avons eu Status.Started. Ensuite, nous appelons la mise en œuvre de la base, réglant le champ sur Status.Closed.
  • Dans tous les autres cas, nous nous retrouvons avec un statut différent.

Si Status a uniquement les deux valeurs possibles Closed et Started (par exemple, une valeur de 2 valeur), tout va bien, il n'y a pas de violation du LSP, car nous obtenons toujours Status.Closed après la méthode Close().

Mais probablement, il y a probablement plus de possibles valeurs Status, finissant par un Status non en train d'être Status.Closed, donc violation du contrat.


L'OP a posé des questions sur la célèbre phrase "où que j'utilise la classe de base, sa classe dérivée peut être utilisée".

Je voudrais donc élaborer à ce sujet.

Je l'ai lu comme "partout où j'utilise la classe de base dans son contrat, sa classe dérivée peut être utilisée, sans violer ce contrat.

Donc, il ne s'agit pas seulement de ne pas produire d'erreurs de compilation ni de courir sans lancer des erreurs, il s'agit faire ce que le contrat exige.

Et cela ne s'applique qu'aux situations où je demande à la classe de faire quelque chose qui se trouve dans sa gamme d'opérations. Nous n'avons donc pas besoin de se soucier des situations d'abus (par exemple, où les conditions préalables ne sont pas remplies).


Après avoir réaffirmé votre question, je pense que je devrais ajouter un paragraphe sur le polymorphisme dans ce contexte.

Le polymorphisme signifie que, pour des instances de classes différentes, la même méthode appelle les résultats dans différentes implémentations étant exécutées. Donc, le polymorphisme vous permet techniquement de remplacer notre méthode Close() méthode avec celle que par ex. ouvre un flux. Techniquement, c'est possible, mais c'est une mauvaise utilisation du polymorphisme. Et un principe sur les bonnes et les mauvaises utilisations du polymorphisme est le LSP.

21
Ralf Kleberhoff

Le principe de substitution de Liskov est tout à propos de contrats . Il consiste en des conditions préalables (conditions qui doivent être vraies afin que le comportement correspondant puisse fonctionner), des postconditions (conditions qui doivent être respectées de manière à ce que le comportement puisse être considéré comme terminé son travail), des invariants (conditions qui doivent être fidèles avant, pendant et après le Exécution de la méthode correspondante) et contrainte d'historique (à mon avis, c'est un sous-ensemble d'invariant, vous feriez donc mieux de vérifier Wikipedia). Dans une question que vous avez liée à un implicite contrat de la classe Task ressemble à ce qui suit:

  • Précondition: il n'y en a pas
  • Postcontion: Status est closed
  • Invariant: ne peut voir aucun

Donc, si l'une des classes d'enfant ne fermez pas la tâche, elle est considérée comme une violation du LSP dans un certain contrat .

Mais si vous postulez explicitement votre contrat pour être comme "Fermer la tâche uniquement si c'est started", alors vous allez bien. Vous pouvez le faire dans votre code - un exemple de ceci est-ce réponse acceptée . Mais très souvent, vous ne pouvez pas - vous pouvez donc utiliser des commentaires clairs.

Fondamentalement, chaque fois que vous pensez à la violation du LSP, vous devez déjà connaître le contrat. Il n'y a pas de "violation LSP", seule "violation de la LSP dans un contrat".

7
Zapadlo

oui, toujours une violation (probablement)

Certains clients de Task s'appuie sur "après Task::Close(), Status est maintenant Closed", puis rompre lorsqu'il rencontre un ProjectTask . Vous pourriez actuellement ne pas avoir de tels clients, mais la postcondition de la fonction de Task::Close() devrait être "Status est dans un état valide mais non spécifié ", qui est fondamentalement inutile.

La chose beaucoup plus naturelle est pour Task::Close() d'avoir la postcondition "Status est Closed", qui exclut la mise en œuvre en ProjectTask d'être valide.

C'est un problème majeur avec void DoStuff() Méthodes: Tout ce que vous avez mes effets secondaires, vous avez donc des choses sur ces effets secondaires. bool TryClose() a la signification "Close() si vous le pouvez et en parle de cela"

1
Caleth

Comme Ralf et d'autres personnes ont mentionné, vous n'avez pas en réalité mis en œuvre ni appliqué de contrats sur votre code, autrement que par le fichier supposumé "le sens commun" Convention que Close() Devrait quitter l'objet dans un état fermé et autre que les commentaires ajoutés à la sous-classe.

À mon avis, l'exemple que vous avez fourni (je sais que c'est copié à partir de n poste associé ) a une faille de conception pour déclarer la méthode Close() tel virtuel sur la base Task classe - Il s'agit simplement d'inviter d'autres personnes à sous-classes Task et de modifier le comportement, même si vous avez fourni une implémentation par défaut qui observe le contrat.

Et pire, puisque Status n'est pas encapsulé du tout, l'état est mutable publiquement, de sorte que tout contrat autour de Close est assez significatif, car l'état peut être attribué de manière aléatoire à l'extérieur de l'extérieur.

Donc, si votre hiérarchie de classe ne nécessite pas de comportement polymorphe de Close, je supprimerais simplement le mot-clé virtual sur Task.Close:

// Encapsulate status, to control state transition
public Status Status { get; private set; }

public void Close()
{
    Status = Status.Closed;
}

(et faites la même chose pour toute autre transition de l'état)

Si toutefois vous avez besoin de comportement polymorphe (c.-à-d. Si les sous-classes doivent fournir des implémentations personnalisées de Close), je convertitais votre base Task classe à une interface, puis appliquez les conditions préalables et post à travers contrat de code , comme suit:

[ContractClass(typeof(TaskContracts))]
public interface ITask
{
    Status Status { get; } // No externally accessible set

    void Close();
    // Other transition methods here.
}

Avec les contrats correspondants:

[ContractClassFor(typeof(ITask))]
public class TaskContracts : ITask
{
    public Status Status { get; }

    public void Close()
    {
        Contract.Requires(Status != Status.Closed, "Already Closed!");
        Contract.Ensures(Status == Status.Closed, "Must close Task on Completion!");
    }
}

L'avantage de cette approche est que le contrat d'utilisation de l'interface est clair (et exécutoire!), Et contrairement à la fonction virtual Close() qui pourrait être contournée, les sous-classes peuvent fournir toute mise en œuvre qu'elles aiment, à condition que le contrat soit rempli.

1
StuartLC

Oui, c'est toujours une violation du LSP.

Dans la base Tâche Classe, après Fermer () a été appelé le statut est fermé.
[.____] dans le fichier dérivé -ASTASK Classe, après invoquant Fermer () Le statut peut ou non être fermé.

Ainsi, la postcondition (statut est fermée) n'est plus remplie dans le Masque de projet classe.
[.____] ou en d'autres termes: un client sachant que la tâche ne peut s'appuyer sur le fait que le statut est fermé après invocation Fermer (). Si vous lui donnez un projet "déguisé" comme tâche (que vous êtes autorisé à faire), et il invoque Fermer () Le résultat est différent (le statut pourrait ne pas être fermé).

0
CharonX