Il lancio di nuove RuntimeExceptions in un codice non raggiungibile è un cattivo stile?


31

Mi è stato assegnato il compito di mantenere un'applicazione scritta qualche tempo fa da sviluppatori più qualificati. Mi sono imbattuto in questo pezzo di codice:

public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
        try {
            return translate(mailManagementService.retrieveUserMailConfiguration(id));
        } catch (Exception e) {
            rethrow(e);
        }
        throw new RuntimeException("cannot reach here");
    }

Sono curioso di sapere se il lancio RuntimeException("cannot reach here")è giustificato. Probabilmente mi manca qualcosa di ovvio sapendo che questo pezzo di codice proviene da un collega più esperto.
EDIT: Ecco il corpo di ripensamento a cui si riferivano alcune risposte. Ho ritenuto non importante in questa domanda.

private void rethrow(Exception e) throws MailException {
        if (e instanceof InvalidDataException) {
            InvalidDataException ex = (InvalidDataException) e;
            rethrow(ex);
        }
        if (e instanceof EntityAlreadyExistsException) {
            EntityAlreadyExistsException ex = (EntityAlreadyExistsException) e;
            rethrow(ex);
        }
        if (e instanceof EntityNotFoundException) {
            EntityNotFoundException ex = (EntityNotFoundException) e;
            rethrow(ex);
        }
        if (e instanceof NoPermissionException) {
            NoPermissionException ex = (NoPermissionException) e;
            rethrow(ex);
        }
        if (e instanceof ServiceUnavailableException) {
            ServiceUnavailableException ex = (ServiceUnavailableException) e;
            rethrow(ex);
        }
        LOG.error("internal error, original exception", e);
        throw new MailUnexpectedException();
    }


private void rethrow(ServiceUnavailableException e) throws
            MailServiceUnavailableException {
        throw new MailServiceUnavailableException();
    }

private void rethrow(NoPermissionException e) throws PersonNotAuthorizedException {
    throw new PersonNotAuthorizedException();
}

private void rethrow(InvalidDataException e) throws
        MailInvalidIdException, MailLoginNotAvailableException,
        MailInvalidLoginException, MailInvalidPasswordException,
        MailInvalidEmailException {
    switch (e.getDetail()) {
        case ID_INVALID:
            throw new MailInvalidIdException();
        case LOGIN_INVALID:
            throw new MailInvalidLoginException();
        case LOGIN_NOT_ALLOWED:
            throw new MailLoginNotAvailableException();
        case PASSWORD_INVALID:
            throw new MailInvalidPasswordException();
        case EMAIL_INVALID:
            throw new MailInvalidEmailException();
    }
}

private void rethrow(EntityAlreadyExistsException e)
        throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
    switch (e.getDetail()) {
        case LOGIN_ALREADY_TAKEN:
            throw new MailLoginNotAvailableException();
        case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
            throw new MailEmailAddressAlreadyForwardedToException();
    }
}

private void rethrow(EntityNotFoundException e) throws
        MailAccountNotCreatedException,
        MailAliasNotCreatedException {
    switch (e.getDetail()) {
        case ACCOUNT_NOT_FOUND:
            throw new MailAccountNotCreatedException();
        case ALIAS_NOT_FOUND:
            throw new MailAliasNotCreatedException();
    }
}

12
è tecnicamente raggiungibile, se rethrownon riesce effettivamente a fare throwun'eccezione. (che potrebbe accadere un giorno, se l'implementazione cambia)
njzk2,

34
A parte questo, un AssertionError potrebbe essere una scelta semanticamente migliore di RuntimeException.
JvR,

1
Il tiro finale è ridondante. Se si abilitano findbugs o altri strumenti di analisi statica, questi tipi di linee verranno contrassegnati per la rimozione.
Bon Ami,

7
Ora che vedo il resto del codice, sto pensando che forse dovresti cambiare "sviluppatori più qualificati" in " sviluppatori più senior ". Code Review sarebbe felice di spiegare il perché.
JvR,

