Le lancement de nouvelles RuntimeExceptions dans du code inaccessible est-il un mauvais style?

31

J'ai été chargé de maintenir une application écrite il y a quelque temps par des développeurs plus qualifiés. Je suis tombé sur ce morceau de code:

public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
        try {
            return translate(mailManagementService.retrieveUserMailConfiguration(id));
        } catch (Exception e) {
            rethrow(e);
        }
        throw new RuntimeException("cannot reach here");
    }

Je suis curieux de savoir si le lancer RuntimeException("cannot reach here")est justifié. Il me manque probablement quelque chose d'évident sachant que ce morceau de code vient d'un collègue plus aguerri.
EDIT: Voici le corps de retour auquel certaines réponses ont fait référence. J'ai jugé que ce n'était pas important dans cette question.

private void rethrow(Exception e) throws MailException {
        if (e instanceof InvalidDataException) {
            InvalidDataException ex = (InvalidDataException) e;
            rethrow(ex);
        }
        if (e instanceof EntityAlreadyExistsException) {
            EntityAlreadyExistsException ex = (EntityAlreadyExistsException) e;
            rethrow(ex);
        }
        if (e instanceof EntityNotFoundException) {
            EntityNotFoundException ex = (EntityNotFoundException) e;
            rethrow(ex);
        }
        if (e instanceof NoPermissionException) {
            NoPermissionException ex = (NoPermissionException) e;
            rethrow(ex);
        }
        if (e instanceof ServiceUnavailableException) {
            ServiceUnavailableException ex = (ServiceUnavailableException) e;
            rethrow(ex);
        }
        LOG.error("internal error, original exception", e);
        throw new MailUnexpectedException();
    }


private void rethrow(ServiceUnavailableException e) throws
            MailServiceUnavailableException {
        throw new MailServiceUnavailableException();
    }

private void rethrow(NoPermissionException e) throws PersonNotAuthorizedException {
    throw new PersonNotAuthorizedException();
}

private void rethrow(InvalidDataException e) throws
        MailInvalidIdException, MailLoginNotAvailableException,
        MailInvalidLoginException, MailInvalidPasswordException,
        MailInvalidEmailException {
    switch (e.getDetail()) {
        case ID_INVALID:
            throw new MailInvalidIdException();
        case LOGIN_INVALID:
            throw new MailInvalidLoginException();
        case LOGIN_NOT_ALLOWED:
            throw new MailLoginNotAvailableException();
        case PASSWORD_INVALID:
            throw new MailInvalidPasswordException();
        case EMAIL_INVALID:
            throw new MailInvalidEmailException();
    }
}

private void rethrow(EntityAlreadyExistsException e)
        throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
    switch (e.getDetail()) {
        case LOGIN_ALREADY_TAKEN:
            throw new MailLoginNotAvailableException();
        case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
            throw new MailEmailAddressAlreadyForwardedToException();
    }
}

private void rethrow(EntityNotFoundException e) throws
        MailAccountNotCreatedException,
        MailAliasNotCreatedException {
    switch (e.getDetail()) {
        case ACCOUNT_NOT_FOUND:
            throw new MailAccountNotCreatedException();
        case ALIAS_NOT_FOUND:
            throw new MailAliasNotCreatedException();
    }
}
Sok Pomaranczowy
la source
12
il est techniquement accessible, s'il rethrowne fait pas réellement throwexception. (ce qui peut arriver un jour si l'implémentation change)
njzk2
34
En passant, une AssertionError peut être un meilleur choix sémantique que RuntimeException.
JvR
1
Le dernier lancer est redondant. Si vous activez les findbugs ou d'autres outils d'analyse statique, il signalerait ces types de lignes à supprimer.
Bon Ami
7
Maintenant que je vois le reste du code, je pense que peut - être vous devriez changer « les développeurs plus qualifiés » dans « plus hauts développeurs ». La révision du code serait heureuse d'expliquer pourquoi.
JvR
3
J'ai essayé de créer des exemples hypothétiques des raisons pour lesquelles les exceptions sont terribles. Mais rien de ce que j'ai jamais fait ne tient le coup.
Paul Draper

Réponses:

36

