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 (esistono già alcune eccezioni, ma è necessaria una struttura generale). Come posso iniziare a farlo?

Sto pensando di seguire questa strategia:

1) Che cosa non va?

  • Viene richiesto qualcosa, che non è consentito.
  • Qualcosa viene chiesto, è permesso, ma non funziona, a causa di parametri sbagliati.
  • Qualcosa viene chiesto, è permesso, ma non funziona, a causa di errori interni.

2) Chi sta lanciando la richiesta?

  • L'applicazione client
  • Un'altra applicazione server

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

Pertanto, potremmo ottenere i seguenti tipi di eccezione:

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

Questo è un approccio molto generale per definire la gerarchia delle eccezioni, ma temo che mi possano mancare alcuni casi ovvi. Avete idee su quali aree non sto coprendo, siete a conoscenza di eventuali inconvenienti di questo metodo o c'è un approccio più generale a questo tipo di domande (in quest'ultimo caso, dove posso trovarlo)?

Grazie in anticipo

    
posta Dominique 26.01.2018 - 11:09
fonte

5 risposte

5

Note generali

(un po 'influenzato dalle opinioni)

In genere non vado per una gerarchia delle eccezioni dettagliata.

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

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

Terzo aspetto è la reazione del chiamante. Cosa può fare il chiamante quando riceve un'eccezione? Qui può avere senso avere diverse classi di eccezioni, quindi il chiamante può 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 user-friendly oltre al testo admin per il file di log, ma non ha bisogno di classi di eccezioni diverse (anche se forse questo può rendere la generazione di testi più facile ...).

Un aspetto importante per la registrazione (e per i messaggi di errore degli utenti) è la possibilità di modificare l'eccezione con le informazioni di contesto catturandola su qualche livello, aggiungendo alcune informazioni di contesto, ad es. parametri del metodo e rilanciarlo.

La tua gerarchia

Chi sta lanciando la richiesta? Non credo che ti serviranno le informazioni che stavano lanciando la richiesta. Non riesco nemmeno a immaginare come tu lo sappia nel profondo dello stack di chiamate.

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

In un commento, si parla di un flag " no logging " durante la creazione di un'eccezione. Non penso che nel luogo in cui crei e generi un'eccezione, puoi prendere una decisione affidabile se registrare o meno quell'eccezione.

L'unica situazione che posso immaginare è che alcuni livelli superiori utilizzano la tua API in un modo che a volte genera eccezioni e questo strato sa che non è necessario disturbare alcun 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 più alto che dovrebbe decidere, non il codice che genera l'eccezione.

    
risposta data 26.01.2018 - 12:25
fonte
2

La cosa principale da tenere presente quando si progetta un pattern di risposta agli errori è assicurarsi che sia utile ai chiamanti. Ciò vale sia che tu stia utilizzando eccezioni o codici di errore definiti, ma ci limiteremo a illustrare con le eccezioni.

  • Se la tua lingua o struttura fornisce già delle classi di eccezione generali, usale dove sono appropriate e dove dovrebbero essere ragionevolmente prevedibili . Non definire le tue classi di eccezione ArgumentNullException o ArgumentOutOfRange . I chiamanti non si aspettano di catturarli.

  • Definisci una classe base MyClientServerAppException per racchiudere gli errori che sono univoci nel contesto della tua applicazione. Mai lancia un'istanza della classe base. Una risposta ambigua all'errore è LA COSA SEMPRE MAI . Se c'è un "errore interno", spiega cosa è quell'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 al chiamante. Ad esempio, se ci sono 5 ragioni per cui un messaggio potrebbe non funzionare da client a server, è possibile definire un'eccezione ServerMessageFault , quindi definire una classe di eccezioni per ciascuno di questi 5 errori al di sotto. In questo modo, il chiamante può prendere la superclasse solo se ha bisogno o vuole farlo. Cerca di limitare questo a casi specifici e ragionevoli.

  • Non provare a definire tutte le classi di eccezioni prima che vengano effettivamente utilizzate. Finirai per rifarlo la maggior parte. Quando si verifica un errore durante la scrittura del codice, quindi decidere il modo migliore per descrivere tale errore. Idealmente, dovrebbe essere espresso 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 le eccezioni solo per gli stati di errore. Ricorda che il lancio di eccezioni è usualmente costoso e il costo delle prestazioni può variare da una lingua all'altra. Con alcune lingue, il costo è più elevato a seconda della profondità dello stack di chiamate, quindi se un errore è profondo all'interno di uno stack di chiamate, controlla se non è possibile utilizzare semplici tipi primitivi (codici di errore interi o flag booleani) l'errore esegue il backup dello stack, quindi può essere lanciato più vicino alla chiamata del chiamante.

  • Se includi la registrazione come parte della risposta all'errore, dovrebbe essere facile per i chiamanti accodare le informazioni di contesto a un oggetto di 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 lavora all'indietro per assicurarti che le classi di eccezioni possano essere facilmente fornite con tali informazioni.

Infine, a meno che la tua applicazione non possa assolutamente trattare con garbo con un errore di memoria esaurita, non provare a gestirli o altre eccezioni di runtime catastrofiche. Lascia che sia il sistema operativo a occuparsene, perché in realtà è tutto ciò che puoi fare.

    
risposta data 26.01.2018 - 16:57
fonte
0

Beh, ti consiglierei di creare innanzitutto una classe base Exception per tutte le eccezioni controllate che potrebbero essere lanciate dalla tua applicazione. Se la tua applicazione è stata chiamata DuckType , quindi crea una% base di classe% co_de.

Ciò ti consente di rilevare qualsiasi eccezione della tua classe base DuckTypeException per la gestione. Da qui, le tue eccezioni dovrebbero diramarsi con nomi descrittivi che possono meglio evidenziare il tipo di problema. Ad esempio, "DatabaseConnectionException".

Vorrei essere chiaro, queste dovrebbero essere tutte le eccezioni controllate per le situazioni che potrebbero accadere che probabilmente vorremmo gestire con garbo nel vostro programma. In altre parole, non è possibile connettersi al database, quindi viene generato un DuckTypeException che è possibile catturare per attendere e riprovare dopo un periodo di tempo.

non vedresti un'eccezione controllata per un problema molto inaspettato come una query SQL non valida o un'eccezione di puntatore nullo, e ti incoraggio a lasciare che queste eccezioni trascendano la maggior parte delle clausole catch (o catturate e ricontrollato se necessario) finché non si arriva al controller principale stesso, che può quindi catturare DatabaseConnectionException per scopi puramente di registrazione.

La mia preferenza personale è quella di non rilanciare un RuntimeException non controllato come un'altra eccezione, dato che dalla natura di un'eccezione non controllata, non ti aspetteresti e quindi rilanciarlo con un'altra eccezione è nascondere le informazioni. Tuttavia, se questa è la tua preferenza, puoi comunque prendere un RuntimeException e lanciare un RuntimeException che, diversamente da DuckTypeInternalException , deriva da DuckTypeException ed è quindi deselezionato.

Se preferisci, puoi sottocategorizzare le tue eccezioni per scopi organizzativi come RuntimeException per tutto ciò che riguarda i database, ma incoraggerei comunque tali eccezioni secondarie a derivare dalla tua eccezione di base DatabaseException e ad essere astratto e quindi derivato con nomi descrittivi espliciti.

Come regola generale, i tentativi di cattura dovrebbero essere sempre più generici quando si sposta lo stack di chiamata dei chiamanti per la gestione delle eccezioni, e ancora, nel controller principale, non si gestirà un DuckTypeException ma piuttosto il DatabaseConnectionException semplice di cui derivano tutte le eccezioni controllate.

    
risposta data 26.01.2018 - 12:13
fonte
0

Prova a semplificarlo.

La prima cosa che ti aiuterà a pensare in un'altra strategia è: catturare molte eccezioni è molto simile all'utilizzo delle eccezioni checked da Java (mi dispiace, non sono uno sviluppatore C ++). Questo non va bene per molte ragioni quindi cerco sempre di non usarli e la tua eccezione gerarchica la strategia mi ricorda molto di questo.

Pertanto, ti consiglio un'altra strategia flessibile: utilizza eccezioni non controllate e errori di codice.

Ad 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 eccezione unica :

public class SystemException extends RuntimeException {

    private ErrorCode errorCode;

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

}

Questa strategia ho trovato su questo link e puoi trovare l'implementazione Java qui , dove puoi vedere ulteriori 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 codice di errore che implementano l'interfaccia ErrorCode.

    
risposta data 26.01.2018 - 11:30
fonte
0

Le eccezioni sono gotos non limitate e devono essere utilizzate con cura. La migliore strategia per loro è limitarli. La funzione di chiamata deve gestire tutte le eccezioni generate dalle funzioni che chiama o il programma muore. Solo la funzione di chiamata ha il contesto corretto per gestire l'eccezione. Avendo le funzioni più in alto nella struttura delle chiamate, gestirli sono gotos 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 da 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. Si verificano a causa del metodo utilizzato per risolvere le equazioni e semplicemente rilanciandoli espongono gli interni e rompono l'incapsulamento. Invece, division_by_zero dovrebbe essere ricacciato come not_quadratic e square_root_of_negative_number e no_real_roots.

Non c'è bisogno di una gerarchia di eccezioni. Un enum delle eccezioni (che li identifica) generate da una funzione è sufficiente perché la funzione chiamante deve gestirle. Permettendo loro di elaborare l'albero delle chiamate è un goto fuori contesto (senza restrizioni)

    
risposta data 31.01.2018 - 14:57
fonte

Leggi altre domande sui tag