Gestion des erreurs Kotlin / Java: comment le faire correctement?


Source


La gestion des erreurs dans tout développement joue un rôle crucial. Presque tout peut mal tourner dans le programme: l'utilisateur entrera des données incorrectes, ou elles peuvent provenir de telles via http, ou nous avons fait une erreur lors de l'écriture de la sérialisation / désérialisation et pendant le traitement, le programme se bloque avec une erreur. Oui, il se peut que l'espace disque soit insuffisant.


spoiler

¯_ (ツ) _ / ¯, il n'y a pas de voie unique, et dans chaque situation spécifique, vous devrez choisir l'option la plus appropriée, mais il y a des recommandations sur la façon de mieux le faire.


Préface


Malheureusement (ou tout simplement une telle vie?), Cette liste s'allonge encore et encore. Le développeur doit constamment penser au fait que quelque part une erreur peut se produire, et il y a 2 situations:


  • lorsque l'erreur attendue se produit lors de l'appel de la fonction que nous avons fournie et que nous pouvons essayer de traiter;
  • lorsqu'une erreur inattendue se produit pendant l'opération que nous n'avions pas prévue.

Et si les erreurs attendues sont au moins localisées, le reste peut se produire presque partout. Si nous ne traitons rien d'important, nous pouvons simplement planter avec une erreur (bien que ce comportement ne soit pas suffisant et que vous deviez au moins ajouter un message au journal des erreurs). Mais si en ce moment le paiement est en cours de traitement et que vous ne pouvez tout simplement pas tomber, mais au moins vous devez renvoyer une réponse au sujet de l'opération infructueuse?


Avant d'examiner les moyens de gérer les erreurs, quelques mots sur l'exception (exceptions):


Exception



Source


La hiérarchie des exceptions est bien décrite et vous pouvez trouver beaucoup d'informations à ce sujet, il est donc inutile de la peindre ici. Ce qui provoque parfois des discussions animées est des erreurs checked et unchecked checked . Et bien que la majorité ait accepté les exceptions unchecked comme préférées (à Kotlin, il n'y a aucune exception checked ), tout le monde n'est pas d'accord avec cela.


Les exceptions checked avaient vraiment la bonne intention d'en faire un mécanisme de gestion des erreurs pratique, mais la réalité a fait ses ajustements, bien que l'idée d'introduire toutes les exceptions qui peuvent être levées de cette fonction dans la signature soit compréhensible et logique.


Regardons un exemple. Supposons que nous ayons une fonction de method qui peut PanicException une PanicException vérifiée. Une telle fonction ressemblerait à ceci:


 public void method() throws PanicException { } 

D'après sa description, il est clair qu'elle peut lever une exception et qu'il ne peut y avoir qu'une seule exception. Semble-t-il assez confortable? Et bien que nous ayons un petit programme, c'est tout. Mais si le programme est légèrement plus grand et qu'il y a plus de telles fonctions, alors certains problèmes apparaissent.


Les exceptions vérifiées nécessitent, par spécification, que toutes les exceptions vérifiées possibles (ou un ancêtre commun pour elles) soient répertoriées dans la signature de la fonction. Par conséquent, si nous avons une chaîne d'appels a -> b -> c et que la fonction la plus imbriquée lève une sorte d'exception, elle doit être supprimée pour tout le monde dans la chaîne. Et s'il existe plusieurs exceptions, la fonction la plus haute de la signature doit avoir une description de toutes.


Ainsi, à mesure que le programme devient plus complexe, cette approche conduit au fait que les exceptions à la fonction supérieure s'effondrent progressivement aux ancêtres communs et finissent par se Exception à Exception . Ce qui sous cette forme devient similaire à une exception unchecked vérifiée et annule tous les avantages des exceptions vérifiées.


Et étant donné que le programme, en tant qu’organisme vivant, est en constante évolution et évolution, il est presque impossible de prévoir à l’avance quelles exceptions peuvent en résulter. Et en conséquence, la situation est que lorsque nous ajoutons une nouvelle fonction avec une nouvelle exception, nous devons parcourir toute la chaîne de son utilisation et changer les signatures de toutes les fonctions. D'accord, ce n'est pas la tâche la plus agréable (même si les IDE modernes le font pour nous).