Tout d'abord, merci d'avoir mis à jour votre question et de nous avoir montré ce qui se rethrowpasse. Donc, en fait, il convertit les exceptions avec des propriétés en classes d'exceptions plus fines. Plus d'informations à ce sujet plus tard.

Comme je n'ai pas vraiment répondu à la question principale à l'origine, c'est parti: oui, c'est généralement un mauvais style de lancer des exceptions d'exécution dans du code inaccessible; vous feriez mieux d'utiliser des assertions, ou mieux encore, d'éviter le problème. Comme déjà souligné, le compilateur ici ne peut pas être sûr que le code ne sort jamais du try/catchbloc. Vous pouvez refactoriser votre code en profitant du fait que ...

Les erreurs sont des valeurs

(Sans surprise, il est bien connu dans go )

Prenons un exemple plus simple, celui que j'ai utilisé avant votre édition: alors imaginez que vous enregistrez quelque chose et que vous créez une exception de wrapper comme dans la réponse de Konrad . Appelons ça logAndWrap.

Au lieu de lancer l'exception comme effet secondaire de logAndWrap, vous pouvez la laisser faire son travail comme effet secondaire et lui faire renvoyer une exception (au moins, celle donnée en entrée). Vous n'avez pas besoin d'utiliser des génériques, juste des fonctions de base:

private Exception logAndWrap(Exception exception) {
    // or whatever it actually does
    Log.e("Ouch! " + exception.getMessage());
    return new CustomWrapperException(exception);
}

Ensuite, vous throwexplicite, et votre compilateur est heureux:

try {
     return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
     throw logAndWrap(e);
}

Et si tu oublies throw?

Comme expliqué dans le commentaire de Joe23 , une manière de programmation défensive pour s'assurer que l'exception est toujours levée consisterait à faire explicitement un throw new CustomWrapperException(exception)à la fin de logAndWrap, comme cela est fait par Guava.Throwables . De cette façon, vous savez que l'exception sera levée et votre analyseur de type est content. Cependant, vos exceptions personnalisées doivent être des exceptions non vérifiées, ce qui n'est pas toujours possible. De plus, je noterais que le risque qu'un développeur manque d'écrire throwsoit très faible: le développeur doit l'oublier et la méthode environnante ne doit rien retourner, sinon le compilateur détecterait un retour manquant. C'est une façon intéressante de combattre le système de type, et cela fonctionne, cependant.

Renouveler

Le réel rethrowpeut également être écrit en fonction, mais j'ai des problèmes avec son implémentation actuelle:

  • Il y a beaucoup de moulages inutiles comme Des moulages sont en effet nécessaires (voir commentaires):

    if (e instanceof ServiceUnavailableException) {
        ServiceUnavailableException ex = (ServiceUnavailableException) e;
        rethrow(ex);
    }
  • Lors du lancement / retour de nouvelles exceptions, l'ancienne est supprimée; dans le code suivant, a MailLoginNotAvailableExceptionne me permet pas de savoir quelle connexion n'est pas disponible, ce qui n'est pas pratique; de plus, les traces de pile seront incomplètes:

    private void rethrow(EntityAlreadyExistsException e)
        throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
        switch (e.getDetail()) {
            case LOGIN_ALREADY_TAKEN:
                throw new MailLoginNotAvailableException();
            case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
                throw new MailEmailAddressAlreadyForwardedToException();
        }
    }
  • Pourquoi le code d'origine ne lance-t-il pas ces exceptions spécialisées en premier lieu? Je soupçonne qu'il rethrowest utilisé comme couche de compatibilité entre un sous-système (de publipostage) et une logique commerciale (peut-être que l'intention est de masquer les détails d'implémentation comme les exceptions levées en les remplaçant par des exceptions personnalisées). Même si je conviens qu'il serait préférable d'avoir du code sans capture, comme suggéré dans la réponse de Pete Becker , je ne pense pas que vous aurez l'occasion de supprimer le code catchet rethrowici sans refactorisations majeures.

