Je pense que j'ai lu la même interview de Bruce Eckel que vous avez faite - et cela m'a toujours dérangé. En fait, l'argument a été avancé par la personne interrogée (si c'est effectivement le poste dont vous parlez) Anders Hejlsberg, le génie MS derrière .NET et C #.
http://www.artima.com/intv/handcuffs.html
Fan que je sois de Hejlsberg et de son travail, cet argument m'a toujours semblé faux. Cela se résume essentiellement à:
"Les exceptions vérifiées sont mauvaises parce que les programmeurs les abusent simplement en les attrapant toujours et en les rejetant, ce qui conduit à des problèmes cachés et ignorés qui seraient autrement présentés à l'utilisateur".
Par "autrement présenté à l'utilisateur" je veux dire que si vous utilisez une exception d'exécution, le programmeur paresseux l'ignorera simplement (par opposition à l'attraper avec un bloc catch vide) et l'utilisateur le verra.
Le résumé du résumé de l'argument est que "les programmeurs ne les utiliseront pas correctement et ne pas les utiliser correctement est pire que de ne pas les avoir" .
Il y a une part de vérité dans cet argument et en fait, je soupçonne que la motivation de Goslings pour ne pas mettre des remplacements d'opérateurs en Java vient d'un argument similaire - ils confondent le programmeur car ils sont souvent abusés.
Mais à la fin, je trouve que c'est un faux argument de Hejlsberg et peut-être post-hoc créé pour expliquer le manque plutôt qu'une décision mûrement réfléchie.
Je dirais que, même si la surutilisation des exceptions vérifiées est une mauvaise chose et a tendance à entraîner une gestion bâclée par les utilisateurs, mais leur utilisation appropriée permet au programmeur API de donner un grand avantage au programmeur client API.
Maintenant, le programmeur API doit faire attention à ne pas lancer d'exceptions vérifiées partout, sinon ils ennuieront simplement le programmeur client. Le programmeur client très paresseux aura recours à la capture (Exception) {}
comme Hejlsberg l'avertit et tous les avantages seront perdus et l'enfer s'ensuivra. Mais dans certaines circonstances, rien ne peut remplacer une bonne exception vérifiée.
Pour moi, l'exemple classique est l'API d'ouverture de fichier. Chaque langage de programmation de l'histoire des langages (sur les systèmes de fichiers au moins) possède une API quelque part qui vous permet d'ouvrir un fichier. Et chaque programmeur client utilisant cette API sait qu'il doit faire face au cas où le fichier qu'il essaie d'ouvrir n'existe pas. Permettez-moi de reformuler cela: Chaque programmeur client utilisant cette API doit savoir qu'il doit gérer ce cas. Et il y a le hic: le programmeur d'API peut-il les aider à savoir qu'ils doivent y faire face en commentant seuls ou peut-il en effet insister le client s'en occupe.
En C, l'idiome va quelque chose comme
if (f = fopen("goodluckfindingthisfile")) { ... }
else { // file not found ...
où fopen
indique l'échec en renvoyant 0 et C (bêtement) vous permet de traiter 0 comme un booléen et ... Fondamentalement, vous apprenez cet idiome et vous êtes d'accord. Mais si vous êtes un noob et que vous n'avez pas appris l'idiome. Ensuite, bien sûr, vous commencez avec
f = fopen("goodluckfindingthisfile");
f.read(); // BANG!
et apprenez à la dure.
Notez que nous ne parlons ici que de langages fortement typés: il y a une idée claire de ce qu'est une API dans un langage fortement typé: c'est un large éventail de fonctionnalités (méthodes) à utiliser avec un protocole clairement défini pour chacune.
Ce protocole clairement défini est généralement défini par une signature de méthode. Ici, fopen requiert que vous lui passiez une chaîne (ou un caractère * dans le cas de C). Si vous lui donnez autre chose, vous obtenez une erreur de compilation. Vous n'avez pas suivi le protocole - vous n'utilisez pas correctement l'API.
Dans certaines langues (obscures), le type de retour fait également partie du protocole. Si vous essayez d'appeler l'équivalent de fopen()
dans certaines langues sans l'assigner à une variable, vous obtiendrez également une erreur de compilation (vous ne pouvez le faire qu'avec des fonctions void).
Le point que j'essaie de faire est le suivant: dans un langage de type statique, le programmeur d'API encourage le client à utiliser correctement l'API en empêchant son code client de se compiler s'il fait des erreurs évidentes.
(Dans un langage typé dynamiquement, comme Ruby, vous pouvez passer n'importe quoi, par exemple un flottant, comme nom de fichier - et il se compilera. Pourquoi harceler l'utilisateur avec des exceptions vérifiées si vous ne contrôlez même pas les arguments de la méthode. les arguments avancés ici ne s'appliquent qu'aux langages de type statique.)
Alors, qu'en est-il des exceptions vérifiées?
Eh bien, voici l'une des API Java que vous pouvez utiliser pour ouvrir un fichier.
try {
f = new FileInputStream("goodluckfindingthisfile");
}
catch (FileNotFoundException e) {
// deal with it. No really, deal with it!
... // this is me dealing with it
}
Vous voyez cette prise? Voici la signature de cette méthode API:
public FileInputStream(String name)
throws FileNotFoundException
Notez qu'il FileNotFoundException
s'agit d'une exception vérifiée .
Le programmeur API vous dit ceci: "Vous pouvez utiliser ce constructeur pour créer un nouveau FileInputStream mais vous
a) doit transmettre le nom du fichier sous forme de chaîne
b) doit accepter la possibilité que le fichier ne soit pas trouvé lors de l'exécution "
Et c'est tout le point en ce qui me concerne.
La clé est fondamentalement ce que la question indique comme "des choses qui sont hors du contrôle du programmeur". Ma première pensée a été qu'il veut dire des choses qui ne sont pas dans l' API contrôle des programmeurs d' . Mais en fait, les exceptions vérifiées lorsqu'elles sont utilisées correctement devraient vraiment concerner des choses qui sont hors du contrôle du programmeur client et du programmeur API. Je pense que c'est la clé pour ne pas abuser des exceptions vérifiées.
Je pense que le fichier ouvert illustre bien le point. Le programmeur d'API sait que vous pourriez leur donner un nom de fichier qui s'avère inexistant au moment de l'appel de l'API, et qu'il ne pourra pas vous retourner ce que vous vouliez, mais devra lever une exception. Ils savent également que cela se produira assez régulièrement et que le programmeur client peut s'attendre à ce que le nom de fichier soit correct au moment où il a écrit l'appel, mais il peut aussi être erroné au moment de l'exécution pour des raisons indépendantes de sa volonté.
Donc, l'API le rend explicite: il y aura des cas où ce fichier n'existe pas au moment où vous m'appelez et vous feriez bien mieux de le gérer.
Ce serait plus clair avec un contre-cas. Imaginez que j'écris une API de table. J'ai le modèle de table quelque part avec une API incluant cette méthode:
public RowData getRowData(int row)
Maintenant, en tant que programmeur d'API, je sais qu'il y aura des cas où certains clients transmettront une valeur négative pour la ligne ou une valeur de ligne en dehors de la table. Je pourrais donc être tenté de lever une exception vérifiée et d'obliger le client à y faire face:
public RowData getRowData(int row) throws CheckedInvalidRowNumberException
(Je ne l'appellerais pas vraiment "Checked" bien sûr.)
C'est une mauvaise utilisation des exceptions vérifiées. Le code client va être plein d'appels pour récupérer les données de ligne, chacun d'entre eux devra utiliser un try / catch, et pour quoi faire? Vont-ils signaler à l'utilisateur que la mauvaise ligne a été recherchée? Probablement pas - car quelle que soit l'interface utilisateur entourant ma vue de table, elle ne devrait pas laisser l'utilisateur entrer dans un état où une ligne illégale est demandée. C'est donc un bug de la part du programmeur client.
Le programmeur API peut toujours prédire que le client va coder de tels bogues et devrait le gérer avec une exception d'exécution comme un IllegalArgumentException
.
Avec une exception cochée getRowData
, c'est clairement un cas qui va conduire le programmeur paresseux de Hejlsberg à simplement ajouter des captures vides. Lorsque cela se produit, les valeurs de ligne illégales ne seront pas évidentes même pour le testeur ou le débogage du développeur client, mais elles entraîneront plutôt des erreurs d'activation difficiles à localiser. Les roquettes Arianne exploseront après leur lancement.
D'accord, voici donc le problème: je dis que l'exception vérifiée FileNotFoundException
n'est pas seulement une bonne chose mais un outil essentiel dans la boîte à outils des programmeurs d'API pour définir l'API de la manière la plus utile pour le programmeur client. Mais CheckedInvalidRowNumberException
c'est un gros inconvénient, conduisant à une mauvaise programmation et doit être évité. Mais comment faire la différence.
Je suppose que ce n'est pas une science exacte et je suppose que cela sous-tend et peut-être justifie dans une certaine mesure l'argument de Hejlsberg. Mais je ne suis pas content de jeter le bébé avec l'eau du bain ici, alors permettez-moi d'extraire quelques règles ici pour distinguer les bonnes exceptions vérifiées des mauvaises:
Hors du contrôle du client ou fermé vs ouvert:
Les exceptions cochées ne doivent être utilisées que lorsque le cas d'erreur est hors de contrôle à la fois de l'API et du programmeur client. Cela a à voir avec l' ouverture ou la fermeture du système. Dans une interface utilisateur contrainte où le programmeur client a le contrôle, par exemple, sur tous les boutons, commandes clavier, etc. qui ajoutent et suppriment des lignes de la vue de table (un système fermé), il s'agit d'un bogue de programmation client s'il tente d'extraire des données de une ligne inexistante. Dans un système d'exploitation basé sur des fichiers où un nombre illimité d'utilisateurs / applications peuvent ajouter et supprimer des fichiers (un système ouvert), il est concevable que le fichier que le client demande a été supprimé à leur insu, de sorte qu'ils devraient être censés y faire face. .
Ubiquité:
Les exceptions cochées ne doivent pas être utilisées sur un appel d'API effectué fréquemment par le client. Par fréquemment, je veux dire de beaucoup d'endroits dans le code client - pas souvent à temps. Donc, un code client n'a pas tendance à essayer d'ouvrir beaucoup le même fichier, mais ma vue de table se fait RowData
partout à partir de différentes méthodes. En particulier, je vais écrire beaucoup de code comme
if (model.getRowData().getCell(0).isEmpty())
et il sera douloureux de devoir en finir avec try / catch à chaque fois.
Informer l'utilisateur:
Les exceptions cochées doivent être utilisées dans les cas où vous pouvez imaginer qu'un message d'erreur utile soit présenté à l'utilisateur final. C'est le "et que ferez-vous quand cela se produira?" question que j'ai soulevée ci-dessus. Il se rapporte également au point 1. Puisque vous pouvez prévoir que quelque chose en dehors de votre système API client pourrait empêcher le fichier d'être là, vous pouvez raisonnablement en informer l'utilisateur:
"Error: could not find the file 'goodluckfindingthisfile'"
Étant donné que votre numéro de ligne illégal a été provoqué par un bogue interne et sans faute de l'utilisateur, il n'y a vraiment aucune information utile que vous pouvez leur donner. Si votre application ne laisse pas les exceptions d'exécution passer par la console, elle finira probablement par leur donner un message laid comme:
"Internal error occured: IllegalArgumentException in ...."
En bref, si vous ne pensez pas que votre programmeur client peut expliquer votre exception d'une manière qui aide l'utilisateur, vous ne devriez probablement pas utiliser d'exception vérifiée.
Voilà donc mes règles. Un peu artificiel, et il y aura sans doute des exceptions (veuillez m'aider à les affiner si vous le souhaitez). Mais mon argument principal est qu'il existe des cas comme ceux FileNotFoundException
où l'exception vérifiée est une partie du contrat API aussi importante et utile que les types de paramètres. Nous ne devons donc pas nous en passer simplement parce qu'il est mal utilisé.
Désolé, je ne voulais pas rendre cela si long et gaufré. Permettez-moi de terminer par deux suggestions:
R: Programmeurs d'API: utilisez les exceptions vérifiées avec parcimonie pour préserver leur utilité. En cas de doute, utilisez une exception non vérifiée.
B: Programmeurs clients: prenez l'habitude de créer une exception encapsulée (google it) dès le début de votre développement. JDK 1.4 et versions ultérieures fournissent un constructeur RuntimeException
pour cela, mais vous pouvez également créer facilement le vôtre. Voici le constructeur:
public RuntimeException(Throwable cause)
Ensuite, prenez l'habitude de gérer une exception vérifiée et vous vous sentez paresseux (ou vous pensez que le programmeur d'API était trop zélé en utilisant l'exception vérifiée en premier lieu), ne vous contentez pas d'avaler l'exception, enveloppez-la et le repousser.
try {
overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
throw new RuntimeException(exception);
}
Mettez-le dans l'un des petits modèles de code de votre IDE et utilisez-le lorsque vous vous sentez paresseux. De cette façon, si vous avez vraiment besoin de gérer l'exception vérifiée, vous serez obligé de revenir et de le traiter après avoir vu le problème lors de l'exécution. Parce que, croyez-moi (et Anders Hejlsberg), vous ne reviendrez jamais à ce TODO dans votre
catch (Exception e) { /* TODO deal with this at some point (yeah right) */}