Prova / Catch / Log / Rethrow - Anti Pattern?


19

Vedo diversi post in cui l'importanza di gestire le eccezioni in una posizione centrale o al limite del processo è stata enfatizzata come una buona pratica piuttosto che sporcare ogni blocco di codice attorno a try / catch. Sono fermamente convinto che la maggior parte di noi ne comprenda l'importanza, tuttavia vedo che le persone continuano a finire con un pattern anti catch-log-rethrow principalmente perché per facilitare la risoluzione dei problemi durante qualsiasi eccezione, vogliono registrare informazioni più specifiche del contesto (esempio: parametri del metodo passato) e il modo è di avvolgere il metodo 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;
    }
}

Esiste un modo giusto per raggiungere questo obiettivo pur mantenendo le buone pratiche di gestione delle eccezioni? Ho sentito parlare di framework AOP come PostSharp per questo, ma vorrei sapere se ci sono svantaggi o costi di prestazioni importanti associati a questi framework AOP.

Grazie!


6
C'è un'enorme differenza tra il wrapping di ogni metodo in try / catch, la registrazione dell'eccezione e il chug del codice. E provare / catturare e riproporre l'eccezione con ulteriori informazioni. La prima è una pratica terribile. Il secondo è il modo perfetto per migliorare la tua esperienza di debug.
Euforico,

Sto dicendo provare / catturare ogni metodo e semplicemente accedere catturare blocco e ricominciare - va bene?
rahulaga_dev,

2
Come sottolineato da Amon. Se la tua lingua ha tracce di stack, accedere a ciascuna cattura è inutile. Ma racchiudere l'eccezione e aggiungere ulteriori informazioni è una buona pratica.
Euforico,

1
Vedi la risposta di @ Liath. Qualsiasi risposta che ho dato rispecchierebbe praticamente la sua: catturare le eccezioni il più presto possibile e se tutto quello che puoi fare in quella fase è registrare alcune informazioni utili, allora fallo e ripeti. A mio avviso, vederlo come un antipasto non ha senso.
David Arno,

1
Liath: aggiunto piccolo frammento di codice. Sto usando c #
rahulaga_dev il

Risposte:


19

Il problema non è il blocco catch locale, il problema è il log e ripeti . Gestisci l'eccezione o racchiudila in una nuova eccezione che aggiunge ulteriore contesto e la genera. Altrimenti ti imbatterai in diverse voci di registro duplicate per la stessa eccezione.

L'idea qui è di migliorare la capacità di eseguire il debug dell'applicazione.

Esempio n. 1: gestirlo

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

Se si gestisce l'eccezione, è possibile ridurre facilmente l'importanza della voce del registro delle eccezioni e non vi è motivo di percolare tale eccezione nella catena. È già stato affrontato.

La gestione di un'eccezione può includere l'informazione agli utenti di un problema, la registrazione dell'evento o semplicemente la sua ignorazione.

NOTA: se si ignora intenzionalmente un'eccezione, si consiglia di fornire un commento nella clausola catch vuota che indichi chiaramente il perché. Questo fa sapere ai futuri manutentori che non è stato un errore o una programmazione pigra. Esempio:

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)
}

Esempio n. 2: aggiungi ulteriore contesto e lancia

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

L'aggiunta di un contesto aggiuntivo (come il numero di riga e il nome del file nel codice di analisi) può aiutare a migliorare la capacità di eseguire il debug dei file di input, supponendo che il problema sia presente. Questo è un caso speciale, quindi reimballare un'eccezione in un "ApplicationException" solo per il rebranding non aiuta il debug. Assicurati di aggiungere ulteriori informazioni.

Esempio n. 3: non fare nulla con l'eccezione

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

In questo caso finale, consenti solo all'eccezione di uscire senza toccarla. Il gestore delle eccezioni a livello più esterno può gestire la registrazione. La finallyclausola viene utilizzata per assicurarsi che tutte le risorse necessarie per il metodo vengano ripulite, ma non è questo il punto in cui è stata registrata l'eccezione.