coredump
la source
1
Les moulages ne sont pas inutiles. Ils sont obligatoires, car il n'y a pas de répartition dynamique basée sur le type d'argument. Le type d'argument doit être connu au moment de la compilation pour appeler la bonne méthode.
Joe23
Dans votre logAndWrapméthode, vous pouvez lever l'exception au lieu de la renvoyer, mais conservez le type de retour (Runtime)Exception. De cette façon, le bloc catch peut rester comme vous l'avez écrit (sans le jeter dans du code inaccessible). Mais il a l'avantage supplémentaire que vous ne pouvez pas oublier throw. Si vous retournez l'exception et oubliez le lancer, cela entraînera l'ingestion silencieuse de l'exception. Lancer tout en ayant un type de retour RuntimeException rendra le compilateur heureux tout en évitant ces erreurs. C'est ainsi que la goyave Throwables.propagateest mise en œuvre.
Joe23
@ Joe23 Merci pour la clarification sur la répartition statique. A propos du lancement d'exceptions à l'intérieur de la fonction: voulez-vous dire que l'erreur possible que vous décrivez est un bloc catch qui appelle logAndWrapmais ne fait rien avec l'exception renvoyée? C'est intéressant. Dans une fonction qui doit renvoyer une valeur, le compilateur remarquera qu'il existe un chemin sans retour de valeur, mais cela est utile pour les méthodes qui ne renvoient rien. Merci encore.
coredump
1
C'est exactement ce que je mentionne. Et juste pour souligner à nouveau le point: ce n'est pas quelque chose que j'ai trouvé et auquel je dois le crédit, mais glané de la source de la goyave.
Joe23
@ Joe23 J'ai ajouté une référence explicite à la bibliothèque
coredump
52

Cette rethrow(e);fonction viole le principe qui dit que dans des circonstances normales, une fonction reviendra, tandis que dans des circonstances exceptionnelles, une fonction lèvera une exception. Cette fonction viole ce principe en lançant une exception dans des circonstances normales. C'est la source de toute la confusion.

Le compilateur suppose que cette fonction sera renvoyée dans des circonstances normales, pour autant que le compilateur puisse le dire, l'exécution peut atteindre la fin de la retrieveUserMailConfigurationfonction, auquel cas c'est une erreur de ne pas avoir d' returninstruction. Le RuntimeExceptionprojeté là-bas est censé atténuer cette préoccupation du compilateur, mais c'est une façon plutôt maladroite de le faire. Une autre façon de prévenir l' function must return a valueerreur consiste à ajouter une return null; //to keep the compiler happydéclaration, mais c'est tout aussi maladroit à mon avis.

Donc, personnellement, je remplacerais ceci:

rethrow(e);

avec ça:

report(e); 
throw e;