Mais le dernier, et probablement le plus gros clou des exceptions vérifiées "a conduit" les lambdas de Java 8. Il n'y a pas d'exceptions vérifiées ¯_ (ツ) _ / ¯ dans leur signature (puisque n'importe quelle fonction peut être appelée dans lambda, avec n'importe quel signature), donc tout appel de fonction avec une exception vérifiée de lambda le force à être encapsulé dans un transfert d'exception comme non vérifié:


 Stream.of(1,2,3).forEach(item -> { try { functionWithCheckedException(); } catch (Exception e) { throw new RuntimeException("rethrow", e); } }); 

Heureusement, dans la spécification JVM, il n'y a aucune exception vérifiée, donc dans Kotlin vous ne pouvez rien encapsuler dans le même lambda, mais simplement appeler la fonction souhaitée.


bien que parfois ...

Bien que cela entraîne parfois des conséquences inattendues, telles que, par exemple, le fonctionnement incorrect de @Transactional dans Spring Framework , qui "n'attend" que des exceptions non unckecked . Mais c'est plus une caractéristique du framework, et peut-être que ce comportement au printemps changera dans le futur problème de github .


Les exceptions elles-mêmes sont des objets spéciaux. Outre le fait qu'ils peuvent être "lancés" par des méthodes, ils collectent également stacktrace à la création. Cette fonctionnalité aide ensuite à l'analyse des problèmes et à la recherche d'erreurs, mais elle peut également entraîner des problèmes de performances si la logique d'application devient fortement liée aux exceptions levées. Comme indiqué dans l' article , la désactivation de l'assemblage stacktrace peut augmenter considérablement leurs performances dans ce cas, mais vous ne devez y recourir que dans des cas exceptionnels lorsque cela est vraiment nécessaire!


Gestion des erreurs


La principale chose à faire avec les erreurs «inattendues» est de trouver un endroit où vous pouvez les intercepter. Dans les langages JVM, cela peut être soit un point de création de flux, soit un point d'entrée / filtre pour la méthode http, où vous pouvez mettre un essai avec la gestion des erreurs unchecked . Si vous utilisez un framework, il est très probable qu'il ait déjà la possibilité de créer des gestionnaires d'erreurs courants, comme, par exemple, dans le Spring Framework, vous pouvez utiliser des méthodes avec l'annotation @ExceptionHandler .