Mi è piaciuto " Il problema non è il blocco di cattura locale, il problema è il registro e riproporre " Penso che questo abbia perfettamente senso per garantire una registrazione più pulita. Ma alla fine significa anche essere a posto con try / catch distribuito su tutti i metodi, non è vero? Penso che ci debbano essere delle linee guida per garantire che questa pratica venga seguita con giudizio piuttosto che avere tutti i metodi per farlo.
rahulaga_dev

Ho fornito una linea guida nella mia risposta. Questo non risponde alle tue domande?
Berin Loritsch,

@rahulaga_dev Non credo che ci sia una linea guida / proiettile d'argento, quindi risolvi questo problema, poiché dipende molto dal contesto. Non ci può essere una linea guida generale che ti dice dove gestire un'eccezione o quando ridisegnarla. IMO, l'unica linea guida che vedo è quella di rinviare la registrazione / gestione all'ultimo momento possibile ed evitare di accedere a codice riutilizzabile, in modo da non creare dipendenze inutili. Gli utenti del tuo codice non sarebbero troppo divertiti se avessi registrato le cose (ad esempio gestite le eccezioni) senza dar loro l'opportunità di gestirle a modo loro. Solo i miei due centesimi :)
andreee

7

Non credo che le catture locali siano un anti-pattern, infatti se ricordo bene è effettivamente applicato in Java!

Qual è la chiave per me quando si implementa la gestione degli errori è la strategia generale. Potresti desiderare un filtro che intercetti tutte le eccezioni al limite del servizio, potresti volerle intercettare manualmente - entrambe vanno bene finché esiste una strategia generale, che rientrerà negli standard di codifica dei tuoi team.

Personalmente mi piace rilevare errori all'interno di una funzione quando posso eseguire una delle seguenti operazioni:

  • Aggiungi informazioni contestuali (come lo stato degli oggetti o cosa stava succedendo)
  • Gestire l'eccezione in modo sicuro (ad esempio un metodo TryX)
  • Il sistema attraversa un limite di servizio e chiama in una libreria o API esterna
  • Vuoi catturare e riproporre un diverso tipo di eccezione (forse con l'originale come eccezione interna)
  • L'eccezione è stata generata come parte di alcune funzionalità in background di basso valore

Se non è uno di questi casi, non aggiungo un tentativo / cattura locale. In tal caso, a seconda dello scenario, potrò gestire l'eccezione (ad esempio un metodo TryX che restituisce un falso) o riproporre in modo che l'eccezione verrà gestita dalla strategia globale.

Per esempio:

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;
  }
}

O un esempio di ripensamento:

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;
  }
}

Quindi si rileva l'errore nella parte superiore dello stack e si presenta all'utente un messaggio di facile utilizzo.

Indipendentemente dall'approccio adottato, vale sempre la pena creare unit test per questi scenari, in modo da poter assicurarsi che la funzionalità non cambi e interrompere il flusso del progetto in un secondo momento.

Non hai menzionato in quale lingua stai lavorando ma essendo uno sviluppatore .NET e hai visto questo troppe volte per non menzionarlo.

NON SCRIVERE:

catch(Exception ex)
{
  throw ex;
}

Uso:

catch(Exception ex)
{
  throw;
}

Il primo ripristina la traccia dello stack e rende il tuo livello più alto completamente inutile!

TLDR

Catturare localmente non è un anti-pattern, spesso può far parte di un design e può aiutare ad aggiungere ulteriore contesto all'errore.


3
Qual è il punto di registrazione nel fermo, quando lo stesso logger verrebbe utilizzato nel gestore delle eccezioni di livello superiore?
Euforico,

Potresti avere ulteriori informazioni (come le variabili locali) a cui non avrai accesso in cima allo stack. Aggiornerò l'esempio per illustrare.
Liath

2
In tal caso, genera una nuova eccezione con dati aggiuntivi ed eccezione interna.
Euforico,

2
@Euforico sì, l'ho visto fare anch'io, personalmente non sono un fan, poiché richiede che tu crei nuovi tipi di eccezione per quasi ogni singolo metodo / scenario che ritengo sia un sovraccarico. L'aggiunta della linea di registro qui (e presumibilmente un'altra in alto) aiuta a illustrare il flusso di codice durante la diagnosi dei problemi
Liath