3
Ho cercato di creare esempi ipotetici del motivo per cui le eccezioni sono terribili. Ma nulla di ciò che io abbia mai fatto regge il confronto.
Paul Draper,

Risposte:


36

Innanzitutto, grazie per aver ascoltato la tua domanda e averci mostrato cosa rethrowfa. Quindi, in effetti, ciò che fa è convertire le eccezioni con proprietà in classi di eccezioni più dettagliate. Più su questo più tardi.

Dal momento che non ho veramente risposto alla domanda principale in origine, eccolo qui: sì, è generalmente un cattivo stile generare eccezioni di runtime in codice non raggiungibile; faresti meglio a usare le asserzioni o, meglio ancora, a evitare il problema. Come già sottolineato, il compilatore qui non può essere sicuro che il codice non esca mai dal try/catchblocco. Puoi riformattare il tuo codice sfruttando il fatto che ...

Gli errori sono valori

(Non sorprende, è ben noto in go )

Facciamo un esempio più semplice, quello che ho usato prima della tua modifica: immagina di registrare qualcosa e creare un'eccezione wrapper come nella risposta di Konrad . Chiamiamolo logAndWrap.

Invece di lanciare l'eccezione come effetto collaterale di logAndWrap, potresti lasciarlo fare il suo lavoro come effetto collaterale e fargli restituire un'eccezione (almeno, quella data in input). Non è necessario utilizzare generici, solo funzioni di base:

private Exception logAndWrap(Exception exception) {
    // or whatever it actually does
    Log.e("Ouch! " + exception.getMessage());
    return new CustomWrapperException(exception);
}

Quindi, throwesplicitamente, e il tuo compilatore è felice:

try {
     return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
     throw logAndWrap(e);
}

E se ti dimentichi di throw?

Come spiegato nel commento di Joe23 , un modo di programmazione difensiva per garantire che l'eccezione sia sempre generata consisterebbe nel fare esplicitamente un throw new CustomWrapperException(exception)alla fine di logAndWrap, come è fatto da Guava . In questo modo, sai che verrà generata l'eccezione e il tuo analizzatore di tipi è felice. Tuttavia, le eccezioni personalizzate devono essere eccezioni non selezionate, il che non è sempre possibile. Inoltre, valuterei il rischio che uno sviluppatore manchi di scrivere throwper essere molto basso: lo sviluppatore deve dimenticarlo e il metodo circostante non dovrebbe restituire nulla, altrimenti il ​​compilatore rileverà un ritorno mancante. Questo è un modo interessante per combattere il sistema dei tipi e funziona, però.

rigenerare

