web-dev-qa-db-fra.com

Essayez / Attrapez / Enregistrez / Réexécutez - Est-ce qu'Anti Pattern?

Je peux voir plusieurs articles où l'importance de la gestion des exceptions à l'emplacement central ou à la limite du processus a été soulignée comme une bonne pratique plutôt que de joncher chaque bloc de code autour de try/catch. Je crois fermement que la plupart d'entre nous en comprenons l'importance, mais je vois des gens qui se retrouvent toujours avec un modèle anti-capture-journalisation, principalement parce que pour faciliter le dépannage lors de toute exception, ils veulent enregistrer des informations plus spécifiques au contexte (exemple: paramètres de méthode passé) et la façon est d'enrouler la méthode autour de try/catch/log/rethrow.

public static bool DoOperation(int num1, int num2)
{
    try
    {
        /* do some work with num1 and num2 */
    }
    catch (Exception ex)
    {
        logger.log("error occured while number 1 = {num1} and number 2 = {num2}"); 
        throw;
    }
}

Existe-t-il un bon moyen d'y parvenir tout en conservant les bonnes pratiques de gestion des exceptions? J'ai entendu parler du cadre AOP comme PostSharp pour cela, mais j'aimerais savoir s'il y a un inconvénient ou un coût de performance majeur associé à ces cadres AOP.

Merci!

21
rahulaga_dev

Le problème n'est pas le bloc catch local, le problème est le journal et relancez . Gérez l'exception ou encapsulez-la avec une nouvelle exception qui ajoute un contexte supplémentaire et lancez-la. Sinon, vous rencontrerez plusieurs entrées de journal en double pour la même exception.

L'idée ici est de améliorer la possibilité de déboguer votre application.

Exemple # 1: Gérez-le

try
{
    doSomething();
}
catch (Exception e)
{
    log.Info("Couldn't do something", e);
    doSomethingElse();
}

Si vous gérez l'exception, vous pouvez facilement rétrograder l'importance de l'entrée du journal des exceptions et il n'y a aucune raison de percoler cette exception dans la chaîne. C'est déjà réglé.

La gestion d'une exception peut consister à informer les utilisateurs qu'un problème est survenu, à consigner l'événement ou simplement à l'ignorer.

REMARQUE: si vous ignorez intentionnellement une exception, je recommande de fournir un commentaire dans la clause catch vide qui indique clairement pourquoi. Cela permet aux futurs responsables de savoir que ce n'était pas une erreur ou une programmation paresseuse. Exemple:

try
{
    context.DrawLine(x1,y1, x2,y2);
}
catch (OutOfMemoryException)
{
    // WinForms throws OutOfMemory if the figure you are attempting to
    // draw takes up less than one pixel (true story)
}

Exemple # 2: Ajouter un contexte supplémentaire et lancer

try
{
    doSomething(line);
}
catch (Exception e)
{
    throw new MyApplicationException(filename, line, e);
}

L'ajout de contexte supplémentaire (comme le numéro de ligne et le nom de fichier dans le code d'analyse) peut aider à améliorer la capacité de débogage des fichiers d'entrée - en supposant que le problème existe. Ceci est une sorte de cas spécial, donc reconditionner une exception dans une "ApplicationException" juste pour renommer cela ne vous aide pas à déboguer. Assurez-vous d'ajouter des informations supplémentaires.

Exemple # 3: Ne faites rien à l'exception

try
{
    doSomething();
}
finally
{
   // cleanup resources but let the exception percolate
}

Dans ce dernier cas, vous autorisez simplement l'exception à quitter sans la toucher. Le gestionnaire d'exceptions de la couche la plus externe peut gérer la journalisation. La clause finally est utilisée pour s'assurer que toutes les ressources nécessaires à votre méthode sont nettoyées, mais ce n'est pas l'endroit pour enregistrer que l'exception a été levée.

21
Berin Loritsch

Je ne crois pas que les captures locales soient un anti-modèle, en fait, si je me souviens bien, elles sont en fait appliquées en Java!

Ce qui est essentiel pour moi lors de la mise en œuvre du traitement des erreurs, c'est la stratégie globale. Vous voudrez peut-être un filtre qui capture toutes les exceptions à la limite de service, vous pouvez les intercepter manuellement - les deux sont très bien tant qu'il existe une stratégie globale, qui tombera dans les normes de codage de vos équipes.

