Les cas spéciaux avec repli violent-ils le principe de substitution de Liskov?

20

Disons que j'ai une interface FooInterfacequi a la signature suivante:

interface FooInterface {
    public function doSomething(SomethingInterface something);
}

Et une classe concrète ConcreteFooqui implémente cette interface:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
    }

}

J'aimerais ConcreteFoo::doSomething()faire quelque chose d'unique s'il passe un type d' SomethingInterfaceobjet spécial (disons qu'il s'appelle SpecialSomething).

C'est définitivement une violation LSP si je renforce les conditions préalables de la méthode ou si je lève une nouvelle exception, mais serait-ce toujours une violation LSP si je casse des SpecialSomethingobjets spéciaux tout en fournissant une solution de secours pour les SomethingInterfaceobjets génériques ? Quelque chose comme:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
        if (something instanceof SpecialSomething) {
            // Do SpecialSomething magic
        }
        else {
            // Do generic SomethingInterface magic
        }
    }

}
Evan
la source

Réponses:

19

Il peut s'agir d'une violation de LSP selon ce qui est d'autre dans le contrat pour la doSomethingméthode. Mais c'est presque certainement une odeur de code même si elle ne viole pas le LSP.

Par exemple, si une partie du contrat de doSomethingest qu'il appellera something.commitUpdates()au moins une fois avant de revenir, et pour le cas particulier , il appelle à la commitSpecialUpdates()place, alors c'est une violation du LSP. Même si SpecialSomethingla commitSpecialUpdates()méthode de a été consciemment conçue pour faire toutes les mêmes choses que commitUpdates(), c'est juste un piratage préventif autour de la violation LSP, et c'est exactement le genre de piratage que l'on n'aurait pas à faire si on suivait LSP de manière cohérente. Si quelque chose comme cela s'applique à votre cas, vous devrez le vérifier en vérifiant votre contrat pour cette méthode (explicite ou implicite).

La raison pour laquelle il s'agit d'une odeur de code est que la vérification du type concret de l'un de vos arguments manque en premier lieu la définition d'une interface / d'un type abstrait et parce qu'en principe, vous ne pouvez plus garantir que la méthode fonctionne même (imaginez si quelqu'un écrit une sous-classe SpecialSomethingavec l'hypothèse qui commitUpdates()sera appelée). Tout d'abord, essayez de faire fonctionner ces mises à jour spéciales dans leSomethingInterface; c'est le meilleur résultat possible. Si vous êtes vraiment sûr de ne pas pouvoir le faire, vous devez mettre à jour l'interface. Si vous ne contrôlez pas l'interface, vous devrez peut-être envisager d'écrire votre propre interface qui fait ce que vous voulez. Si vous ne pouvez même pas proposer une interface qui fonctionne pour chacun d'eux, vous devriez peut-être supprimer entièrement l'interface et avoir plusieurs méthodes prenant différents types de béton, ou peut-être qu'un refactoriseur encore plus grand est en règle. Il nous faudrait en savoir plus sur la magie que vous avez commentée pour savoir laquelle est appropriée.

Ixrec
la source
Merci! CA aide. En théorie, le but de la doSomething()méthode serait de convertir le type en SpecialSomething: si elle le recevait, SpecialSomethingelle retournerait simplement l'objet non modifié, tandis que si elle recevait un SomethingInterfaceobjet générique , elle exécuterait un algorithme pour le convertir en SpecialSomethingobjet. Étant donné que les conditions préalables et postconditions restent les mêmes, je ne pense pas que le contrat ait été violé.
Evan
1
@Evan Oh wow ... c'est un cas intéressant. Cela pourrait en fait ne poser aucun problème. La seule chose à laquelle je peux penser est que si vous retournez l'objet existant au lieu de construire un nouvel objet, peut - être que quelqu'un dépend de cette méthode pour renvoyer un nouvel objet ... mais si ce genre de chose peut briser les gens, cela dépend de la langue. Est-il possible pour quelqu'un d'appeler y = doSomething(x), x.setFoo(3)puis de trouver le y.getFoo()résultat 3?
Ixrec
Cela est problématique en fonction de la langue, mais il devrait être facilement contourné en renvoyant simplement une copie de l' SpecialSomethingobjet à la place. Bien que pour des raisons de pureté, je pouvais également voir renoncer à l'optimisation du cas spécial lorsque l'objet passé est SpecialSomethinget simplement l'exécuter à travers l'algorithme de conversion plus grand, car il devrait toujours fonctionner car il est également un SomethingInterfaceobjet.
Evan
1

