Eccezioni: perché lanciare in anticipo? Perché prendere tardi?


156

Esistono molte best practice ben note sulla gestione delle eccezioni in isolamento. Conosco abbastanza bene le cose da fare e da non fare, ma le cose si complicano quando si tratta di buone pratiche o schemi in ambienti più grandi. "Lancia presto, prendi tardi" - Ho sentito molte volte e mi confonde ancora.

Perché dovrei lanciare in anticipo e prendere in ritardo, se a un livello di livello basso viene generata un'eccezione puntatore null? Perché dovrei prenderlo ad un livello superiore? Non ha senso per me cogliere un'eccezione di basso livello a un livello superiore, come un livello aziendale. Sembra violare le preoccupazioni di ogni livello.

Immagina la seguente situazione:

Ho un servizio che calcola una cifra. Per calcolare la cifra il servizio accede a un repository per ottenere dati grezzi e alcuni altri servizi per preparare il calcolo. Se qualcosa è andato storto nel livello di recupero dei dati, perché dovrei lanciare DataRetrievalException a un livello superiore? Al contrario, preferirei racchiudere l'eccezione in un'eccezione significativa, ad esempio CalculationServiceException.

Perché lanciare in anticipo, perché prendere in ritardo?


104
L'idea alla base di "catturare in ritardo" è, piuttosto che catturare il più tardi possibile, catturare il più presto possibile. Se si dispone di un parser di file, ad esempio, non ha senso gestire un file non trovato. Cosa intendi fare con questo, qual è il tuo percorso di recupero? Non ce n'è uno, quindi non prenderlo. Vai al tuo lexer, cosa fai lì, come ti riprendi da questo in modo che il tuo programma possa continuare? Non può, lascia passare l'eccezione. Come può lo scanner gestirlo? Non può, lascialo passare. Come può gestire il codice chiamante? Può provare un altro percorso file o avvisare l'utente, quindi intercetta.
Phoshi

16
Ci sono pochissimi casi in cui una NullPointerException (suppongo sia ciò che significa NPE) dovrebbe mai essere catturata; se possibile, dovrebbe essere evitato in primo luogo. Se hai NullPointerExceptions, allora hai del codice rotto che deve essere corretto. Molto probabilmente è anche una soluzione semplice.
Phil

6
Per favore, ragazzi, prima di suggerire di chiudere questa domanda come duplicata, controllate che la risposta all'altra domanda non risponda molto bene a questa domanda.
Doc Brown

1
(Citebot) today.java.net/article/2003/11/20/… Se questa non è l'origine della citazione, fornisci un riferimento alla fonte che ritieni sia la citazione originale più probabile.
rwong,

1
Solo un promemoria per chiunque raggiunga questa domanda e faccia sviluppo Android. Su Android, le eccezioni devono essere rilevate e gestite localmente, nella stessa funzione in cui vengono rilevate per la prima volta. Questo perché le eccezioni non si propagano tra i gestori di messaggi: la tua applicazione verrà uccisa in caso affermativo. Quindi, non dovresti citare questo consiglio durante lo sviluppo di Android.
rwong

Risposte:


118

Nella mia esperienza, è meglio gettare eccezioni nel punto in cui si verificano gli errori. Lo fai perché è il punto in cui sai di più sul motivo per cui l'eccezione è stata attivata.

Man mano che l'eccezione srotola il backup dei livelli, la cattura e il rilancio sono un buon modo per aggiungere ulteriore contesto all'eccezione. Questo può significare lanciare un diverso tipo di eccezione, ma includere l'eccezione originale quando si esegue questa operazione.

Alla fine l'eccezione raggiungerà un livello in cui si è in grado di prendere decisioni sul flusso di codice (ad esempio, chiedere all'utente di agire). Questo è il punto in cui dovresti finalmente gestire l'eccezione e continuare la normale esecuzione.

Con la pratica e l'esperienza con la tua base di codice diventa abbastanza facile giudicare quando aggiungere ulteriore contesto agli errori e dove è più sensato effettivamente gestire finalmente gli errori.

Cattura → Rethrow

Fai questo dove puoi utilmente aggiungere più informazioni che salverebbero uno sviluppatore che deve lavorare attraverso tutti i livelli per capire il problema.

Cattura → Maniglia

Fallo dove puoi prendere le decisioni finali su ciò che è appropriato, ma il flusso di esecuzione diverso attraverso il software.