L'attuale rethrowpuò anche essere scritto come una funzione, ma ho problemi con la sua attuale implementazione:

  • Ci sono molti cast inutili come I cast sono infatti richiesti (vedi commenti):

    if (e instanceof ServiceUnavailableException) {
        ServiceUnavailableException ex = (ServiceUnavailableException) e;
        rethrow(ex);
    }
  • Quando si generano / restituiscono nuove eccezioni, quella vecchia viene scartata; nel seguente codice, a MailLoginNotAvailableExceptionnon mi consente di sapere quale accesso non è disponibile, il che è scomodo; inoltre, stacktraces sarà incompleto:

    private void rethrow(EntityAlreadyExistsException e)
        throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
        switch (e.getDetail()) {
            case LOGIN_ALREADY_TAKEN:
                throw new MailLoginNotAvailableException();
            case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
                throw new MailEmailAddressAlreadyForwardedToException();
        }
    }
  • Perché il codice di origine non genera in primo luogo quelle eccezioni specializzate? Ho il sospetto che rethrowvenga utilizzato come livello di compatibilità tra un sottosistema (mailing) e la logica di business (forse l'intento è quello di nascondere i dettagli di implementazione come eccezioni generate sostituendole con eccezioni personalizzate). Anche se concordo sul fatto che sarebbe meglio avere un codice privo di catture, come suggerito nella risposta di Pete Becker , non credo che avrai l'opportunità di rimuovere il codice catche rethrowqui senza importanti refactoring.


1
I cast non sono inutili. Sono necessari perché non esiste un dispacciamento dinamico basato sul tipo di argomento. Il tipo di argomento deve essere noto al momento della compilazione per chiamare il metodo giusto.
Joe23,

Nel tuo logAndWrapmetodo potresti lanciare l'eccezione invece di restituirla, ma mantieni il tipo restituito (Runtime)Exception. In questo modo il blocco catch può rimanere nel modo in cui lo hai scritto (senza inserire un codice irraggiungibile). Ma ha l'ulteriore vantaggio di non poter dimenticare throw. Se si restituisce l'eccezione e si dimentica il lancio, ciò farà sì che l'eccezione venga ingerita silenziosamente. Lanciare con un tipo restituito RuntimeException renderà felice il compilatore evitando anche questi errori. Ecco come Throwables.propagateviene implementato Guava .
Joe23,

@ Joe23 Grazie per il chiarimento sulla spedizione statica. Informazioni sul lancio di eccezioni all'interno della funzione: vuoi dire che il possibile errore che stai descrivendo è un blocco catch che chiama logAndWrapma non fa nulla con l'eccezione restituita? Interessante. All'interno di una funzione che deve restituire un valore, il compilatore noterà che esiste un percorso senza valore restituito, ma questo è utile per i metodi che non restituiscono nulla. Grazie ancora.
coredump,

1
Questo è esattamente ciò che menziono. E solo per sottolineare ancora una volta il punto: non è qualcosa che mi è venuto in mente e di cui mi merito, ma che ho raccolto dalla fonte di Guava.
Joe23,

@ Joe23 Ho aggiunto un riferimento esplicito alla libreria
coredump

52

Questa rethrow(e);funzione viola il principio secondo il quale in circostanze normali una funzione restituirà, mentre in circostanze eccezionali una funzione genererà un'eccezione. Questa funzione viola questo principio generando un'eccezione in circostanze normali. Questa è la fonte di tutta la confusione.

Il compilatore presuppone che questa funzione tornerà in circostanze normali, quindi, per quanto può dire il compilatore, l'esecuzione può raggiungere la fine della retrieveUserMailConfigurationfunzione, a quel punto è un errore non avere returnun'istruzione. Il RuntimeExceptionlancio lì dovrebbe alleviare questa preoccupazione del compilatore, ma è un modo piuttosto goffo di farlo. Un altro modo per prevenire l' function must return a valueerrore è aggiungere una return null; //to keep the compiler happydichiarazione, ma secondo me è ugualmente goffo.

Quindi, personalmente, vorrei sostituire questo:

rethrow(e);

con questo:

report(e); 
throw e;

o, meglio ancora, (come suggerito coredump ), con questo:

throw reportAndTransform(e);

Pertanto, il flusso di controllo sarebbe reso evidente al compilatore, quindi il tuo finale throw new RuntimeException("cannot reach here");diventerebbe non solo ridondante, ma in realtà nemmeno compilabile, poiché sarebbe contrassegnato dal compilatore come codice irraggiungibile.

Questo è il modo più elegante e in realtà anche il più semplice per uscire da questa brutta situazione.


1
-1 Il suggerimento di dividere la funzione in due istruzioni può introdurre errori di programmazione dovuti a copia / incolla errate; Sono anche leggermente preoccupato per il fatto che tu abbia modificato la tua domanda per suggerire throw reportAndTransform(e)un paio di minuti dopo che ho pubblicato la mia risposta. Forse hai modificato la tua domanda indipendentemente da essa, e mi scuso in anticipo se è così, ma per il resto, dare un po 'di credito è qualcosa che la gente di solito fa.
coredump,

4
@coredump In effetti ho letto il tuo suggerimento e ho anche letto un'altra risposta che attribuisce questo suggerimento a te, e mi sono ricordato che in realtà ho usato proprio un tale costrutto in passato, quindi ho deciso di includerlo nella mia risposta. Ho pensato che non fosse così importante da includere un'attribuzione, perché qui non stiamo parlando di un'idea originale, ma se ne metti in discussione, non voglio aggravarti, quindi l'attribuzione è ora lì, completo di un link. Saluti.
Mike Nakis,

Non è che ho un brevetto per questo costrutto, che è abbastanza comune; ma immagina di scrivere una risposta e non molto tempo dopo, viene "assimilato" da un altro. Quindi l'attribuzione è molto apprezzata. Grazie.
coredump,

3
Non c'è nulla di sbagliato di per sé con una funzione che non ritorna. Succede sempre, ad esempio, nei sistemi in tempo reale. Il problema finale è un difetto di Java in quanto il linguaggio analizza e rafforza il concetto di correttezza delle lingue e tuttavia il linguaggio non fornisce un meccanismo per annotare in qualche modo che una funzione non ritorna. Tuttavia, ci sono modelli di progettazione che aggirano questo come throw reportAndTransform(e). Molti modelli di progettazione sono patch che mancano funzionalità linguistiche. Questo è uno di loro.
David Hammen,

2
D'altra parte, molte lingue moderne sono abbastanza complesse. Trasformare ogni modello di progettazione in una funzione del linguaggio principale li gonfia ulteriormente. Questo è un buon esempio di un modello di progettazione che non appartiene al linguaggio principale. Come scritto, è ben compreso non solo dal compilatore, ma anche da molti altri strumenti che devono analizzare il codice.
Salterio

7

Il throwfu probabilmente aggiunto per aggirare il "metodo deve restituire un valore" errore che altrimenti si verificherebbe - il flusso di dati di Java analizzatore è abbastanza intelligente per capire che non returnè necessario dopo una throw, ma non dopo la vostra abitudine rethrow()metodo, e non v'è alcuna @NoReturnannotazione che potresti usare per risolvere questo problema.

Tuttavia, la creazione di una nuova eccezione nel codice non raggiungibile sembra superflua. Scriverei semplicemente return null, sapendo che in realtà non succede mai.


Hai ragione. Ho controllato cosa sarebbe successo se avessi commentato la throw new...riga e si lamentasse della mancata dichiarazione di reso. È una cattiva programmazione? Esiste una convenzione sulla sostituzione di una dichiarazione di reso non raggiungibile? Lascerò questa domanda senza risposta per un po 'per incoraggiare ulteriori input. Tuttavia, grazie per la tua risposta.
Sok Pomaranczowy,

3
@SokPomaranczowy Sì, è una cattiva programmazione. Tale rethrow()metodo nasconde il fatto che catchsta ridisegnando l'eccezione. Eviterei di fare qualcosa del genere. Inoltre, ovviamente, rethrow()potrebbe non riuscire a riproporre l'eccezione, nel qual caso succede qualcos'altro.
Ross Patterson,

26
Si prega di non sostituire l'eccezione con return null. Con l'eccezione, se in futuro qualcuno romperà il codice in modo che l'ultima riga diventi raggiungibile, sarà immediatamente chiaro quando viene generata l'eccezione. Se invece return null, ora hai un nullvalore fluttuante nella tua applicazione che non era previsto. Chissà dove NullPointerExceptionnascerà l'applicazione ? A meno che tu non sia fortunato ed è mediatamente dopo la chiamata a questa funzione, sarà molto difficile rintracciare il vero problema.
unholysampler,

5
@MatthewRead L'esecuzione di codice inteso come irraggiungibile è un bug grave, return nullè molto probabile che maschera il bug perché non è possibile distinguere gli altri casi in cui ritorna nullda questo bug.
Codici

4
Inoltre, se la funzione restituisce già null in alcune circostanze e si utilizza anche un ritorno null in questa circostanza, gli utenti ora hanno due possibili scenari in cui potrebbero trovarsi quando ottengono un ritorno null. A volte questo non ha importanza, perché il valore null restituisce già "non c'è alcun oggetto e non sto specificando il perché", quindi aggiungere un altro motivo possibile per cui fa poca differenza. Ma spesso aggiungere ambiguità è un male, e certamente significa che inizialmente i bug saranno trattati come "determinate circostanze" invece di sembrare immediatamente come "questo è rotto". Fallire presto.
Steve Jessop,

6

Il

throw new RuntimeException("cannot reach here");

L'istruzione chiarisce a una PERSON che legge il codice cosa sta succedendo, quindi è molto meglio che restituire null, ad esempio. Inoltre, semplifica il debug se il codice viene modificato in modo imprevisto.

Tuttavia rethrow(e)sembra proprio sbagliato! Quindi nel tuo caso penso che il refactoring del codice sia un'opzione migliore. Vedi le altre risposte (mi piace il meglio di coredump) per i modi in cui ordinare il codice.


4

Non so se c'è una convenzione.

Ad ogni modo, un altro trucco sarebbe quello di fare così:

private <T> T rethrow(Exception exception) {
    // or whatever it actually does
    Log.e("Ouch! " + exception.getMessage());
    throw new CustomWrapperException(exception);
}

Tenendo conto di ciò:

try {
     return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
     return rethrow(e);
}

E non RuntimeExceptionè più necessario alcun artificiale . Anche se in rethrowrealtà non restituisce mai alcun valore, ora è abbastanza buono per il compilatore.

Restituisce un valore in teoria (firma del metodo), quindi è esente dal farlo in quanto genera un'eccezione.

Sì, potrebbe sembrare strano, ma poi di nuovo - lanciare un fantasma RuntimeExceptiono restituire valori nulli che non saranno mai visti in questo mondo - non è nemmeno esattamente una cosa di bellezza.

Cercando la leggibilità, potresti rinominare rethrowe avere qualcosa di simile:

} catch (Exception e) {
     return nothingJustRethrow(e);
}