ou, mieux encore, (comme l' a suggéré coredump ) avec ceci:

throw reportAndTransform(e);

Ainsi, le flux de contrôle serait rendu évident pour le compilateur, de sorte que votre version finale throw new RuntimeException("cannot reach here");deviendrait non seulement redondante, mais en fait même pas compilable, car elle serait signalée par le compilateur comme du code inaccessible.

C'est le moyen le plus élégant et le plus simple de sortir de cette horrible situation.

Mike Nakis
la source
1
-1 Suggérer de diviser la fonction en deux instructions peut introduire des erreurs de programmation en raison d'une mauvaise copie / collage; Je suis aussi un peu inquiet au sujet du fait que vous avez modifié votre question pour suggérer throw reportAndTransform(e)un ou deux minutes après que je posté ma propre réponse. Peut-être que vous avez modifié votre question indépendamment de cela, et je m'excuse à l'avance si c'est le cas, mais sinon, donner un peu de crédit est quelque chose que les gens font habituellement.
coredump
4
@coredump J'ai en fait lu votre suggestion, et j'ai même lu une autre réponse qui vous attribue cette suggestion, et je me suis souvenu que j'avais effectivement utilisé une telle construction dans le passé, j'ai donc décidé de l'inclure dans ma réponse. Je pensais que ce n'était pas si important pour inclure une attribution, parce que nous ne parlons pas d'une idée originale ici, mais si vous contestez cela, je ne veux pas vous aggraver, donc l'attribution est maintenant là, compléter avec un lien. À votre santé.
Mike Nakis
Ce n'est pas que j'ai un brevet pour cette construction, ce qui est assez courant; mais imaginez que vous écrivez une réponse et peu de temps après, elle est «assimilée» par une autre. L'attribution est donc très appréciée. Merci.
coredump
3
Il est faux de rien en soi avec une fonction qui ne retourne pas. Cela se produit tout le temps, par exemple, dans les systèmes en temps réel. Le problème ultime est une faille dans java en ce que le langage analyse et applique le concept d'exactitude des langages et pourtant le langage ne fournit pas de mécanisme pour annoter d'une manière ou d'une autre qu'une fonction ne revient pas. Cependant, il existe des modèles de conception qui permettent de contourner cela, tels que throw reportAndTransform(e). De nombreux modèles de conception sont des correctifs qui manquent de fonctionnalités linguistiques. C'est l'un d'eux.
David Hammen
2
En revanche, de nombreuses langues modernes sont suffisamment complexes. Transformer chaque modèle de conception en une fonction de langage de base les alourdit encore plus. Ceci est un bon exemple d'un modèle de conception qui n'appartient pas au langage principal. Comme écrit, il est bien compris non seulement par le compilateur, mais aussi par de nombreux autres outils qui doivent analyser le code.
MSalters
7

L' throwerreur a probablement été ajoutée pour contourner l'erreur "la méthode doit renvoyer une valeur" qui se produirait autrement - l'analyseur de flux de données Java est suffisamment intelligent pour comprendre qu'il n'est pas returnnécessaire après a throw, mais pas après votre rethrow()méthode personnalisée , et qu'il n'y a pas d' @NoReturnannotation que vous pourriez utiliser pour résoudre ce problème.

Néanmoins, la création d'une nouvelle exception dans du code inaccessible semble superflue. J'écrirais simplement return null, sachant que cela ne se produit jamais.

Kilian Foth
la source
Vous avez raison. J'ai vérifié ce qui se passerait si je commentais la throw new...ligne et il se plaignait de l'absence de déclaration de retour. Est-ce une mauvaise programmation? Existe-t-il une convention concernant la substitution d'une déclaration de retour inaccessible? Je vais laisser cette question sans réponse pendant un certain temps pour encourager plus de commentaires. Merci néanmoins pour votre réponse.
Sok Pomaranczowy
3
@SokPomaranczowy Ouais, c'est une mauvaise programmation. Cette rethrow()méthode masque le fait que le catchretire l'exception. J'éviterais de faire quelque chose comme ça. De plus, bien sûr, il rethrow()pourrait ne pas réussir à lever l'exception, auquel cas quelque chose d'autre se produit.
Ross Patterson
26
Veuillez ne pas remplacer l'exception par return null. À l'exception, si quelqu'un à l'avenir rompt le code afin que la dernière ligne devienne accessible, il sera immédiatement clair lorsque l'exception sera levée. Si vous à la place return null, vous avez maintenant une nullvaleur flottante dans votre application qui n'était pas attendue. Qui sait où dans l'application NullPointerExceptionse produira? À moins que vous n'ayez de la chance et que ce soit immédiatement après l'appel à cette fonction, il sera très difficile de trouver le vrai problème.
unholysampler
5
@MatthewRead L'exécution de code destiné à être inaccessible est un bogue grave, return nullest très susceptible de masquer le bogue car vous ne pouvez pas distinguer les autres cas où il revient nullde ce bogue.
CodesInChaos
4
En outre, si la fonction retourne déjà null dans certaines circonstances et que vous utilisez également un retour nul dans cette circonstance, les utilisateurs ont maintenant deux scénarios possibles dans lesquels ils peuvent se trouver lorsqu'ils obtiennent un retour nul. Parfois, cela n'a pas d'importance, car le retour nul signifie déjà "il n'y a pas d'objet et je ne spécifie pas pourquoi", donc l'ajout d'une raison supplémentaire possible ne fait pas de différence. Mais souvent, ajouter de l'ambiguïté est mauvais, et cela signifie certainement que les bogues seront initialement traités comme "certaines circonstances" au lieu de ressembler immédiatement à "ceci est cassé". Échouez tôt.
Steve Jessop
6

le

throw new RuntimeException("cannot reach here");

L'instruction indique clairement à une PERSONNE lisant le code ce qui se passe, c'est donc beaucoup mieux que de retourner null par exemple. Cela facilite également le débogage si le code est modifié de manière inattendue.

Mais rethrow(e)semble juste mal! Donc, dans votre cas, je pense que la refactorisation du code est une meilleure option. Voir les autres réponses (j'aime mieux coredump) pour savoir comment trier votre code.

Ian
la source
4

Je ne sais pas s'il y a une convention.

Quoi qu'il en soit, une autre astuce serait de faire comme ça:

private <T> T rethrow(Exception exception) {
    // or whatever it actually does
    Log.e("Ouch! " + exception.getMessage());
    throw new CustomWrapperException(exception);
}

Permettant cela:

try {
     return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
     return rethrow(e);
}

Et aucun artificiel RuntimeExceptionn'est plus nécessaire. Même si rethrowjamais ne retourne réellement de valeur, c'est assez bon pour le compilateur maintenant.

Il retourne une valeur en théorie (signature de méthode), puis il est exempté de le faire car il lève une exception à la place.

Oui, cela peut sembler étrange, mais encore une fois - lancer un fantôme RuntimeExceptionou retourner des nulls qui ne seront jamais vus dans ce monde - ce n'est pas exactement une chose de beauté non plus.

Dans un souci de lisibilité, vous pouvez renommer rethrowet avoir quelque chose comme:

} catch (Exception e) {
     return nothingJustRethrow(e);
}
Konrad Morawski
la source
2

Si vous supprimez trycomplètement le bloc, vous n'avez pas besoin du rethrowou du throw. Ce code fait exactement la même chose que l'original:

public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
    return translate(mailManagementService.retrieveUserMailConfiguration(id));
}

