Le problème
J’ai toujours détesté être obligé d’attraper une exception, en grande partie parce que :
- Rappelez-vous que le code que vous avez écrit apporte la sauvegarde de la base de données, ajoute de l’espace disque ou de la mémoire, accorde les privilèges de fichier corrects en tant que root ? Moi non plus. Si un problème réel survient qui échappe au contrôle de votre code, alors par définition, vous ne pouvez pas changer le résultat. Alors à quoi bon vous forcer à le gérer ?
- Ce n’est pas parce que quelque chose est un problème pour toi que c’est un problème pour moi. Par exemple, dans Java JNI (essentiellement un magasin clé/valeur), si vous recherchez une valeur pour une clé donnée, mais que la clé n’existe pas, une exception est levée. Il semble que les concepteurs de JNI ont supposé qu’il s’agissait de la seule source d’information pour une valeur donnée. Et si ce n’est pas le cas ?
- Puisqu’il n’y a rien que vous puissiez vraiment faire en réponse à une exception, ils finissent par être renvoyés au sommet, où un service Web renvoie un code HTTP 500, une interface graphique affiche un « il y a un problème que nous ne pouvons pas résoudre » dialogue à l’utilisateur.
- Si une méthode d’interface ne déclare aucun type d’exception vérifié, l’implémentation ne peut pas lever d’exception vérifiée. Mais que se passe-t-il si l’implémentation doit effectuer des appels qui lèvent des exceptions vérifiées ?
- Parfois, vous êtes obligé d’utiliser la gestion des exceptions comme une instruction if/then
- Parfois, vous devez avoir des blocs try/catch imbriqués, ce qui peut être difficile à raisonner et ne sont invariablement pas testés unitairement pour chaque instruction catch.
Stratégies
Si vous regardez le code Java comme exemple, les gens utilisent diverses stratégies pour essayer de gérer les exceptions vérifiées, telles que :
- Écrivez une classe MyLibraryException, que chaque méthode qui lève une exception vérifiée utilise. Si le code de la bibliothèque doit gérer tout autre type d’exception, il l’enveloppe dans MyLibraryException.
- Relancez les exceptions vérifiées en tant que RuntimeException.
- Dans des cas tels que Integer.valueOf(String), vous devez intercepter l’exception NumberFormatException pour gérer les chaînes qui ne sont pas des entiers, à moins que vous ne puissiez garantir que les chaînes sont toujours des entiers.
Résultats
Cela conduit à divers résultats problématiques :
- Si vous utilisez plusieurs bibliothèques (ou des bibliothèques qui dépendent de bibliothèques), même si vous pouviez réellement faire quelque chose pour gérer le problème racine, il peut être enfoui dans un nombre inconnaissable de classes d’enveloppe d’exception de différentes bibliothèques dans la trace de pile.
- Dans un programme d’une complexité réelle et d’une chaîne de dépendances non triviale, il est tout simplement impossible d’utiliser un modèle de codage unique pour la gestion des exceptions. Vous devez utiliser plusieurs stratégies.
- Différents développeurs – comme chaque problème de codage – ont leur propre façon de gérer cela, qu’ils peuvent soutenir comme « la meilleure ». Bonne chance pour obtenir une gestion cohérente des exceptions sur une base de code de taille décente et un roulement d’équipe.
L’une des principales raisons pour lesquelles j’aime aller
En fin de compte, c’est pourquoi j’aime l’utilisation de Go de panique et de report à la place des exceptions vérifiées. Il présente des avantages indéniables :
- Vous n’êtes jamais obligé d’attraper quoi que ce soit – en fait, il n’y a même pas d’instruction catch.
- Defer ne se limite pas à la gestion des exceptions, il peut également être utilisé pour simplement s’assurer qu’un certain nombre de ressources sont fermées avant que la portée ne se termine.
- Vous pouvez introduire des portées avec des fonctions en ligne pour contrôler à quel code une instruction defer s’applique – je trouve cela particulièrement utile dans les tests unitaires, où je veux une seule fonction de test pour vérifier que chacune des paniques dans la fonction testée se produit uniquement lorsqu’elles sont Supposé.
Qu’en est-il des autres langues
Et si vous utilisiez un langage comme Java ? Je pense qu’une bonne solution consiste à écrire une classe Try qui a diverses méthodes statiques, chacune représentant une situation particulière. Les noms des méthodes doivent être suffisamment descriptifs pour ne pas avoir à lire constamment la documentation une fois que vous avez compris.
Si vous avez besoin d’une situation try/catch imbriquée, elle devient des appels de méthode imbriqués. Fondamentalement, il offre une solution fonctionnelle au problème et constitue une utilisation particulièrement bonne des lambdas. Je trouve qu’un code fonctionnel bien écrit est presque toujours plus concis, plus facile à lire et plus facile à tester (car il a moins de contexte et est généralement exempt du problème de la « banana gorilla jungle »).
Quelques exemples Essayez les méthodes de classe
Tout d’abord, nous avons besoin de quelques interfaces fonctionnelles (interfaces compatibles avec les lambdas) :
/**
* A functional interface for executing logic with no arguments, a side effect.
*/
@FunctionalInterface
public interface TryTo {
void execute() throws Throwable;
}
/**
* A version of {@link Supplier} that allows throwing any kind of exception.
* @param <T> the type to return
*/
@FunctionalInterface
public interface TrySupplier<T> {
T get() throws Throwable;
}
La classe Try peut alors utiliser les interfaces fonctionnelles ci-dessus :
Et voici quelques extraits de code réels qui appellent certaines des méthodes ci-dessus :
// This method is part of a class that abstracts reading and writing fields of a Java class
/**
* set the value of the field on the given instance
*
* @param instance the instance to set the value of
* @param value the value to set
*/
public void set(final Object instance, final Object value) {
Try.to(() -> setter.invoke(instance, value));
}
// The next two methods are part of a class that adapts a JDBC ResultSet into an Iterator.
// The class is also AutoCloseable since a ResultSet needs to be closed.
public boolean hasNext() {
// Allow user to call hasNext multiple times in a row
if (mode == IterationMode.HAS_NEXT) {
return true;
}
if (mode == IterationMode.DOES_NOT_HAVE_NEXT) {
return false;
}
final boolean hasNext = Try.get(rs::next).booleanValue();
mode = hasNext ? IterationMode.HAS_NEXT : IterationMode.DOES_NOT_HAVE_NEXT;
return hasNext;
}
// Close the result set and statement
@Override
public void close() {
Try.to(
Try.all(
rs::close,
stmt::close
)
);
}
Conclusion
Quand j’ai le choix de la langue, j’utilise Go. Pas seulement à cause de la panique/du report, mais aussi parce que la communauté Go épouse la simplicité, moins de code, de petites bibliothèques et évite généralement les grands frameworks.
Si je dois utiliser Java mais que j’ai le choix, je choisirais d’utiliser une classe Try et de ne pas utiliser de grands frameworks comme Spring et JPA. le code et la mémoire sont utilisés.
De plus, si un microservice n’a que quelques milliers de lignes de code avec une poignée de requêtes, pourquoi avez-vous besoin de ces grands frameworks de toute façon ?