2

Se si rimuove trycompletamente il blocco, non è necessario il rethrowo il throw. Questo codice fa esattamente la stessa cosa dell'originale:

public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
    return translate(mailManagementService.retrieveUserMailConfiguration(id));
}

Non lasciarti ingannare dal fatto che provenga da sviluppatori più esperti. Questo è il marciume del codice e succede sempre. Basta aggiustarlo.

EDIT: ho letto male rethrow(e)come semplicemente rilanciando l'eccezione e. Se quel rethrowmetodo fa effettivamente qualcosa di diverso dal ri-lanciare l'eccezione, allora liberarsene cambia la semantica di questo metodo.


Le persone continueranno a credere che le catture jolly che non gestiscono nulla siano in realtà gestori di eccezioni. La tua versione ignora tale presupposto imperfetto non fingendo di gestire ciò che non può. Il chiamante di recuperaUserMailConfiguration () si aspetta di recuperare qualcosa. Se quella funzione non può restituire qualcosa di ragionevole, dovrebbe fallire velocemente e ad alta voce. È come se le altre risposte non vedessero alcun problema con catch(Exception)un odore di codice odioso.
msw,

1
@msw - Codifica Cargo-cult.
Pete Becker,

@msw - d'altra parte, ti dà un posto per impostare un breakpoint.
Pete Becker,

1
È come se non vedessi problemi a scartare un'intera parte della logica. La tua versione chiaramente non fa la stessa cosa dell'originale, che tra l'altro, già fallisce velocemente e rumorosamente. Preferirei scrivere metodi privi di cattura come quello nella tua risposta, ma quando si lavora con codice esistente, non dovremmo rompere i contratti con altre parti funzionanti. È possibile che ciò che viene fatto rethrowsia inutile (e questo non è il punto qui), ma non puoi semplicemente sbarazzarti del codice che non ti piace e dire che è equivalente all'originale quando chiaramente non lo è. Ci sono effetti collaterali nella rethrowfunzione personalizzata .
coredump,

1
@ jpmc26 Il nocciolo della domanda riguarda l'eccezione "impossibile raggiungere qui", che potrebbe sorgere per altri motivi oltre al precedente blocco try / catch. Ma sono d'accordo sul fatto che il blocco abbia un odore di pesce, e se questa risposta fosse formulata in modo più accurato in origine, avrebbe ottenuto molti più voti (non ho votato in basso, tra l'altro). L'ultima modifica è buona.
coredump,
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.