Ne vous laissez pas tromper par le fait qu'il provient de développeurs plus expérimentés. C'est de la pourriture de code, et ça arrive tout le temps. Il suffit de le réparer.

EDIT: J'ai mal interprété rethrow(e)comme relançant simplement l'exception e. Si cette rethrowméthode fait en fait autre chose que de relancer l'exception, sa suppression modifie la sémantique de cette méthode.

Pete Becker
la source
Les gens continueront de croire que les captures génériques qui ne gèrent rien sont en fait des gestionnaires d'exceptions. Votre version ignore cette hypothèse erronée en ne faisant pas semblant de gérer ce qu'elle ne peut pas. L'appelant de retrieveUserMailConfiguration () s'attend à récupérer quelque chose. Si cette fonction ne peut pas renvoyer quelque chose de raisonnable, elle devrait échouer rapidement et fort. C'est comme si les autres réponses ne voyaient aucun problème avec catch(Exception)ce qui est une odeur de code odieuse.
msw
1
@msw - Codage culte du fret.
Pete Becker du
@msw - d'un autre côté, cela vous donne un endroit pour définir un point d'arrêt.
Pete Becker
1
C'est comme si vous ne voyiez aucun problème à rejeter toute une partie de la logique. Votre version ne fait clairement pas la même chose que l'original, qui d'ailleurs, échoue déjà rapidement et fort. Je préférerais écrire des méthodes sans accroc comme celle de votre réponse, mais lorsque vous travaillez avec du code existant, nous ne devons pas rompre les contrats avec d'autres parties fonctionnelles. Il se peut que ce qui est fait dans rethrowsoit inutile (et ce n'est pas le point ici), mais vous ne pouvez pas simplement vous débarrasser du code que vous n'aimez pas et dire qu'il est équivalent à l'original alors qu'il ne l'est clairement pas. Il y a des effets secondaires dans la rethrowfonction personnalisée .
coredump
1
@ jpmc26 Le cœur de la question concerne l'exception "ne peut pas atteindre ici", qui pourrait survenir pour d'autres raisons que le bloc try / catch précédent. Mais je suis d'accord que le bloc sent le poisson, et si cette réponse avait été formulée plus soigneusement à l'origine, elle aurait gagné beaucoup plus de votes positifs (je n'ai pas baissé les voix, au fait). Le dernier montage est bon.
coredump