Cattura → Errore Ritorno

Mentre ci sono situazioni in cui ciò è appropriato, prendere eccezioni e restituire un valore di errore al chiamante dovrebbe essere considerato per il refactoring in un'implementazione Catch → Rethrow.


Sì, lo so già ed è fuori discussione, che dovrei gettare eccezioni nel punto in cui ha origine l'errore. Ma perché non dovrei catturare l'NPE e invece lasciarlo salire su Stacktrace? Vorrei sempre catturare l'NPE e inserirlo in un'eccezione significativa. Inoltre, non vedo alcun vantaggio perché dovrei inviare un'eccezione DAO al livello di servizio o dell'interfaccia utente. Lo prenderei sempre a livello di servizio e lo includerei in un'eccezione di servizio con ulteriori informazioni dettagliate, perché la chiamata al servizio non è riuscita.
shylynx,

8
@shylynx La cattura di un'eccezione e quindi la riproposizione di un'eccezione più significativa è una buona cosa da fare. Quello che non dovresti fare è prendere un'eccezione troppo presto e quindi non riproporla. L'errore di cui il proverbio mette in guardia sta rilevando l'eccezione troppo presto e quindi tentando di gestirla nel livello sbagliato nel codice.
Simon B

Rendere evidente il contesto al punto di ricevere l'eccezione semplifica la vita agli sviluppatori del tuo team. Un NPE richiede ulteriori indagini per comprendere il problema
Michael Shaw,

4
@shylynx Uno potrebbe porre la domanda, "Perché hai un punto nel tuo codice che può lanciare un NullPointerException? Perché non controllare nulle lanciare un'eccezione (forse una IllegalArgumentException) prima in modo che il chiamante sappia esattamente dove nullè passato il male ?" Credo che sarebbe ciò che suggerirebbe la parte "dire presto" del detto.
jpmc26

2
@jpmc Ho preso l'NPE solo come esempio per enfatizzare le preoccupazioni intorno ai livelli e alle eccezioni. Potrei sostituirlo con un IllegalArgumentException.
shylynx,

56

Vuoi lanciare un'eccezione il prima possibile perché ciò rende più facile trovare la causa. Ad esempio, considera un metodo che potrebbe fallire con determinati argomenti. Se convalidi gli argomenti e fallisci all'inizio del metodo, sai immediatamente che l'errore si trova nel codice chiamante. Se aspetti fino a quando gli argomenti non sono necessari prima di fallire, devi seguire l'esecuzione e capire se il bug si trova nel codice chiamante (argomento errato) o se il metodo ha un bug. Prima si lancia l'eccezione, più si avvicina alla sua causa sottostante e più facile è capire dove le cose sono andate male.

Il motivo per cui le eccezioni vengono gestite a livelli più alti è perché i livelli inferiori non sanno quale sia il corso d'azione appropriato per gestire l'errore. In effetti, potrebbero esserci più modi appropriati per gestire lo stesso errore a seconda di quale sia il codice chiamante. Prendi ad esempio l'apertura di un file. Se stai tentando di aprire un file di configurazione e non è presente, ignorare l'eccezione e procedere con la configurazione predefinita può essere una risposta appropriata. Se stai aprendo un file privato che è vitale per l'esecuzione del programma e che in qualche modo manca, la tua unica opzione è probabilmente quella di chiudere il programma.

Avvolgere le eccezioni nei tipi giusti è una preoccupazione puramente ortogonale.


1
+1 per spiegare chiaramente perché i diversi livelli contano. Ottimo esempio dell'errore del file system.
Juan Carlos Coto,

24

Altri hanno riassunto abbastanza bene perché lanciare in anticipo . Consentitemi di concentrarmi sul perché prendere la parte tardiva invece, per la quale non ho visto una spiegazione soddisfacente per i miei gusti.

PERCHÉ LE ECCEZIONI?

Sembra esserci una certa confusione sul perché le eccezioni esistano in primo luogo. Vorrei condividere il grande segreto qui: il motivo delle eccezioni e la gestione delle eccezioni è ... ASTRAZIONE .

Hai visto il codice in questo modo:

static int divide(int dividend, int divisor) throws DivideByZeroException {
    if (divisor == 0)
        throw new DivideByZeroException(); // that's a checked exception indeed

    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    try {
        int res = divide(a, b);
        System.out.println(res);
    } catch (DivideByZeroException e) {
        // checked exception... I'm forced to handle it!
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

Non è così che dovrebbero essere usate le eccezioni. Codice come quello sopra esiste nella vita reale, ma sono più un'aberrazione e sono davvero l'eccezione (gioco di parole). La definizione di divisione, ad esempio, anche nella matematica pura, è condizionata: è sempre il "codice chiamante" che deve gestire il caso eccezionale di zero per limitare il dominio di input. È brutto. Fa sempre male al chiamante. Tuttavia, per tali situazioni il modello check-then-do è il modo naturale di procedere:

static int divide(int dividend, int divisor) {
    // throws unchecked ArithmeticException for 0 divisor
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt();
    if (b != 0) {
        int res = divide(a, b);
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

In alternativa, puoi eseguire il comando completo sullo stile OOP in questo modo:

static class Division {
    final int dividend;
    final int divisor;

    private Division(int dividend, int divisor) {
        this.dividend = dividend;
        this.divisor = divisor;
    }

    public boolean check() {
        return divisor != 0;
    }

    public int eval() {
        return dividend / divisor;
    }

    public static Division with(int dividend, int divisor) {
        return new Division(dividend, divisor);
    }
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Division d = Division.with(a, b);
    if (d.check()) {
        int res = d.eval();
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

Come vedi, il codice chiamante ha l'onere del controllo preliminare, ma non fa alcuna gestione delle eccezioni dopo. Se ArithmeticExceptionmai viene dalla chiamata divideo eval, allora sei TU che devi fare la gestione delle eccezioni e correggere il tuo codice, perché hai dimenticato il check(). Per le ragioni simili catturare un NullPointerExceptionè quasi sempre la cosa sbagliata da fare.

Ora ci sono alcune persone che affermano di voler vedere casi eccezionali nella firma del metodo / funzione, ovvero di estendere esplicitamente il dominio di output . Sono quelli che preferiscono le eccezioni verificate . Naturalmente, la modifica del dominio di output dovrebbe forzare l'adattamento di qualsiasi codice del chiamante diretto, e ciò sarebbe effettivamente ottenuto con eccezioni verificate. Ma non hai bisogno di eccezioni per questo! Ecco perché hai Nullable<T> classi generiche , classi di casi , tipi di dati algebrici e tipi di unione . Alcune persone OO potrebbero persino preferire tornare null per semplici casi di errore come questo:

static Integer divide(int dividend, int divisor) {
    if (divisor == 0) return null;
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Integer res = divide(a, b);
    if (res != null) {
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

Tecnicamente le eccezioni possono essere utilizzate per lo scopo di cui sopra, ma qui è il punto: non esistono eccezioni per tale uso . Le eccezioni sono l'astrazione pro. Le eccezioni riguardano il riferimento indiretto. Le eccezioni consentono di estendere il dominio "outcome" senza interrompere i contratti diretti con i clienti e rinviare la gestione degli errori a "altrove". Se il tuo codice genera eccezioni che vengono gestite nei chiamanti diretti dello stesso codice, senza alcun livello di astrazione nel mezzo, lo stai facendo SBAGLIATO

COME CATTURARE TARDI?

Allora eccoci qua. Ho discusso a fondo per dimostrare che l'utilizzo delle eccezioni negli scenari precedenti non è il modo in cui le eccezioni devono essere utilizzate. Esiste tuttavia un caso d'uso reale, in cui l'astrazione e l'indirizzamento offerto dalla gestione delle eccezioni è indispensabile. Comprendere tale utilizzo aiuterà anche a capire la raccomandazione del ritardo di cattura .

Quel caso d'uso è: Programmazione contro astrazioni di risorse ...

Sì, la logica aziendale dovrebbe essere programmata contro astrazioni , non implementazioni concrete. Il codice di "cablaggio" IOC di livello superiore creerà un'istanza delle implementazioni concrete delle astrazioni delle risorse e le trasmetterà alla logica aziendale. Niente di nuovo qui. Ma le implementazioni concrete di quelle astrazioni di risorse possono potenzialmente generare eccezioni specifiche per la propria implementazione , no?

Quindi chi può gestire tali eccezioni specifiche di implementazione? È quindi possibile gestire eccezioni specifiche delle risorse nella logica aziendale? No, non lo è. La logica aziendale è programmata contro le astrazioni, il che esclude la conoscenza di tali dettagli di eccezione specifici dell'implementazione.

"Ah!", Potresti dire: "ma è per questo che possiamo sottoclassare le eccezioni e creare gerarchie di eccezioni" (controlla Mr. Spring !). Lascia che te lo dica, è un errore. In primo luogo, ogni libro ragionevole su OOP afferma che l'eredità concreta è negativa, ma in qualche modo questa componente fondamentale di JVM, la gestione delle eccezioni, è strettamente legata all'eredità concreta. Ironia della sorte, Joshua Bloch non avrebbe potuto scrivere il suo libro Effective Java prima di poter avere l'esperienza con una JVM funzionante, vero? È più un libro "lezioni apprese" per la prossima generazione. In secondo luogo, e ancora più importante, se si rileva un'eccezione di alto livello, come si gestirà?PatientNeedsImmediateAttentionException: dobbiamo darle una pillola o amputarle le gambe !? Che ne dici di un'istruzione switch su tutte le possibili sottoclassi? Ecco il tuo polimorfismo, ecco l'astrazione. Hai capito.

Quindi chi può gestire le eccezioni specifiche delle risorse? Deve essere colui che conosce le concrezioni! Colui che ha istanziato la risorsa! Il codice "cablaggio" ovviamente! Controllalo:

Logica aziendale codificata contro le astrazioni ... NESSUNA GESTIONE DEGLI ERRORI DELLE RISORSE CONCRETE!

static interface InputResource {
    String fetchData();
}

static interface OutputResource {
    void writeData(String data);
}

static void doMyBusiness(InputResource in, OutputResource out, int times) {
    for (int i = 0; i < times; i++) {
        System.out.println("fetching data");
        String data = in.fetchData();
        System.out.println("outputting data");
        out.writeData(data);
    }
}

Nel frattempo da qualche altra parte le implementazioni concrete ...

static class ConstantInputResource implements InputResource {
    @Override
    public String fetchData() {
        return "Hello World!";
    }
}

static class FailingInputResourceException extends RuntimeException {
    public FailingInputResourceException(String message) {
        super(message);
    }
}

static class FailingInputResource implements InputResource {
    @Override
    public String fetchData() {
        throw new FailingInputResourceException("I am a complete failure!");
    }
}

static class StandardOutputResource implements OutputResource {
    @Override
    public void writeData(String data) {
        System.out.println("DATA: " + data);
    }
}

E infine il codice di cablaggio ... Chi gestisce le eccezioni concrete delle risorse? Quello che li conosce!

static void start() {
    InputResource in1 = new FailingInputResource();
    InputResource in2 = new ConstantInputResource();
    OutputResource out = new StandardOutputResource();

    try {
        ReusableBusinessLogicClass.doMyBusiness(in1, out, 3);
    }
    catch (FailingInputResourceException e)
    {
        System.out.println(e.getMessage());
        System.out.println("retrying...");
        ReusableBusinessLogicClass.doMyBusiness(in2, out, 3);
    }
}

Ora abbi pazienza con me. Il codice sopra è semplicistico. Si potrebbe dire che si dispone di un'applicazione / contenitore Web aziendale con più ambiti di risorse gestite dal contenitore IOC e sono necessari tentativi automatici e reinizializzazione delle risorse dell'ambito della sessione o della richiesta, ecc. La logica di cablaggio sugli ambiti di livello inferiore potrebbe ricevere fabbriche astratte per creare risorse, quindi non essere a conoscenza delle esatte implementazioni. Solo gli ambiti di livello superiore saprebbero davvero quali eccezioni possono generare quelle risorse di livello inferiore. Adesso aspetta!

Sfortunatamente, le eccezioni consentono solo l'indirizzamento indiretto sullo stack di chiamate e diversi ambiti con le loro diverse cardinalità di solito vengono eseguiti su più thread diversi. Non c'è modo di comunicare attraverso questo con eccezioni. Abbiamo bisogno di qualcosa di più potente qui. Risposta: passaggio del messaggio asincrono . Cattura ogni eccezione alla radice dell'ambito di livello inferiore. Non ignorare nulla, non lasciar passare nulla. Ciò chiuderà e eliminerà tutte le risorse create nello stack di chiamate dell'ambito corrente. Quindi propagare i messaggi di errore agli ambiti più in alto utilizzando le code / i canali dei messaggi nella routine di gestione delle eccezioni, fino a raggiungere il livello in cui sono note le concrezioni. Questo è il ragazzo che sa come gestirlo.

SUMMA SUMMARUM

Quindi, secondo la mia interpretazione, catturare in ritardo significa catturare le eccezioni nel posto più conveniente DOVE NON STAI RIPENDENDO ANCORA PIÙ . Non prendere troppo presto! Cattura le eccezioni nel livello in cui crei l'eccezione concreta generando istanze delle astrazioni delle risorse, il livello che conosce le concrezioni delle astrazioni. Il livello "cablaggio".

HTH. Buona programmazione!


Hai ragione sul fatto che il codice che fornisce l'interfaccia saprà di più su cosa può andare storto rispetto al codice che utilizza l'interfaccia, ma supponi che un metodo utilizzi due risorse dello stesso tipo di interfaccia e che gli errori debbano essere gestiti in modo diverso? O se una di quelle risorse internamente, come dettaglio di implementazione non noto al suo creatore, utilizza altre risorse nidificate dello stesso tipo? Avere il livello aziendale generato WrappedFirstResourceExceptiono WrappedSecondResourceExceptionrichiedere il livello "cablaggio" per guardare all'interno di
quell'eccezione

... può essere icky, ma sembrerebbe meglio che supporre che qualsiasi FailingInputResourceeccezione sarà il risultato di un'operazione con in1. In realtà, penso che in molti casi l'approccio giusto sarebbe quello di fare in modo che il livello di cablaggio passi un oggetto di gestione delle eccezioni e che il livello aziendale includa un catchche quindi invoca il handleExceptionmetodo di quell'oggetto . Tale metodo potrebbe riprogrammare o fornire dati predefiniti o inserire un prompt "Interrompi / Riprova / Fallisci" e consentire all'operatore di decidere cosa fare, ecc. A seconda di ciò che l'applicazione ha richiesto.
supercat

@supercat Capisco quello che stai dicendo. Direi che un'implementazione concreta delle risorse è responsabile della conoscenza delle eccezioni che può generare. Non deve specificare tutto (esiste il cosiddetto comportamento indefinito ), ma deve garantire l'assenza di ambiguità. Inoltre, le eccezioni di runtime non selezionate devono essere documentate. Se contraddice la documentazione, allora è un bug. Se ci si aspetta che il codice chiamante faccia qualcosa di ragionevole riguardo a un'eccezione, il minimo indispensabile è che la risorsa li avvolge in alcuni UnrecoverableInternalException, simile a un codice di errore HTTP 500.
Daniel Dinnyes,

@supercat Informazioni sul tuo suggerimento sui gestori di errori configurabili: esattamente! Nel mio ultimo esempio, la logica di gestione degli errori è codificata, chiamando il doMyBusinessmetodo statico . Questo per brevità, ed è perfettamente possibile renderlo più dinamico. Tale Handlerclasse verrebbe istanziata con alcune risorse di input / output e avrebbe un handlemetodo che riceve una classe che implementa a ReusableBusinessLogicInterface. È quindi possibile combinare / configurare per utilizzare diverse implementazioni di gestori, risorse e business logic nello strato di cablaggio da qualche parte sopra di essi.
Daniel Dinnyes,

10

Per rispondere correttamente a questa domanda, facciamo un passo indietro e facciamo una domanda ancora più fondamentale.

Perché abbiamo eccezioni in primo luogo?

Facciamo eccezioni per far sapere al chiamante del nostro metodo che non potremmo fare quello che ci è stato chiesto di fare. Il tipo di eccezione spiega perché non siamo riusciti a fare ciò che volevamo fare.

Diamo un'occhiata ad alcuni codici:

double MethodA()
{
    return PropertyA - PropertyB.NestedProperty;
}

Questo codice può ovviamente generare un'eccezione di riferimento null se PropertyBè null. Ci sono due cose che potremmo fare in questo caso per "correggere" questa situazione. Potremmo:

  • Crea automaticamente PropertyB se non ce l'abbiamo; o
  • Lascia che l'eccezione raggiunga il metodo chiamante.

La creazione di PropertyB qui potrebbe essere molto pericolosa. Quale motivo ha questo metodo per creare PropertyB? Sicuramente ciò violerebbe il principio della responsabilità unica. Con ogni probabilità, se PropertyB non esiste qui, indica che qualcosa è andato storto. Il metodo viene chiamato su un oggetto parzialmente costruito o PropertyB è stato impostato su null in modo errato. Creando PropertyB qui, potremmo nascondere un bug molto più grande che potrebbe morderci in seguito, come un bug che causa il danneggiamento dei dati.

Se, invece, lasciamo bolle il riferimento null, stiamo facendo sapere allo sviluppatore che ha chiamato questo metodo, non appena possiamo, che qualcosa è andato storto. È stata persa una condizione preliminare vitale per chiamare questo metodo.

Quindi, in effetti, stiamo lanciando in anticipo perché separa molto meglio le nostre preoccupazioni. Non appena si è verificato un errore, stiamo informando gli sviluppatori a monte.

Perché "prendere in ritardo" è una storia diversa. Non vogliamo davvero catturare in ritardo, vogliamo davvero catturare non appena sappiamo come gestire correttamente il problema. Alcune volte questo sarà quindici strati di astrazione più tardi e altre volte questo sarà al punto della creazione.

Il punto è che vogliamo cogliere l'eccezione al livello di astrazione che ci consente di gestire l'eccezione nel punto in cui abbiamo tutte le informazioni necessarie per gestire correttamente l'eccezione.


Penso che tu stia usando gli sviluppatori upstream nel senso sbagliato. Inoltre, hai detto che viola il principio della singola responsabilità, ma in realtà molte inizializzazioni su richiesta e memorizzazione nella cache del valore sono implementate in questo modo (con i controlli di concorrenza adeguati al posto del corso)
Daniel Dinnyes

Nel tuo esempio, che ne dici di controllare null prima dell'operazione di sottrazione, in questo modoif(PropertyB == null) return 0;
user1451111

1
Puoi anche approfondire il tuo ultimo paragrafo, in particolare cosa intendi per " strato di astrazione ".
user1451111

Se stiamo eseguendo un lavoro di IO, lo strato di astrazione per catturare un'eccezione di IO sarebbe dove stiamo facendo il lavoro. A quel punto abbiamo tutte le informazioni di cui abbiamo bisogno per decidere se vogliamo riprovare o lanciare una finestra di messaggio per l'utente o creare un oggetto usando una serie di impostazioni predefinite.
Stephen,

"Nel tuo esempio dato, che ne dici di controllare null prima dell'operazione di sottrazione, in questo modo se (PropertyB == null) restituisce 0;" Che schifo. Sarebbe dire al metodo di chiamata che ho cose valide da sottrarre da e verso. Naturalmente questo è contestuale, ma sarebbe una cattiva pratica fare il mio errore controllando qui nella maggior parte dei casi.
Stephen,

6

Lancia non appena vedi qualcosa per cui vale la pena lanciare per evitare di mettere gli oggetti in uno stato non valido. Ciò significa che se viene passato un puntatore nullo, lo si controlla in anticipo e si lancia un NPE prima che abbia la possibilità di passare al livello basso.

Cattura non appena sai cosa fare per correggere l'errore (questo non è generalmente dove si getta altrimenti potresti semplicemente usare un if-else), se viene passato un parametro non valido, il layer che ha fornito il parametro dovrebbe affrontare le conseguenze .


1
Hai scritto: Lancia presto, ... Presto presto ...! Perché? Questo è un approccio completamente contrario al contrario di "lanciare in anticipo, prendere in ritardo".
shylynx,

1
@shylynx Non so da dove venga "lancia presto, cattura tardi", ma il suo valore è discutibile. Cosa significa esattamente "tardi"? Dove ha senso catturare un'eccezione (se non del tutto) dipende dal problema. L'unica cosa chiara è che vuoi rilevare i problemi (e lanciarli) il prima possibile.
Doval

2
Suppongo che "catturare in ritardo" abbia lo scopo di contrastare la pratica della cattura prima che tu possa sapere cosa fare per correggere l'errore, ad esempio a volte vedi funzioni che catturano tutto solo in modo che possano stampare un messaggio di errore e quindi riproporre l'eccezione.

@Hurkyl: Un problema con "cattura in ritardo" è che se un'eccezione si diffonde attraverso livelli che non ne sanno nulla, può essere difficile per il codice che potrebbe essere in grado di fare qualcosa per la situazione sapere che le cose sono come previsto. Come semplice esempio, supponiamo che un parser per un file di documento utente debba caricare un CODEC dal disco e si verifichi un errore del disco durante la lettura, il codice che chiama il parser potrebbe agire in modo inappropriato se ritiene che si sia verificato un errore del disco durante la lettura dell'utente documento.
supercat

4

Una regola aziendale valida è "se il software di livello inferiore non riesce a calcolare un valore, quindi ..."

Questo può essere espresso solo a un livello superiore, altrimenti il ​​software di livello inferiore sta cercando di cambiare il suo comportamento in base alla propria correttezza, che sta per finire solo in un nodo.


2

Innanzitutto, le eccezioni riguardano situazioni eccezionali. Nel tuo esempio, nessuna cifra può essere calcolata se i dati grezzi non sono presenti perché non è stato possibile caricarli.

Dalla mia esperienza, è buona pratica sottrarre eccezioni mentre si cammina in cima allo stack. In genere i punti in cui si desidera eseguire ciò è ogni volta che un'eccezione attraversa il confine tra due livelli.

Se si verifica un errore durante la raccolta dei dati non elaborati nel livello dati, genera un'eccezione per informare chiunque abbia richiesto i dati. Non tentare di aggirare questo problema qui. La complessità del codice di gestione può essere molto elevata. Inoltre, il livello dati è responsabile solo della richiesta dei dati, non della gestione degli errori che si verificano durante tale operazione. Questo significa "lanciare presto" .

Nel tuo esempio il livello di cattura è il livello di servizio. Il servizio stesso è un nuovo livello, situato sopra il livello di accesso ai dati. Quindi vuoi cogliere l'eccezione lì. Forse il tuo servizio ha qualche infrastruttura di failover e prova a richiedere i dati da un altro repository. Se anche questo fallisce, racchiudi l'eccezione all'interno di qualcosa che il chiamante del servizio comprende (se si tratta di un servizio Web, potrebbe trattarsi di un errore SOAP). Imposta l'eccezione originale come eccezione interna in modo che i livelli successivi possano registrare esattamente ciò che è andato storto.

L'errore del servizio può essere rilevato dal layer che chiama il servizio (ad esempio l'interfaccia utente). E questo significa "catturare tardi" . Se non si è in grado di gestire l'eccezione in un livello inferiore, ridisegnarla. Se il livello più alto non può gestire l'eccezione, gestiscilo! Ciò potrebbe includere la registrazione o la presentazione.

Il motivo per cui dovresti ricodificare le eccezioni (come descritto in precedenza avvolgendole in eccezioni più generali) è che molto probabilmente l'utente non è in grado di capire che si è verificato un errore perché, ad esempio, un puntatore puntava a memoria non valida. E a lui non importa. Gli importa solo che la cifra non possa essere calcolata dal servizio e questa è l'informazione che dovrebbe essere mostrata a lui.

Andando oltre puoi (in un mondo ideale) tralasciare completamente try/ catchcodice dall'interfaccia utente. Utilizzare invece un gestore di eccezioni globale in grado di comprendere le eccezioni eventualmente generate dagli strati inferiori, scriverle in alcuni log e inserirle in oggetti di errore che contengono informazioni significative (e possibilmente localizzate) dell'errore. Tali oggetti possono essere facilmente presentati all'utente in qualsiasi forma desiderata (finestre di messaggio, notifiche, toast di messaggi e così via).


1

Generare eccezioni all'inizio in generale è una buona pratica perché non si desidera che i contratti rotti scorrano il codice oltre il necessario. Ad esempio, se si prevede che un determinato parametro di funzione sia un numero intero positivo, è necessario applicare quel vincolo nel punto della chiamata di funzione anziché attendere fino a quando quella variabile non viene utilizzata da qualche altra parte nello stack di codice.

Prendere in ritardo non posso davvero commentare perché ho le mie regole e cambia da un progetto all'altro. L'unica cosa che cerco di fare è però separare le eccezioni in due gruppi. Uno è solo per uso interno e l'altro è solo per uso esterno. Le eccezioni interne vengono rilevate e gestite dal mio codice e le eccezioni esterne devono essere gestite da qualsiasi codice mi stia chiamando. Questa è fondamentalmente una forma di cattura delle cose in seguito, ma non del tutto perché mi offre la flessibilità di deviare dalla regola quando necessario nel codice interno.

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.