Vous pouvez «lever» des exceptions à ces points de traitement central que nous ne voulons pas gérer à des endroits spécifiques en lançant les mêmes exceptions non unckecked (lorsque, par exemple, nous ne savons pas quoi faire à un endroit particulier et comment gérer l'erreur). Mais cette méthode n'est pas toujours appropriée, car elle peut parfois nécessiter de gérer l'erreur sur place et vous devez vérifier que tous les appels de fonction sont traités correctement. Réfléchissez aux moyens de procéder.


  1. Utilisez toujours des exceptions et le même try-catch:


      int a = 10; int b = 20; int sum; try { sum = calculateSum(a,b); } catch (Exception e) { sum = -1; } 

    Le principal inconvénient est que nous pouvons «oublier» de l'envelopper dans un try-catch à l'endroit de l'appel et ignorer la tentative de traitement en place, à cause de quoi l'exception se retrouvera au point commun du traitement des erreurs. Ici, nous pouvons aller aux exceptions checked (pour Java), mais nous obtiendrons ensuite tous les inconvénients mentionnés ci-dessus. Cette approche est pratique à utiliser si la gestion des erreurs en place n'est pas toujours requise, mais dans de rares cas, elle est nécessaire.


  2. Utilisez la classe scellée à la suite d'un appel (Kotlin).
    Dans Kotlin, vous pouvez limiter le nombre d'héritiers de classe, les rendre calculables au stade de la compilation - cela permet au compilateur de vérifier que toutes les options possibles sont analysées dans le code. En Java, vous pouvez cependant créer une interface commune et plusieurs descendants, en perdant les vérifications au niveau de la compilation.


     sealed class Result data class SuccessResult(val value: Int): Result() data class ExceptionResult(val exception: Exception): Result() val a = 10 val b = 20 val sum = when (val result = calculateSum(a,b)) { is SuccessResult -> result.value is ExceptionResult -> { result.exception.printStackTrace() -1 } } 

    Ici, nous obtenons quelque chose comme une approche d'erreur golang lorsque vous devez vérifier explicitement les valeurs résultantes (ou ignorer explicitement). L'approche est assez pratique et particulièrement pratique lorsque vous devez lancer un grand nombre de paramètres dans chaque situation. La classe Result peut être développée avec différentes méthodes qui facilitent l'obtention du résultat avec une exception levée ci-dessus, le cas échéant (c'est-à-dire que nous n'avons pas besoin de gérer l'erreur à l'endroit de l'appel). L'inconvénient principal ne sera que la création d'objets superflus intermédiaires (et une entrée légèrement plus verbeuse), mais il peut également être supprimé en utilisant inline classes en inline (si un argument nous suffit). et, comme exemple particulier, il existe une classe Result de Kotlin. Certes, ce n'est que pour un usage interne, comme à l'avenir, son implémentation peut changer légèrement, mais si vous souhaitez l'utiliser, vous pouvez ajouter l'indicateur de compilation -Xallow-result-return-type .


  3. Comme l'un des types possibles de la revendication 2, l'utilisation du type de la programmation fonctionnelle de Either , qui peut être soit un résultat soit une erreur. Le type lui-même peut être une classe sealed ou une classe en inline . Voici un exemple d'utilisation de l'implémentation de la bibliothèque de arrow :


     val a = 10 val b = 20 val value = when(val result = calculateSum(a,b)) { is Either.Left -> { result.a.printStackTrace() -1 } is Either.Right -> result.b } 

    Either convient le mieux à ceux qui aiment une approche fonctionnelle et qui aiment construire des chaînes d'appels.


  4. Utilisez l' Option ou le type nullable de Kotlin:


     fun testFun() { val a = 10 val b = 20 val sum = calculateSum(a,b) ?: throw RuntimeException("some exception") } fun calculateSum(a: Int, b: Int): Int? 

    Cette approche convient si la cause de l'erreur n'est pas très importante et lorsqu'elle n'en est qu'une. Une réponse vide est considérée comme une erreur et est renvoyée plus haut. L'enregistrement le plus court, sans créer d'objets supplémentaires, mais cette approche ne peut pas toujours être appliquée.


  5. Semblable à l'article 4, utilise uniquement une valeur de code dur comme marqueur d'erreur:


     fun testFun() { val a = 10 val b = 20 val sum = calculateSum(a,b) if (sum == -1) { throw RuntimeException(“error”) } } fun calculateSum(a: Int, b: Int): Int 

    C'est probablement la plus ancienne approche de gestion des erreurs qui soit revenue de C (ou même d'Algol). Il n'y a pas de surcharge, seulement un code qui n'est pas entièrement clair (avec des restrictions sur le choix du résultat), mais, contrairement à l'étape 4, il est possible de faire divers codes d'erreur si plus d'une exception possible est requise.



Conclusions


Toutes les approches peuvent être combinées en fonction de la situation, et aucune d'entre elles ne convient dans tous les cas.


Ainsi, par exemple, vous pouvez réaliser une approche golang des erreurs en utilisant sealed classes sealed , et là où ce n'est pas très pratique, passez aux erreurs unchecked vérifiées.


Ou, dans la plupart des endroits, nullable type nullable comme marqueur qu'il n'a pas été possible de calculer la valeur ou de l'obtenir quelque part (par exemple, comme indicateur que la valeur n'a pas été trouvée dans la base de données).


Et si vous avez du code entièrement fonctionnel avec une arrow ou une autre bibliothèque similaire, il est probablement préférable d'utiliser Either .


En ce qui concerne les serveurs http, il est plus facile de signaler toutes les erreurs aux points centraux et, à certains endroits seulement, de combiner l'approche nullable avec sealed classes sealed .


Je serai heureux de voir dans les commentaires que vous utilisez cela, ou peut-être existe-t-il d'autres méthodes de gestion des erreurs pratiques?


Et merci à tous ceux qui ont lu jusqu'au bout!

Source: https://habr.com/ru/post/fr471766/


All Articles