Ce n'est pas une violation du LSP. Cependant, il s'agit toujours d'une violation de la règle "ne faites pas de vérification de type". Il serait préférable de concevoir le code de telle manière que le spécial se pose naturellement; peut-être a SomethingInterfacebesoin d'un autre membre qui pourrait accomplir cela, ou peut-être que vous devez injecter une usine abstraite quelque part.

Cependant, ce n'est pas une règle stricte et rapide, vous devez donc décider si le compromis en vaut la peine. À l'heure actuelle, vous avez une odeur de code et un obstacle possible aux améliorations futures. S'en débarrasser pourrait signifier une architecture beaucoup plus compliquée. Sans plus d'informations, je ne peux pas dire laquelle est la meilleure.

Sebastian Redl
la source
1

Non, profiter du fait qu'un argument donné ne fournit pas seulement l'interface A, mais aussi A2, ne viole pas le LSP.

Assurez-vous simplement que le chemin spécial n'a pas de conditions préalables plus fortes (à part celles testées pour décider de le prendre), ni de conditions postérieures plus faibles.

Les modèles C ++ le font souvent pour fournir de meilleures performances, par exemple en exigeant InputIterators, mais en donnant des garanties supplémentaires s'ils sont appelés avec RandomAccessIterators.

Si vous devez plutôt décider au moment de l'exécution (par exemple à l'aide de la diffusion dynamique), méfiez-vous de la décision à prendre pour consommer tous vos gains potentiels ou même plus.

Profiter du cas spécial va souvent à l'encontre de DRY (ne vous répétez pas), car vous pourriez avoir à dupliquer du code, et à KISS (Keep it Simple), car il est plus complexe.

Déduplicateur
la source
0

Il existe un compromis entre le principe de «ne pas faire de vérification de type» et de «séparer vos interfaces». Si de nombreuses classes fournissent un moyen réalisable mais éventuellement inefficace d'effectuer certaines tâches, et que certaines d'entre elles peuvent également offrir un meilleur moyen, et il existe un besoin de code qui peut accepter n'importe quelle catégorie plus large d'éléments pouvant effectuer la tâche (peut-être de manière inefficace) mais ensuite effectuer la tâche aussi efficacement que possible, il sera nécessaire que tous les objets implémentent une interface qui inclut un membre pour dire si la méthode la plus efficace est prise en charge et une autre pour l'utiliser si c'est le cas, ou bien demander au code qui reçoit l'objet de vérifier s'il prend en charge une interface étendue et le cas échéant.

Personnellement, je privilégie la première approche, bien que je souhaite que les frameworks orientés objet comme .NET permettent aux interfaces de spécifier des méthodes par défaut (rendant les interfaces plus grandes moins difficiles à utiliser). Si l'interface commune comprend des méthodes facultatives, une seule classe wrapper peut gérer des objets avec de nombreuses combinaisons de capacités différentes tout en ne promettant aux consommateurs que les capacités présentes dans l'objet wrappé d'origine. Si de nombreuses fonctions sont divisées en différentes interfaces, un objet wrapper différent sera nécessaire pour chaque combinaison d'interfaces différentes que les objets wrappés pourraient avoir besoin de prendre en charge.

supercat
la source
0

Le principe de substitution de Liskov concerne les sous-types agissant conformément à un contrat de leur supertype. Ainsi, comme l'a écrit Ixrec, il n'y a pas suffisamment d'informations pour savoir s'il s'agit d'une violation LSP.

Ce qui est violé ici cependant, c'est un principe ouvert et fermé. Si vous avez une nouvelle exigence - faire de la magie spéciale - et que vous devez modifier le code existant, vous violez définitivement OCP .

Zapadlo
la source