Est-ce une bonne pratique d'implémenter deux méthodes par défaut Java 8 l'une par rapport à l'autre?

14

Je conçois une interface avec deux méthodes connexes, similaires à ceci:

public interface ThingComputer {
    default Thing computeFirstThing() {
        return computeAllThings().get(0);
    }

    default List<Thing> computeAllThings() {
        return ImmutableList.of(computeFirstThing());
    }
}

Environ la moitié des implémentations ne calculeront jamais qu'une chose, tandis que l'autre moitié peut en calculer plus.

Cela a-t-il un précédent dans le code Java 8 largement utilisé? Je sais que Haskell fait des choses similaires dans certaines classes ( Eqpar exemple).

L'avantage est que je dois écrire beaucoup moins de code que si j'avais deux classes abstraites ( SingleThingComputeret MultipleThingComputer).

L'inconvénient est qu'une implémentation vide se compile mais explose au moment de l'exécution avec a StackOverflowError. Il est possible de détecter la récursivité mutuelle avec a ThreadLocalet de donner une erreur plus agréable, mais cela ajoute une surcharge au code non bogué.

Tavian Barnes
la source
3
Pourquoi voudriez-vous jamais délibérément écrire du code garanti d'avoir une récursion infinie? Rien de bon ne peut en résulter.
1
Eh bien, ce n'est pas "garanti"; une mise en œuvre appropriée remplacerait l'une de ces méthodes.
Tavian Barnes
6
Tu ne le sais pas. Une classe ou une interface doit être autonome et ne pas supposer qu'une sous-classe fera les choses d'une certaine manière pour éviter des erreurs logiques majeures. Sinon, il est fragile et ne peut pas être à la hauteur de ses garanties. Notez que je ne dis pas que votre interface peut ou devrait imposer une classe d'implémentation faisant throw new Error();ou quelque chose de stupide, seulement que l' interface elle-même ne devrait pas avoir un contrat fragile à travers les defaultméthodes.
Je suis d'accord avec @Snowman, c'est une mine terrestre. Je pense que c'est un "code maléfique" dans le sens où Jon Skeet signifie - notez que le mal n'est pas la même chose que le mal , c'est méchant / intelligent / tentant, mais toujours faux :) Je recommande youtu.be/lGbQiguuUGc?t=15m26s ( ou en effet, tout le discours pour un contexte plus large - c'est très intéressant).
Konrad Morawski du
1
Ce n'est pas parce que chaque fonction peut être définie en fonction de l'autre qu'elles peuvent toutes deux être définies en fonction l'une de l'autre. Cela ne vole dans aucune langue. Vous voulez une interface avec 2 héritiers abstraits, chacun implémente l'un en fonction de l'autre mais résume l'autre afin que les implémenteurs choisissent le côté qu'ils souhaitent implémenter en héritant de la classe abstraite correcte. Un côté doit être défini indépendamment de l'autre, sinon la pop va dans la pile . Vous avez des valeurs par défaut insensées en supposant que les implémenteurs résoudront le problème pour vous, abstractexistent pour les forcer à le résoudre.
Jimmy Hoffa

Réponses:

16

TL; DR: ne faites pas cela.

Ce que vous montrez ici, c'est du code fragile.

Une interface est un contrat. Il dit "quel que soit l'objet que vous obtenez, il peut faire X et Y". Au moment où elle est écrite, votre interface ne fait ni X ni Y car elle est garantie de provoquer un débordement de pile.

Lancer une erreur ou une sous-classe indique une erreur grave qui ne devrait pas être interceptée:

Une erreur est une sous-classe de Throwable qui indique de graves problèmes qu'une application raisonnable ne devrait pas essayer d'attraper.

De plus, VirtualMachineError , la classe parente de StackOverflowError , dit ceci:

Lancé pour indiquer que la machine virtuelle Java est en panne ou a épuisé les ressources nécessaires pour continuer à fonctionner.

Votre programme ne doit pas se préoccuper des ressources JVM . C'est le travail de la JVM. Faire un programme qui provoque une erreur JVM dans le cadre d'un fonctionnement normal est mauvais. Il garantit soit que votre programme plantera, soit oblige les utilisateurs de cette interface à intercepter les erreurs qui ne devraient pas le concerner.


Vous connaissez peut-être Eric Lippert grâce à des efforts comme émérite «membre du comité de conception du langage C #». Il parle de fonctionnalités linguistiques poussant les gens vers le succès ou l'échec: bien qu'il ne s'agisse pas d'une fonctionnalité linguistique ou d'une partie de la conception du langage, son argument est tout aussi valable lorsqu'il s'agit de mettre en œuvre des interfaces ou d'utiliser des objets.

Vous vous souvenez de The Princess Bride lorsque Westley se réveille et se retrouve enfermé dans The Pit Of Despair avec un albinos rauque et le sinistre homme à six doigts, le comte Rugen? L'idée principale d'une fosse de désespoir est double. D'abord, c'est une fosse, donc facile à rentrer mais difficile à sortir. Et deuxièmement, cela induit le désespoir. D'où le nom.

Source: C ++ et la fosse du désespoir

Avoir une interface jette un StackOverflowErrorpar défaut pousse les développeurs dans la fosse du désespoir et c'est une mauvaise idée . Poussez plutôt les développeurs vers la fosse du succès . Faites - facile à utiliser l' interface correctement et sans écraser la machine virtuelle Java.

Déléguer entre les méthodes est bien ici. Cependant, la dépendance doit aller dans un sens. J'aime penser la délégation de méthode comme un graphique dirigé . Chaque méthode est un nœud sur le graphique. Chaque fois qu'une méthode appelle une autre méthode, tracez un bord entre la méthode appelante et la méthode appelée.

Si vous dessinez un graphique et remarquez qu'il est cyclique, c'est une odeur de code. C'est un potentiel pour pousser les développeurs dans la fosse du désespoir. Notez qu'il ne doit pas être catégoriquement interdit, seulement que l' on doit faire preuve de prudence . Les algorithmes récursifs auront spécifiquement des cycles dans le graphe d'appel: c'est très bien. Documentez-le et avertissez les développeurs. S'il n'est pas récursif, essayez de briser ce cycle. Si vous ne le pouvez pas, découvrez quelles entrées peuvent provoquer un débordement de pile et atténuez-les ou documentez-les comme dernier cas si rien d'autre ne fonctionne.

Communauté
la source
Très bonne réponse. Je suis curieux de savoir pourquoi c'est une pratique si courante à Haskell à l'époque. Différentes cultures de développement, je suppose.
Tavian Barnes
Je n'ai pas d'expérience à Haskell et je ne peux pas vous dire si ou pourquoi cela est plus courant dans la programmation fonctionnelle.
En y regardant un peu plus, il semble que la communauté Haskell n'en soit pas vraiment satisfaite non plus. Le compilateur a également récemment appris à vérifier les «définitions complètes minimales», de sorte qu'une implémentation vide générerait au moins un avertissement.
Tavian Barnes
1
La plupart des langages de programmation fonctionnels ont une optimisation des appels de queue. Avec cette optimisation, la récursivité de la queue ne ferait pas exploser la pile, de telles pratiques ont donc un sens.
Jason Yeo