15 votes

Comment éviter de violer le principe de substitution de Liskov (LSP) ?

Je suis dans une situation très similaire à ce que Steve McConnell a mentionné dans Code Complete. Seulement mon problème est basé sur des véhicules et le Trike se retrouve par la loi dans la catégorie des voitures. Les voitures avaient quatre roues jusqu'à présent. En tout cas, mon domaine est inutilement complexe donc il est facile de rester avec l'exemple des chats ci-dessous.

Soyez méfiant envers les classes qui remplacent une routine et ne font rien à l'intérieur de la routine dérivée. Cela indique généralement une erreur dans la conception de la classe de base. Par exemple, supposez que vous ayez une classe Chat et une routine Griffer() et supposons que vous découvriez finalement que certaines chats sont dégriffés et ne peuvent pas griffer. Vous pourriez être tenté de créer une classe dérivée de Chat appelée ChatSansGriffes et de remplacer la routine Griffer() pour ne rien faire. Cette approche présente plusieurs problèmes :

Cela viole l'abstraction (contrat d'interface) présentée dans la classe Chat en changeant la sémantique de son interface.

Cette approche devient rapidement incontrôlable lorsque vous l'étendez à d'autres classes dérivées. Que se passe-t-il lorsque vous trouvez un chat sans queue ? Ou un chat qui n'attrape pas de souris ? Ou un chat qui ne boit pas de lait ? Vous finirez par avoir des classes dérivées comme ChatSansQueueSansSourisSansLait.

Au fil du temps, cette approche donne lieu à un code confus à maintenir car les interfaces et le comportement des classes ancestrales n'impliquent que peu ou rien sur le comportement de leurs descendants.

L'endroit pour corriger ce problème n'est pas dans la classe de base, mais dans la classe Cat d'origine. Créez une classe Griffes et incluez-la dans la classe Chats. Le problème principal était l'hypothèse que tous les chats griffent, donc corrigez ce problème à la source, plutôt que de simplement le panser à la destination.

Conformément au texte de son excellent livre ci-dessus. Voici ce qui est mauvais

La classe parent n'a pas besoin d'être abstraite

public abstract class Cat {
   public void scratch() {
      System.out.println("Je peux griffer");
   }
}

Classe dérivée

public class ChatSansGriffes extends Cat {
   @Override
   public void scratch() {
      // ne rien faire
   }
}

Maintenant, il suggère de créer une autre classe Griffes, mais je ne comprends pas comment je peux utiliser cette classe pour éviter le besoin de ChatSansGriffes#Griffer.

11voto

David Harkness Points 16674

Le fait que tous les chats n'aient pas de griffes et ne soient pas capables de griffer est un gros indice que Cat ne devrait pas définir une méthode scratch publique dans son API. La première étape est de réfléchir à pourquoi vous avez défini scratch en premier lieu. Peut-être que les chats griffent s'ils le peuvent lorsqu'ils sont attaqués ; sinon, ils feulent ou s'enfuient.

public class Cat extends Animal {
    private Claws claws;

    public void onAttacked(Animal attacker) {
        if (claws != null) {
            claws.scratch(attacker);
        }
        else {
            // Suppose que tous les chats peuvent feuler.
            // Cela peut aussi être faux et vous avez besoin de Voicebox.
            // Rincer et répéter.
            hiss();
        }
    }
}

Maintenant vous pouvez substituer n'importe quelle sous-classe de Cat par une autre et elle se comportera correctement selon qu'elle ait des griffes ou non. Vous pourriez définir une classe DefenseMechanism pour organiser les différentes défenses telles que Scratch, Hiss, Bite, etc.

10voto

Attila Points 18290

Vous auriez toujours une méthode scratch(), mais elle ne sera pas remplacée par les classes dérivées :

public class Cat {
  Claw claw_;
  public Cat(Claw claw) {claw = claw_;}
  public final void scratch() {
    if (claw_ != null) {
      claw_.scratch(this);
    }
  }
}

Cela vous permet de déléguer la logique de grattage à l'objet Claw contenu, s'il est présent (et de ne pas gratter s'il n'y a pas de griffes). Les classes dérivées de Cat n'ont pas leur mot à dire sur la manière de gratter, donc pas besoin de créer des hiérarchies d'ombres basées sur les capacités.

De plus, étant donné que les classes dérivées ne peuvent pas modifier l'implémentation de la méthode, il n'y a pas de problème les empêchant de respecter la sémantique prévue de la méthode scratch() dans l'interface de la classe de base.

Si vous poussez cela à l'extrême, vous pourriez constater que vous avez beaucoup de classes et peu de dérivés -- la plupart de la logique est déléguée aux objets de composition, et non confiée aux classes dérivées.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X