Come progettare le eccezioni


11

Sto lottando con una domanda molto semplice:

Ora sto lavorando su un'applicazione server e ho bisogno di inventare una gerarchia per le eccezioni (alcune eccezioni esistono già, ma è necessario un framework generale). Come posso iniziare a farlo?

Sto pensando di seguire questa strategia:

1) Cosa non va?

  • Viene chiesto qualcosa, che non è consentito.
  • Viene richiesto qualcosa, è consentito, ma non funziona a causa di parametri errati.
  • Viene chiesto qualcosa, è consentito, ma non funziona a causa di errori interni.

2) Chi avvia la richiesta?

  • L'applicazione client
  • Un'altra applicazione server

3) Gestione dei messaggi: poiché abbiamo a che fare con un'applicazione server, si tratta solo di ricevere e inviare messaggi. Quindi cosa succede se l'invio di un messaggio va storto?

Pertanto, potremmo ottenere i seguenti tipi di eccezione:

  • ServerNotAllowedException
  • ClientNotAllowedException
  • ServerParameterException
  • ClientParameterException
  • InternalException (nel caso in cui il server non sappia da dove provenga la richiesta)
    • ServerInternalException
    • ClientInternalException
  • MessageHandlingException

Questo è un approccio molto generale per definire la gerarchia delle eccezioni, ma temo che mi manchino alcuni casi ovvi. Hai idee su quali aree non sto trattando, sei a conoscenza di eventuali svantaggi di questo metodo o esiste un approccio più generale a questo tipo di domanda (in quest'ultimo caso, dove posso trovarlo)?

Grazie in anticipo


5
Non hai menzionato ciò che vuoi ottenere con la tua gerarchia di classi di eccezioni (e questo non è affatto ovvio). Registrazione significativa? Consentire ai clienti di reagire ragionevolmente alle diverse eccezioni? O cosa?
Ralf Kleberhoff,

2
È probabilmente utile analizzare alcune storie e usare prima i casi e vedere cosa viene fuori. Ad esempio: il client richiede X , non è consentito. Il client richiede X , ma la richiesta non è valida. Lavora attraverso di loro pensando a chi dovrebbe gestire l'eccezione, cosa possono farci (prompt, riprovare, qualunque cosa) e quali informazioni hanno bisogno per farlo bene. Quindi , una volta che sai quali sono le tue eccezioni concrete e quali informazioni sono necessarie ai gestori per elaborarle, puoi trasformarle in una bella gerarchia.
Inutile


1
Non ho mai veramente capito il desiderio di voler usare così tanti diversi tipi di eccezione. In genere per la maggior parte dei catchblocchi che utilizzo, non ho molto più utilizzo per l'eccezione rispetto al messaggio di errore che contiene. In realtà non ho nulla di diverso che posso fare per un'eccezione legata alla mancata lettura di un file poiché non riesco ad allocare memoria durante il processo di lettura, quindi tendo a catturare std::exceptione segnalare il messaggio di errore che contiene, forse decorando prima con "Failed to open file: %s", ex.what()uno stack buffer prima di stamparlo.

3
Inoltre non posso anticipare tutti i tipi di eccezione che verranno lanciati in primo luogo. Potrei essere in grado di anticiparli ora, ma i colleghi potrebbero introdurne di nuovi in ​​futuro, ad es. Quindi è un po 'senza speranza per me conoscere, per ora e per sempre, tutti i diversi tipi di eccezioni che potrebbero essere lanciate nel processo di un operazione. Quindi prendo semplicemente super in generale con un singolo blocco di cattura. Ho visto esempi di persone che usano molti catchblocchi diversi in un singolo sito di recupero, ma spesso è solo per ignorare il messaggio all'interno dell'eccezione e stampare un messaggio più localizzato ...

Risposte:


5

Revisione generale

(un po 'di parte)

In genere non sceglierei una gerarchia dettagliata delle eccezioni.

La cosa più importante: un'eccezione dice al chiamante che il tuo metodo non è riuscito a completare il suo lavoro. E il tuo chiamante deve esserne informato, quindi non continua semplicemente. Funziona con qualsiasi eccezione, indipendentemente dalla classe di eccezione scelta.

Il secondo aspetto è la registrazione. Volete trovare voci di registro significative ogni volta che qualcosa va storto. Anche questo non ha bisogno di diverse classi di eccezioni, solo di messaggi di testo ben progettati (suppongo che non sia necessario un automat per leggere i log degli errori ...).

Il terzo aspetto è la reazione del tuo chiamante. Cosa può fare il chiamante quando riceve un'eccezione? Qui può avere senso avere classi di eccezione diverse, in modo che il chiamante possa decidere se riprovare la stessa chiamata, utilizzare una soluzione diversa (ad esempio, utilizzare una fonte di fallback) o rinunciare.

E forse vuoi usare le tue eccezioni come base per informare l'utente finale del problema. Ciò significa creare un messaggio intuitivo oltre al testo di amministrazione per il file di registro, ma non necessita di diverse classi di eccezioni (anche se forse ciò può facilitare la generazione del testo ...).

Un aspetto importante per la registrazione (e per i messaggi di errore dell'utente) è la possibilità di modificare l'eccezione con le informazioni di contesto catturandola in un livello, aggiungendo alcune informazioni di contesto, ad esempio i parametri del metodo, e rilanciandola.

La tua gerarchia

Chi lancia la richiesta? Non credo che avrai bisogno delle informazioni su chi ha avviato la richiesta. Non riesco nemmeno a immaginare come tu lo sappia nel profondo di uno stack di chiamate.

Gestione dei messaggi : non è un aspetto diverso, ma solo casi aggiuntivi per "Che cosa non va?".

In un commento, si parla di un flag " nessuna registrazione " durante la creazione di un'eccezione. Non credo che nel punto in cui si crea e si genera un'eccezione, è possibile prendere una decisione affidabile se registrare o meno tale eccezione.

L'unica situazione che posso immaginare è che un livello superiore utilizza l'API in un modo che a volte produrrà eccezioni, e questo livello sa che non deve disturbare nessun amministratore con l'eccezione, quindi ingoia silenziosamente l'eccezione. Ma questo è un odore di codice: un'eccezione prevista è una contraddizione in sé, un suggerimento per cambiare l'API. Ed è il livello superiore che dovrebbe decidere, non il codice che genera l'eccezione.


Nella mia esperienza, un codice di errore in combinazione con un testo intuitivo funziona molto bene. Gli amministratori possono utilizzare il codice di errore per trovare ulteriori informazioni.
Sjoerd,

1
Mi piace l'idea alla base di questa risposta in generale. Dalla mia esperienza, le eccezioni non dovrebbero mai trasformarsi in una bestia troppo complessa. Lo scopo principale di un'eccezione è consentire al codice chiamante di risolvere un problema specifico e ottenere le informazioni di debug / riprova pertinenti senza interferire con la risposta della funzione.
greggle138,

2

La cosa principale da tenere a mente quando si progetta un modello di risposta agli errori è assicurarsi che sia utile per i chiamanti. Questo vale sia per le eccezioni che per i codici di errore definiti, ma ci limiteremo a illustrare con eccezioni.

  • Se la tua lingua o il tuo framework forniscono già classi di eccezione generali, usale dove sono appropriate e dove devono essere ragionevolmente previste . Non definire le tue classi ArgumentNullExceptiono ArgumentOutOfRangeeccezioni. I chiamanti non si aspetteranno di prenderli.

  • Definire una MyClientServerAppExceptionclasse di base per comprendere gli errori che sono unici nel contesto dell'applicazione. Non lanciare mai un'istanza della classe base. Una risposta di errore ambigua è LA PEGGIORE COSA DI SEMPRE . Se c'è un "errore interno", spiega cos'è l'errore.

  • Per la maggior parte, la gerarchia sotto la classe base dovrebbe essere ampia, non profonda. Hai solo bisogno di approfondirlo in situazioni in cui è utile per il chiamante. Ad esempio, se ci sono 5 motivi per cui un messaggio potrebbe non riuscire da client a server, è possibile definire ServerMessageFaultun'eccezione, quindi definire una classe di eccezioni per ciascuno di quei 5 errori sottostanti. In questo modo, il chiamante può semplicemente catturare la superclasse se ne ha bisogno o se lo desidera. Cerca di limitare questo a casi specifici e ragionevoli.

  • Non tentare di definire tutte le classi di eccezione prima che vengano effettivamente utilizzate. Finirai per rifare la maggior parte. Quando si verifica un caso di errore durante la scrittura del codice, quindi decidere il modo migliore per descrivere quell'errore. Idealmente, dovrebbe esprimersi nel contesto di ciò che il chiamante sta cercando di fare.

  • In relazione al punto precedente, ricorda che solo perché usi le eccezioni per rispondere agli errori, ciò non significa che devi usare solo le eccezioni per gli stati di errore. Tieni presente che generare eccezioni è generalmente costoso e che il costo delle prestazioni può variare da una lingua all'altra. Con alcune lingue, il costo è maggiore a seconda della profondità dello stack di chiamate, quindi se un errore è profondo all'interno di uno stack di chiamate, verificare se non è possibile utilizzare tipi primitivi semplici (codici di errore interi o flag booleani) per inviare l'errore esegue il backup dello stack, in modo che possa essere lanciato più vicino all'invocazione del chiamante.

  • Se si sta includendo la registrazione come parte della risposta all'errore, per i chiamanti dovrebbe essere facile cadere per aggiungere informazioni di contesto a un oggetto eccezione. Inizia da dove vengono utilizzate le informazioni nel codice di registrazione. Decidi quante informazioni sono necessarie affinché il registro sia utile (senza essere un gigantesco muro di testo). Quindi lavorare all'indietro per assicurarsi che le classi di eccezione possano essere facilmente fornite con tali informazioni.

Infine, a meno che l'applicazione non sia in grado di gestire in modo assolutamente corretto un errore di memoria insufficiente, non tentare di gestirli o altre eccezioni di runtime catastrofiche. Lascia che sia il sistema operativo a gestirlo, perché in realtà è tutto ciò che puoi fare.


2
Tranne il penultimo penultimo (circa il costo delle eccezioni), la risposta è buona. Quel proiettile sul costo delle eccezioni è fuorviante, perché in tutti i modi comuni di implementare le eccezioni a basso livello, il costo di generare un'eccezione è completamente indipendente dalla profondità dello stack di chiamate. Metodi alternativi di segnalazione degli errori potrebbero essere migliori se si sa che l'errore verrà gestito dal chiamante immediato, ma non per ottenere alcune funzioni dallo stack di chiamate prima di generare un'eccezione.
Bart van Ingen Schenau,

@BartvanIngenSchenau: ho provato a non legarlo a una lingua specifica. In alcune lingue ( Java, ad esempio ), la profondità dello stack di chiamate influisce sul costo dell'istanza. Modificherò per riflettere che non è così tagliato e secco.
Mark Benningfield,

0

Bene, ti consiglio innanzitutto di creare una Exceptionclasse base per tutte le eccezioni verificate che potrebbero essere generate dalla tua applicazione. Se la tua applicazione è stata chiamata DuckType, crea una DuckTypeExceptionclasse base.

Ciò consente di rilevare qualsiasi eccezione della DuckTypeExceptionclasse base per la gestione. Da qui, le tue eccezioni dovrebbero ramificarsi con nomi descrittivi che possano evidenziare meglio il tipo di problema. Ad esempio, "DatabaseConnectionException".

Consentitemi di chiarire, dovrebbero essere verificate tutte le eccezioni per le situazioni che potrebbero accadere che probabilmente vorreste gestire con garbo nel vostro programma. In altre parole, non è possibile connettersi al database, quindi DatabaseConnectionExceptionviene lanciato un oggetto che è possibile catturare per attendere e riprovare dopo un periodo di tempo.

Si potrebbe non vedere un eccezione controllata per un problema molto inaspettato, come query SQL valida o un'eccezione di puntatore nullo, e io ti vorremmo suggerire lasciare che queste eccezioni trascendono la maggior parte delle clausole di cattura (o catturati e rilanciati, se necessario) fino ad arrivare al principale controller stesso, che può quindi essere catturato RuntimeExceptionesclusivamente a scopo di registrazione.

La mia preferenza personale è quella di non riproporre una non selezionata RuntimeExceptioncome un'altra eccezione, poiché per natura di un'eccezione non selezionata non ti aspetteresti e quindi riproporla sotto un'altra eccezione sta nascondendo informazioni. Tuttavia, se questa è la tua preferenza, puoi ancora prendere a RuntimeExceptione lanciare un DuckTypeInternalExceptionche diversamente DuckTypeExceptionda deriva RuntimeExceptione quindi non è selezionato.

Se preferisci, puoi sottocategorizzare le tue eccezioni per scopi organizzativi come DatabaseExceptionper qualsiasi cosa relativa al database, ma incoraggerei comunque tali sotto-eccezioni a derivare dalla tua eccezione di base DuckTypeExceptione ad essere astratte e quindi derivate con nomi descrittivi espliciti.

Come regola generale, le tue catture di prova dovrebbero essere sempre più generiche man mano che sali sul callstack dei chiamanti per gestire le eccezioni, e di nuovo, nel tuo controller principale, non gestiresti una DatabaseConnectionExceptionma piuttosto la semplice DuckTypeExceptionda cui derivano tutte le tue eccezioni verificate.


2
Si noti che la domanda è taggata "C ++".
Martin Ba,

0

Prova a semplificarlo.

La prima cosa che ti aiuterà a pensare in un'altra strategia è: catturate molte eccezioni è molto simile all'uso delle eccezioni controllate da Java (scusate, non sono uno sviluppatore C ++). Questo non va bene per molte ragioni, quindi cerco sempre di non usarle e la tua strategia di eccezione gerarchica me ne ricorda molto.

Quindi, ti consiglio un'altra strategia flessibile: utilizzare eccezioni non controllate ed errori di codice.

Per esempio, vedi questo codice Java:

public class SystemErrorCode implements ErrorCode {

    INVALID_NAME(101),
    ORDER_NOT_FOUND(102),
    PARAMETER_NOT_FOUND(103),
    VALUE_TOO_SHORT(104);

    private final int number;

    private ErrorCode(int number) {
        this.number = number;
    }

    @Override
    public int getNumber() {
        return number;
    }
}

E la tua unica eccezione:

public class SystemException extends RuntimeException {

    private ErrorCode errorCode;

    public SystemException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

}

Questa strategia l'ho trovata su questo link e puoi trovare l'implementazione Java qui , dove puoi vedere maggiori dettagli a riguardo, perché il codice sopra è semplificato.

Poiché è necessario separare le diverse eccezioni tra le applicazioni "client" e "altro server", è possibile avere più classi di codici di errore che implementano l'interfaccia ErrorCode.


2
Si noti che la domanda è taggata "C ++".
Sjoerd,

Ho modificato per adattare l'idea.
Dherik,

0

Le eccezioni sono goto illimitate e devono essere utilizzate con cura. La migliore strategia per loro è quella di limitarli. La funzione chiamante deve gestire tutte le eccezioni generate dalle funzioni che chiama o il programma muore. Solo la funzione chiamante ha il contesto corretto per gestire l'eccezione. Avere funzioni più in alto nella struttura delle chiamate gestirle sono goto illimitate.

Le eccezioni non sono errori. Si verificano quando le circostanze impediscono al programma di completare un ramo del codice e indicano un altro ramo che deve seguire.

Le eccezioni devono essere nel contesto della funzione chiamata. Ad esempio: una funzione che risolve equazioni quadratiche . Deve gestire due eccezioni: division_by_zero e square_root_of_negative_number. Ma queste eccezioni non hanno senso per le funzioni che chiamano questo risolutore di equazioni quadratiche. Accadono a causa del metodo utilizzato per risolvere le equazioni e semplicemente riproporle espone gli interni e rompe l'incapsulamento. Invece, division_by_zero dovrebbe essere riproposto come not_quadratic e square_root_of_negative_number e no_real_roots.

Non è necessaria una gerarchia di eccezioni. Un enumerato delle eccezioni (che le identifica) generato da una funzione è sufficiente perché la funzione chiamante deve gestirle. Consentire loro di elaborare l'albero delle chiamate è un goto fuori contesto (senza restrizioni).

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.