Personnellement, j'aime détecter les erreurs dans une fonction lorsque je peux effectuer l'une des opérations suivantes:

  • Ajouter des informations contextuelles (telles que l'état des objets ou ce qui se passait)
  • Gérez l'exception en toute sécurité (comme une méthode TryX)
  • Votre système franchit une frontière de service et appelle une bibliothèque externe ou une API
  • Vous souhaitez intercepter et renvoyer un autre type d'exception (peut-être avec l'original comme exception interne)
  • L'exception a été levée dans le cadre d'une fonctionnalité d'arrière-plan de faible valeur

Si ce n'est pas un de ces cas, je n'ajoute pas de try/catch local. Si c'est le cas, selon le scénario, je peux gérer l'exception (par exemple une méthode TryX qui retourne un faux) ou relancer afin que l'exception soit gérée par la stratégie globale.

Par exemple:

public bool TryConnectToDatabase()
{
  try
  {
    this.ConnectToDatabase(_databaseType); // this method will throw if it fails to connect
    return true;
  }
  catch(Exception ex)
  {
     this.Logger.Error(ex, "There was an error connecting to the database, the databaseType was {0}", _databaseType);
    return false;
  }
}

Ou un exemple de retour:

public IDbConnection ConnectToDatabase()
{
  try
  {
    // connect to the database and return the connection, will throw if the connection cannot be made
  }
  catch(Exception ex)
  {
     this.Logger.Error(ex, "There was an error connecting to the database, the databaseType was {0}", _databaseType);
    throw;
  }
}

Ensuite, vous interceptez l'erreur en haut de la pile et présentez un message convivial à l'utilisateur.

Quelle que soit l'approche que vous adoptez, il est toujours utile de créer des tests unitaires pour ces scénarios afin de vous assurer que la fonctionnalité ne change pas et ne perturbe pas le flux du projet à une date ultérieure.

Vous n'avez pas mentionné la langue dans laquelle vous travaillez, mais vous êtes un développeur .NET et vous l'avez vu trop de fois pour ne pas le mentionner.

N'ÉCRIS PAS:

catch(Exception ex)
{
  throw ex;
}

Utilisation:

catch(Exception ex)
{
  throw;
}

Le premier réinitialise la trace de la pile et rend votre capture de niveau supérieur totalement inutile!

TLDR

La capture locale n'est pas un anti-modèle, elle peut souvent faire partie d'une conception et peut aider à ajouter un contexte supplémentaire à l'erreur.

7
Liath

Cela dépend beaucoup de la langue. Par exemple. C++ n'offre pas de traces de pile dans les messages d'erreur d'exception, donc le suivi de l'exception par le biais de captures-journaux-retouches fréquentes peut être utile. En revanche, Java et les langages similaires offrent de très bonnes traces de pile, bien que le format de ces traces de pile ne soit pas très configurable. La capture et la réexception d'exceptions dans ces langages sont totalement inutiles à moins que vous ne puissiez vraiment ajouter un contexte important (par exemple, la connexion d'une exception SQL de bas niveau avec le contexte d'une opération de logique métier).

Toute stratégie de gestion des erreurs implémentée par réflexion est presque nécessairement moins efficace que les fonctionnalités intégrées au langage. De plus, la journalisation omniprésente a des coûts inévitables de performances. Vous devez donc équilibrer le flux d'informations que vous obtenez par rapport aux autres exigences de ce logiciel. Cela dit, les solutions comme PostSharp qui sont construites sur une instrumentation au niveau du compilateur feront généralement beaucoup mieux que la réflexion au moment de l'exécution.

Personnellement, je pense que tout enregistrer n'est pas utile, car il comprend des tonnes d'informations non pertinentes. Je suis donc sceptique vis-à-vis des solutions automatisées. Étant donné un bon cadre de journalisation, il pourrait être suffisant d'avoir une directive de codage convenue qui discute du type d'informations que vous souhaitez enregistrer et de la façon dont ces informations doivent être formatées. Vous pouvez ensuite ajouter la journalisation là où cela est important.

