|
errière
le terme de "gestion des erreurs" se cache
souvent un spectre beaucoup plus large que le
simple fait de gérer une erreur. En effet, il
faut :
-
identifier ce qu'est une
erreur d'exécution
-
définir comment la
transporter
-
définir pour qui cette
erreur est significative
-
définir comment la
traiter
Dans cette article nous
allons présenter chacun de ces aspects et les
appliquer à la plateforme .Net.
Qu'est ce qu'une erreur d'exécution?
La définition d'une erreur d'exécution n'est bien évidement pas
liée à la plateforme .Net, Java EE ou autre et donner une définition n'est pas
chose simple. Prenons l'exemple d'une ouverture de connexion TCP/IP
avec un serveur dont la connexion échoue. Ceci doit être considéré
comme une erreur d'exécution au niveau du protocole. Par contre si l'on
invoque sur un serveur une liste de services et qu'aucun n'est supporté
par le serveur il n'y a pas d'erreur d'exécution (enfin du moins au
niveau du protocole).
Donc pour résumer il n'y a erreur que lorsque le contrat "moral"
entre les 2 partenaires est brisé. Ou encore, une erreur d'exécution se
produit lorsqu'un partenaire ne peut pas réaliser ce pour quoi il a
été développé.
Transporter les erreurs
Il y a 2 méthodes pour transporter les erreurs, les codes de retour
ou les exceptions. La plateforme .Net est clairement orientée
exception. Mais pourquoi ? Et surtout comment utiliser ce mode de
transport le plus efficacement possible.
Intérêt des exceptions
Voici donc une liste non exhaustive de raisons de choisir de
transporter les erreurs par le biais d'exceptions:
- L'exception favorise les APIs dites consistantes, car celles-ci ne
sont utilisées (et surtout ne doivent être utilisées) que pour transporter
une erreur. A contrario, un code de retour peut rentre compte d'une
multitude de choses (Erreur, warning, nombre d'octets reçus).
- L'exception peut être utilisée dans tous les cas de figure:
méthode, propriété ou constructeur.
- L'exception permet de rendre le code plus robuste car il est très
simple d'ignorer un code de retour, alors qu'ignorer une
exception est beaucoup plus complexe. D'ailleurs il ne faut pas
attraper une exception si l'on ne sait pas comment la traiter. Si
l'exception remonte jusqu'au runtime alors il y a un bug dans
l'application, on pourra l'identifier, le corriger et rendre le système
encore plus robuste.
- On sépare le code métier du code du traitement d'erreur (block
catch) ceci rendant par la même le code métier beaucoup plus lisible.
- L'exception peut transporter des informations très complètes sur la
cause de l'erreur.
- Et puis chose très agréable lors du lancement d'une exception le
debugger peut directement pointer sur la ligne de code d'où l'exception
a été lancée.
Clairement l'utilisation d'exceptions pour transporter les erreurs
procure beaucoup d'avantages. Mais il faut également utiliser se
mécanisme correctement voici quelques guidelines.
Guidelines
1) Toutes les exceptions d'une API publique doivent être documentées.
2) Il ne faut pas différencier les exceptions sur un de leurs membres (par
exemple : je fais ceci si le Message d'erreur commence par "Z"). Il faut éviter
de lancer une exception explicitement dans un block finally.
3) Une bonne pratique consiste aussi à utiliser en priorité les exceptions fournies par le framework
lorsque celles-ci correspondent à l'erreur qui doit être transportée.
4) On doit toujours envoyer une exception spécifique (par exemple: on lance ArgumentNullException plutôt que
ArgumentException) plutôt qu'une classe générique.
5) Il ne faut pas non plus utiliser de block catch sur des exceptions non spécifique
(ex: catch(System.Exception e){...}). Lorsque l'on attrape une exception
il faut exactement savoir ce que l'on va en faire car le catch à un
coût. De plus, en attrapant System.Exception on aura perdu toute la
sémantique de l'erreur. On sait juste que quelque chose s'est mal
passé.
6) Utiliser un block try finally lorsque l'on veut libérer des
ressources mais sans chercher à traiter l'erreur.
FileStream stream = null;
try{ stream = new FileStream(...); }
finally{ if(steam!=null)stream.Close(); }
Il n'y a là aucun problème de performance.
7) Ne pas lancer explicitement les exceptions de type
StackOverFlowException, OutOfMemoryException,
ComException,
ExecutionEngineException,... Ces exceptions ne sont lancées que par
l'infrastructure du CLR.
8) Ne pas permettre à une API publique de lancer des exceptions du
type NullreferenceException, IndexOutOfRangeException,
AccessViolationException. Pour cela il suffit de vérifier la validité
des arguments et ensuite de renvoyer une exception de type
ArgumentNullException ou ArgumentOutOfRangeException.
9) Lorsqu'un objet est dans un état invalide renvoyer
InvalidOperationException.
10) Il peut arriver que les exceptions disponibles ne soit pas
utilisables il faut donc en créer de nouvelles. Et bien évidement la
aussi quelques règles doivent être respectées.
11) Éviter des hiérarchies très ou trop profondes.
12) Toujours se dériver de System.Exception ou d'une des autres
exceptions de base.
13) Le nom de la classe doit toujours se terminer par le suffixe
Exception.
14) Une Exception doit être sérialisable.
15) Une exception doit au moins proposer ces constructeur:
public class SomeException : Exception, ISerializable{ public SomeException(); public SomeException(string message); public SomeException(string message, Exception inner); // this constructor is needed for serialization protected SomeException(SerializationInfo info, StreamingContext ctx); }
Les exceptions présentent beaucoup d'avantages mais également un
gros inconvénient, les performances chutent lors du "lancé" d'exception.
Vous me direz lorsqu'une exception est lancée on se trouve dans un cas
d'erreur, et le faite de perdre en performance est raisonnablement
acceptable. Cependant, pour certaines applications, ceci n'est pas
acceptable. Imaginons le transfert d'un gros volume de données par le réseau;
vous n'allez pas ralentir ou pire annuler le transfert parce que
quelqu'un n'a pas écrit la date correctement. Dans ce cas on peut
utiliser le Try-Parse pattern; DateTime fournit un exemple
d'implémentation.
Traiter l'erreur
On a vu précédemment que l'utilisation d'exception permettait de
séparer le code métier du code de traitement de l'erreur. Pour réaliser
cette séparation on utilise un bock try catch. Mais il peut arriver que
le même traitement d'erreur se répète à plusieurs endroits dans le
code. Ou encore, on souhaite lors de toutes les phases de traitement
d'erreur (block catch) effectuer une opération tel que laisser une
trace dans un log ou écrire dans l'event viewer...
La mauvaise méthode pour implémenter ces traitements serait d'utiliser le
copier coller (d'ailleurs en passant le copier coller est toujours la
mauvaise méthode). Il faut aussi se rappeler d'une règle de la
programmation objet: lorsqu'une classe doit changer, elle ne doit
changer que pour une seule raison. Alors si notre code métier change
pourquoi devrait-on changer notre traitement d'erreur? La réponse est
simple parce que le traitement d'erreur fait partie intégrante du code
métier (même s'il est localisé dans un block catch). Il nous faudrait
une infrastructure qui externaliserait complètement ce code de
traitement d'erreur et qui plus est soit une infrastructure générique
donc réutilisable.
Externaliser le traitement en utilisant une infrastructure générique
L'idée de base est de pouvoir définir dans des objets un traitement
générique de base par exemple un rollback sur une session DB, écrire
une trace, logger, relancer une exception avec plus de contexte. Et de
pouvoir les agréger afin de construire notre traitement d'erreur (par
exemple : rollBack + trace). Pour cela on va définir une interface dont
voici le code:
public interface IErrorHandlingStrategy <ExceptionType> { void Handle(ExceptionType _exception_in); }
Cette interface utilise les Generics de .Net 2.0 pour typer
l'exception qui sera à traiter. On est maintenant capable d'implémenter
nos traitements mais il nous faut quelqu'un pour les exécuter. C'est le
rôle de l'ExceptionHandler dont voici le code:
public class ExceptionHandler <ExceptionType, ErrorHandlingStrategyType> where ErrorHandlingStrategyType : IErrorHandlingStrategy <ExceptionType>, new() { private ErrorHandlingStrategyType m_strategy; public ExceptionHandler() { m_strategy = new ErrorHandlingStrategyType(); }
public void Handle(ExceptionType _exception_in) { m_strategy.Handle(_exception_in); } }
Lui aussi utilise les Generics, lors de son instantiation on lui
passera l'exception qu'il aura à traiter et la manière de la traiter on
obtiendra ainsi un code ressemblant à celui-ci:
try{ // something throws an exception of type IOException }
catch(IOException ex){ ExceptionHandler hdler = new ExceptionHandler < IOException,TraceAndReThrowStrategy >(); hdler.handle(ex); }
Avec cette infrastructure, notre code métier ne sera pas impacté par
un changement de notre gestion d'erreur. On pourra factoriser et
réutiliser certains traitements et tout cela dans un environnement typesafe.
Et Pourquoi pas un peu d'AOP ?
Certains me diront mais on peut encore aller plus loin dans
l'externalisation du traitement de l'erreur... Et oui pourquoi pas
appeler notre tisseur préferé pour rajouter le traitement de l'erreur
là où bon nous semble? Ainsi le développeur vivrait dans un monde
parfait, sans erreurs, et seulement après coup; grâce à une bonne
documentation des erreurs on pourrait rajouter le traitement en lieu et
place et faire évoluer ce traitement au cours du temps sans jamais
impacter le code métier qui, je le rappelle est ce pourquoi nous sommes
rémunéré...
Conclusion
J'espère avoir démontré l'intérêt d'utiliser les exceptions pour
transporter les erreurs d'exécution plutôt que les codes de retour.
Cependant pour réaliser un error handling correct il ne suffit pas de
lancer des exceptions, il faut suivre un certain nombre de règles et il
faut globalement répondre à 3 questions:
- Quoi? :
Est ce une erreur d'exécution
- Pour qui?
: à qui est destinée cette erreur
- Où? : Ou
traiter cette erreur
Il faut également noter que le traitement des erreurs n'est pas
quelque chose de naturel pendant une phase de développement. Et que
pouvoir mettre en place des traitements d'erreurs génériques ou, encore
mieux, les tisser une fois l'implémentation fonctionnelle réalisée
changera radicalement la manière de développer.
|