4
Java non ti obbliga a gestire l'eccezione, ti costringe a esserne consapevole. puoi prenderlo e fare qualunque cosa o semplicemente dichiararlo come qualcosa che la funzione può lanciare e non fare nulla con esso nella funzione .... Piccolo pignolo su una risposta altrimenti abbastanza buona!
Newtopian,

4

Questo dipende molto dalla lingua. Ad esempio, C ++ non offre tracce di stack nei messaggi di errore dell'eccezione, quindi può essere utile tracciare l'eccezione attraverso frequenti catch-log-rethrow. Al contrario, Java e linguaggi simili offrono ottime tracce dello stack, sebbene il formato di queste tracce dello stack potrebbe non essere molto configurabile. Catturare e ridisegnare le eccezioni in questi linguaggi è assolutamente inutile a meno che non si possa davvero aggiungere un contesto importante (ad esempio, collegare un'eccezione SQL di basso livello con il contesto di un'operazione di logica aziendale).

Qualsiasi strategia di gestione degli errori che viene implementata attraverso la riflessione è quasi necessariamente meno efficiente della funzionalità integrata nel linguaggio. Inoltre, la registrazione pervasiva ha un sovraccarico di prestazioni inevitabile. Quindi è necessario bilanciare il flusso di informazioni ottenute con altri requisiti per questo software. Detto questo, soluzioni come PostSharp basate sulla strumentazione a livello di compilatore, in genere, faranno molto meglio della riflessione in fase di esecuzione.

Personalmente credo che la registrazione di tutto non sia utile, perché include un sacco di informazioni irrilevanti. Sono quindi scettico sulle soluzioni automatizzate. Dato un buon framework di registrazione, potrebbe essere sufficiente disporre di una linea guida di codifica concordata che discuti su quale tipo di informazioni si desidera registrare e come queste informazioni dovrebbero essere formattate. È quindi possibile aggiungere la registrazione dove è importante.

L'accesso alla logica aziendale è molto più importante dell'accesso alle funzioni di utilità. E la raccolta di tracce dello stack di rapporti sugli arresti anomali del mondo reale (che richiede solo la registrazione al livello più alto di un processo) consente di individuare aree del codice in cui la registrazione avrebbe il massimo valore.


4

Quando vedo try/catch/login ogni metodo, suscita preoccupazioni sul fatto che gli sviluppatori non avessero idea di cosa potesse o meno accadere nella loro applicazione, presumendo il peggio e registrando preventivamente tutto dappertutto a causa di tutti i bug che si aspettavano.

Questo è un sintomo che i test di unità e integrazione sono insufficienti e gli sviluppatori sono abituati a passare attraverso un sacco di codice nel debugger e sperano che in qualche modo un sacco di log consentirà loro di distribuire il codice buggy in un ambiente di test e trovare i problemi guardando i registri.

Il codice che genera eccezioni può essere più utile del codice ridondante che rileva e registra le eccezioni. Se si genera un'eccezione con un messaggio significativo quando un metodo riceve un argomento imprevisto (e lo registra al limite del servizio) è molto più utile che registrare immediatamente l'eccezione generata come effetto collaterale dell'argomento non valido e dover indovinare cosa l'ha causato .

I null sono un esempio. Se ottieni un valore come argomento o il risultato di una chiamata al metodo e non dovrebbe essere nullo, lancia l'eccezione. Non limitarti a registrare le risultanti NullReferenceExceptiongenerate dopo cinque righe a causa del valore null. Ad ogni modo ottieni un'eccezione, ma uno ti dice qualcosa mentre l'altro ti fa cercare qualcosa.

Come affermato da altri, è meglio registrare le eccezioni al limite del servizio o ogni volta che un'eccezione non viene riproposta perché gestita con grazia. La differenza più importante è tra qualcosa e niente. Se le tue eccezioni sono registrate in un posto che è facile da raggiungere, troverai le informazioni di cui hai bisogno quando ne hai bisogno.


Grazie Scott. Indica che hai fatto " Se lanci un'eccezione con un messaggio significativo quando un metodo riceve un argomento inaspettato (e lo registri al limite del servizio) " colpisce davvero e mi ha aiutato a visualizzare la situazione che mi circonda nel contesto degli argomenti del metodo. Penso che abbia senso avere una clausola di guardia sicura e gettare ArgumentException in quel caso piuttosto che fare affidamento sulla cattura e registrazione dei dettagli dell'argomento
rahulaga_dev