La connexion à la logique métier est beaucoup plus importante que la connexion aux fonctions utilitaires. De plus, la collecte des traces de pile des rapports de plantage du monde réel (qui ne nécessite que la journalisation au niveau supérieur d'un processus) vous permet de localiser les zones du code où la journalisation aurait le plus de valeur.

4
amon

Quand je vois try/catch/log dans chaque méthode, cela soulève des inquiétudes quant au fait que les développeurs n'avaient aucune idée de ce qui pourrait ou ne pourrait pas se produire dans leur application, ont supposé le pire et ont tout enregistré de manière préventive partout en raison de tous les bogues auxquels ils s'attendaient.

C'est un symptôme que les tests unitaires et d'intégration sont insuffisants et que les développeurs sont habitués à parcourir beaucoup de code dans le débogueur et espèrent que beaucoup de journalisation leur permettront de déployer du code bogué dans un environnement de test et de trouver les problèmes en regardant les journaux.

Le code qui lève les exceptions peut être plus utile que le code redondant qui intercepte et enregistre les exceptions. Si vous lancez une exception avec un message significatif lorsqu'une méthode reçoit un argument inattendu (et l'enregistrez à la limite du service), il est beaucoup plus utile que de consigner immédiatement l'exception levée comme effet secondaire de l'argument non valide et d'avoir à deviner ce qui l'a provoqué .

Les nuls en sont un exemple. Si vous obtenez une valeur en tant qu'argument ou le résultat d'un appel de méthode et qu'elle ne doit pas être nulle, lancez l'exception alors. Ne vous contentez pas de consigner le NullReferenceException résultant jeté cinq lignes plus tard en raison de la valeur null. Quoi qu'il en soit, vous obtenez une exception, mais l'un vous dit quelque chose tandis que l'autre vous fait rechercher quelque chose.

Comme indiqué par d'autres, il est préférable de consigner les exceptions à la limite du service, ou chaque fois qu'une exception n'est pas renvoyée car elle est gérée avec élégance. La différence la plus importante est entre quelque chose et rien. Si vos exceptions sont enregistrées dans un seul endroit facile d'accès, vous trouverez les informations dont vous avez besoin quand vous en avez besoin.

4
Scott Hannen

Si vous devez enregistrer des informations de contexte qui ne figurent pas déjà dans l'exception, vous les encapsulez dans une nouvelle exception et fournissez l'exception d'origine sous la forme InnerException. De cette façon, vous avez toujours la trace de pile d'origine préservée. Donc:

public static bool DoOperation(int num1, int num2)
{
    try
    {
        /* do some work with num1 and num2 */
    }
    catch (Exception ex)
    {
        throw new Exception("error occured while number 1 = {num1} and number 2 = {num2}", ex);
    }
}

Le deuxième paramètre du constructeur Exception fournit une exception interne. Ensuite, vous pouvez enregistrer toutes les exceptions en un seul endroit, et vous obtenez toujours la trace complète de la pile et les informations contextuelles dans la même entrée de journal.

Vous voudrez peut-être utiliser une classe d'exception personnalisée, mais le point est le même.

try/catch/log/rethrow est un gâchis car cela conduira à des journaux confus - par exemple que se passe-t-il si une exception différente se produit dans un autre thread entre la journalisation des informations contextuelles et la journalisation de l'exception réelle dans le gestionnaire de niveau supérieur? try/catch/throw est bien si la nouvelle exception ajoute des informations à l'original.

2
JacquesB

L'exception elle-même devrait fournir toutes les informations nécessaires à une bonne journalisation, y compris le message, le code d'erreur, etc. Par conséquent, il ne devrait pas être nécessaire d'attraper une exception uniquement pour la relancer ou lever une exception différente.

Souvent, vous verrez le modèle de plusieurs exceptions interceptées et renvoyées comme une exception courante, comme intercepter une DatabaseConnectionException, une InvalidQueryException et une InvalidSQLParameterException et renvoyer une DatabaseException. Bien qu'à cela, je dirais que toutes ces exceptions spécifiques devraient dériver de DatabaseException en premier lieu, donc une nouvelle tentative n'est pas nécessaire.

Vous constaterez que la suppression des clauses try catch inutiles (même celles à des fins purement de journalisation) rendra le travail plus facile, pas plus difficile. Seuls les emplacements de votre programme qui gèrent l'exception doivent consigner l'exception et, en cas d'échec, un gestionnaire d'exceptions à l'échelle du programme pour une dernière tentative de consignation de l'exception avant de quitter le programme en douceur. L'exception doit avoir une trace de pile complète indiquant le point exact où l'exception a été levée, il n'est donc souvent pas nécessaire de fournir une journalisation "contextuelle".

Cela dit, l'AOP peut être une solution miracle pour vous, bien qu'elle entraîne généralement un léger ralentissement global. Je vous encourage à supprimer à la place les clauses try catch inutiles où rien de valeur n'est ajouté.

1
Neil