Scott, ho la stessa sensazione. Ogni volta che vedo log e rethow solo per registrare il contesto, sento che gli sviluppatori non possono controllare gli invarianti della classe o non possono salvaguardare la chiamata del metodo. Invece, ogni metodo fino in fondo è racchiuso in un simile tentativo / cattura / registro / lancio. Ed è semplicemente terribile.
Max

2

Se è necessario registrare informazioni di contesto che non sono già presenti nell'eccezione, inserirle in una nuova eccezione e fornire l'eccezione originale come InnerException. In questo modo hai ancora la traccia dello stack originale conservata. Così:

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);
    }
}

Il secondo parametro per il Exceptioncostruttore fornisce un'eccezione interna. Quindi è possibile registrare tutte le eccezioni in un'unica posizione e ottenere comunque la traccia dello stack completo e le informazioni contestuali nella stessa voce di registro.

È possibile che si desideri utilizzare una classe di eccezione personalizzata, ma il punto è lo stesso.

try / catch / log / rethrow è un disastro perché porterà a registri confusi - ad esempio cosa succede se si verifica un'eccezione diversa in un altro thread tra la registrazione delle informazioni contestuali e la registrazione dell'eccezione effettiva nel gestore di livello superiore? try / catch / throw va bene, tuttavia, se la nuova eccezione aggiunge informazioni all'originale.


Che dire del tipo di eccezione originale? Se lo avvolgiamo, non c'è più. È un problema? Qualcuno si basava su SqlTimeoutException per esempio.
Max

@Max: il tipo di eccezione originale è ancora disponibile come eccezione interna.
Jacques B,

Questo è ciò che intendo! Ora tutti nello stack di chiamate, che erano alla ricerca di SqlException, non lo capiranno mai.
Max

1

L'eccezione stessa dovrebbe fornire tutte le informazioni necessarie per una corretta registrazione, inclusi messaggio, codice di errore e cosa no. Pertanto, non dovrebbe essere necessario rilevare un'eccezione solo per riproporla o generare un'eccezione diversa.

Spesso vedrai il modello di diverse eccezioni catturate e riproposte come un'eccezione comune, come la cattura di DatabaseConnectionException, InvalidQueryException e InvalidSQLParameterException e il rilancio di DatabaseException. Però, direi che tutte queste eccezioni specifiche dovrebbero derivare da DatabaseException in primo luogo, quindi non è necessario ricodificare.

Scoprirai che la rimozione di clausole try catch inutili (anche solo per scopi di registrazione) renderà il lavoro più semplice, non più difficile. Solo le posizioni nel programma che gestiscono l'eccezione dovrebbero registrare l'eccezione e, in caso contrario, un gestore di eccezioni a livello di programma per un ultimo tentativo di registrazione dell'eccezione prima di uscire con grazia dal programma. L'eccezione dovrebbe avere una traccia dello stack completo che indica il punto esatto in cui è stata generata l'eccezione, quindi spesso non è necessario fornire la registrazione "contestuale".

Detto questo, AOP può essere una soluzione rapida per te, sebbene di solito comporti un leggero rallentamento generale. Vorrei incoraggiarvi invece a rimuovere completamente le clausole inutili try catch in cui non viene aggiunto nulla di valore.


1
" L'eccezione stessa dovrebbe fornire tutte le informazioni necessarie per una corretta registrazione, inclusi messaggio, codice di errore e cosa no ". Dovrebbero, ma in pratica non fanno riferimento a eccezioni come un caso classico. Non sono a conoscenza di alcun linguaggio che ti dirà la variabile che l'ha causata all'interno di un'espressione complessa, per esempio.
David Arno,

1
@DavidArno Vero, ma qualsiasi contesto che potresti fornire non potrebbe essere neanche così specifico. Altrimenti avresti try { tester.test(); } catch (NullPointerException e) { logger.error("Variable tester was null!"); }. La traccia dello stack è sufficiente nella maggior parte dei casi, ma in mancanza di questo, il tipo di errore è generalmente adeguato.
Neil,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.