Quali sono le migliori pratiche per la cattura e il rilancio delle eccezioni?


156

Le eccezioni rilevate devono essere respinte direttamente o devono essere racchiuse in una nuova eccezione?

Cioè, dovrei fare questo:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw $e;
}

o questo:

try {
  $connect = new CONNECT($db, $user, $password, $driver, $host);
} catch (Exception $e) {
  throw new Exception("Exception Message", 1, $e);
}

Se la tua risposta è lanciare direttamente , suggerisci l'uso del concatenamento delle eccezioni , non sono in grado di comprendere uno scenario del mondo reale in cui utilizziamo il concatenamento delle eccezioni.

Risposte:


287

Non dovresti cogliere l'eccezione se non hai intenzione di fare qualcosa di significativo .

"Qualcosa di significativo" potrebbe essere uno di questi:

Gestire l'eccezione

L'azione più ovvia e significativa è gestire l'eccezione, ad esempio visualizzando un messaggio di errore e interrompendo l'operazione:

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    echo "Error while connecting to database!";
    die;
}

Registrazione o pulizia parziale

A volte non sai come gestire correttamente un'eccezione all'interno di un contesto specifico; forse ti mancano le informazioni sul "quadro generale", ma vuoi registrare il guasto il più vicino possibile al punto in cui è successo. In questo caso, potresti voler catturare, registrare e ri-lanciare:

try {
    $connect = new CONNECT($db, $user, $password, $driver, $host);
}
catch (Exception $e) {
    logException($e); // does something
    throw $e;
}

Uno scenario correlato è quello in cui ci si trova nel posto giusto per eseguire alcune operazioni di pulizia per l'operazione non riuscita, ma non per decidere come gestire l'errore al livello più alto. Nelle precedenti versioni di PHP questo sarebbe stato implementato come

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
catch (Exception $e) {
    $connect->disconnect(); // we don't want to keep the connection open anymore
    throw $e; // but we also don't know how to respond to the failure
}

PHP 5.5 ha introdotto la finallyparola chiave, quindi per gli scenari di pulizia c'è ora un altro modo per affrontarlo. Se il codice di cleanup deve essere eseguito indipendentemente da ciò che è accaduto (sia in caso di errore sia in caso di successo) è ora possibile farlo consentendo in modo trasparente la propagazione di eventuali eccezioni generate:

$connect = new CONNECT($db, $user, $password, $driver, $host);
try {
    $connect->insertSomeRecord();
}
finally {
    $connect->disconnect(); // no matter what
}

Astrazione degli errori (con concatenamento delle eccezioni)

Un terzo caso è quello in cui si desidera raggruppare logicamente molti possibili guasti sotto un ombrello più grande. Un esempio per il raggruppamento logico:

class ComponentInitException extends Exception {
    // public constructors etc as in Exception
}

class Component {
    public function __construct() {
        try {
            $connect = new CONNECT($db, $user, $password, $driver, $host);
        }
        catch (Exception $e) {
            throw new ComponentInitException($e->getMessage(), $e->getCode(), $e);
        }
    }
}

In questo caso, non si desidera che gli utenti Componentsappiano che è implementato utilizzando una connessione al database (forse si desidera mantenere aperte le opzioni e utilizzare l'archiviazione basata su file in futuro). Quindi la tua specifica Componentdirebbe che "in caso di errore di inizializzazione, ComponentInitExceptionverrà lanciato". Ciò consente ai consumatori di Componentrilevare eccezioni del tipo previsto, consentendo al contempo al codice di debug di accedere a tutti i dettagli (dipendenti dall'implementazione) .

Fornire un contesto più ricco (con il concatenamento delle eccezioni)

Infine, ci sono casi in cui potresti voler fornire più contesto per l'eccezione. In questo caso ha senso racchiudere l'eccezione in un'altra che contiene ulteriori informazioni su ciò che si stava tentando di fare quando si è verificato l'errore. Per esempio:

class FileOperation {
    public static function copyFiles() {
        try {
            $copier = new FileCopier(); // the constructor may throw

            // this may throw if the files do no not exist
            $copier->ensureSourceFilesExist();

            // this may throw if the directory cannot be created
            $copier->createTargetDirectory();

            // this may throw if copying a file fails
            $copier->performCopy();
        }
        catch (Exception $e) {
            throw new Exception("Could not perform copy operation.", 0, $e);
        }
    }
}

Questo caso è simile al precedente (e l'esempio probabilmente non è il migliore che si potrebbe trovare), ma illustra il punto di fornire più contesto: se viene generata un'eccezione, ci dice che la copia del file non è riuscita. Ma perché ha fallito? Questa informazione è fornita nelle eccezioni racchiuse (di cui potrebbe esserci più di un livello se l'esempio fosse molto più complicato).

Il valore di ciò è illustrato se si pensa a uno scenario in cui, ad esempio, la creazione di un UserProfileoggetto provoca la copia di file perché il profilo utente è archiviato in file e supporta la semantica delle transazioni: è possibile "annullare" le modifiche perché vengono eseguite solo su un copia del profilo fino al momento del commit.

In questo caso, se lo hai fatto

try {
    $profile = UserProfile::getInstance();
}

e di conseguenza è stato rilevato un errore di eccezione "Impossibile creare la directory di destinazione", si avrebbe il diritto di essere confuso. Avvolgere questa eccezione "core" in strati di altre eccezioni che forniscono il contesto renderà molto più facile gestire l'errore ("Creazione della copia del profilo non riuscita" -> "Operazione di copia del file non riuscita" -> "Impossibile creare la directory di destinazione").


Sono d'accordo solo con gli ultimi 2 motivi: 1 / gestione dell'eccezione: non dovresti farlo a questo livello, 2 / registrazione o pulizia: usa finalmente e registra l'eccezione sopra il tuo datalayer
remi bourgarel

1
@remi: tranne che PHP non supporta il finallycostrutto (non ancora almeno) ... Quindi è fuori, il che significa che dobbiamo ricorrere a cose sporche come questa ...
ircmaxell,

@remibourgarel: 1: era solo un esempio. Ovviamente non dovresti farlo a questo livello, ma la risposta è abbastanza lunga. 2: Come dice @ircmaxell, non c'è finallyPHP.
Jon,

3
Infine, PHP 5.5 ora implementa finalmente.
OCDev

12
C'è una ragione per cui penso che ti sia sfuggito dalla tua lista qui - potresti non essere in grado di dire se puoi gestire un'eccezione fino a quando non l'hai colta e hai avuto la possibilità di ispezionarla. Ad esempio, un wrapper per un'API di livello inferiore che utilizza codici di errore (e ne ha milioni) potrebbe avere una singola classe di eccezione di cui genera un'istanza per qualsiasi errore, con una error_codeproprietà che può essere verificata per ottenere l'errore sottostante codice. Se sei solo in grado di gestire in modo significativo alcuni di quegli errori, probabilmente vorrai catturare, ispezionare e se non riesci a gestire l'errore, ripeti.
Mark Amery,

37

Bene, si tratta di mantenere l'astrazione. Quindi suggerirei di usare il concatenamento delle eccezioni per lanciare direttamente. Per quanto riguarda il motivo, lasciatemi spiegare il concetto di astrazioni che perdono

Diciamo che stai costruendo un modello. Il modello dovrebbe astrarre tutta la persistenza e la convalida dei dati dal resto dell'applicazione. Quindi ora cosa succede quando si riceve un errore del database? Se ripeti il ​​passaggio DatabaseQueryException, stai perdendo l'astrazione. Per capire il perché, pensa all'astrazione per un secondo. Non ti interessa come il modello memorizza i dati, solo che lo fa. Allo stesso modo non ti interessa esattamente cosa è andato storto nei sistemi sottostanti del modello, solo che sai che qualcosa è andato storto e approssimativamente cosa è andato storto.

Quindi, rilanciando DatabaseQueryException, perdi l'astrazione e richiedi al codice chiamante di comprendere la semantica di ciò che sta accadendo sotto il modello. Invece, crea un generico ModelStorageExceptione avvolgilo DatabaseQueryExceptionall'interno. In questo modo, il tuo codice chiamante può ancora provare a gestire l'errore semanticamente, ma non importa la tecnologia sottostante del Modello poiché stai solo esponendo errori da quel livello di astrazione. Ancora meglio, dal momento che hai racchiuso l'eccezione, se bolle completamente e deve essere registrato, puoi tracciare l'eccezione radice generata (walk the chain) in modo da avere ancora tutte le informazioni di debug di cui hai bisogno!

Non limitarti a catturare e riproporre la stessa eccezione a meno che non sia necessario eseguire un po 'di post-elaborazione. Ma un blocco come } catch (Exception $e) { throw $e; }è inutile. Ma puoi ri-avvolgere le eccezioni per un significativo guadagno di astrazione.


2
Bella risposta. Sembra che alcune persone intorno a StackTranslate.it (sulla base di risposte ecc.) Le stiano usando in modo sbagliato.
James,

8

IMHO, catturare un'eccezione per riproporla è inutile . In questo caso, non catturarlo e lasciare che i metodi chiamati in precedenza lo gestiscano (ovvero i metodi che sono "superiori" nello stack di chiamate) .

Se lo ripeti, concatenare l'eccezione catturata a quella nuova che lancerai è sicuramente una buona pratica, poiché manterrà le informazioni contenute nell'eccezione catturata. Tuttavia, ricodificarlo è utile solo se aggiungi alcune informazioni o gestisci qualcosa all'eccezione rilevata, che possa essere un contesto, valori, registrazione, liberare risorse, qualunque cosa.

Un modo per aggiungere alcune informazioni è quello di estendere la Exceptionclasse, per avere eccezioni come NullParameterException, DatabaseExceptionecc Più sopra, questo permette il developper per prendere solo alcune eccezioni che può gestire. Ad esempio, si può solo catturare DatabaseExceptione provare a risolvere ciò che ha causato Exception, come ricollegarsi al database.


2
Non è inutile, ci sono momenti in cui devi fare qualcosa su un'eccezione, dire nella funzione che lo lancia e poi lanciarlo per consentire a un pescatore più alto di fare qualcos'altro. In uno dei progetti a cui sto lavorando a volte rileviamo un'eccezione in un metodo di azione, visualizziamo un avviso amichevole all'utente e quindi lo lanciamo in modo che un blocco di prova di prova più in là nel codice possa catturarlo nuovamente per registrare l'errore in un registro.
MitMaro,

1
Quindi, come ho detto, aggiungi alcune informazioni all'eccezione (visualizzazione di un avviso, registrazione). Non lo ripeti come nell'esempio del PO.
Clemente Herreman,

2
Bene, puoi semplicemente ricodificarlo se devi chiudere le risorse, ma non hai ulteriori informazioni da aggiungere. Sono d'accordo che non è la cosa più pulita del mondo, ma non è orribile
ircmaxell,

2
@ircmaxell Concordato, modificato per riflettere che è inutile solo se non fai nulla tranne che ridisegnarlo
Clement Herreman,

1
Il punto importante è che perdi il file e / o le informazioni sulla linea di dove l'eccezione era stata originariamente generata rilanciandolo. Quindi di solito è meglio lanciarne uno nuovo e passare quello vecchio, come nel secondo esempio della domanda. Altrimenti indicherà semplicemente il blocco di cattura, lasciandoti indovinare quale sia stato il vero problema.
DanMan,

2

Devi dare un'occhiata alle migliori pratiche di eccezione in PHP 5.3

La gestione delle eccezioni in PHP non è una novità in alcun modo. Nel seguente link, vedrai due nuove funzionalità in PHP 5.3 basate su eccezioni. Il primo è eccezioni nidificate e il secondo è un nuovo set di tipi di eccezione offerti dall'estensione SPL (che ora è un'estensione principale del runtime PHP). Entrambe queste nuove funzionalità hanno trovato la loro strada nel libro delle migliori pratiche e meritano di essere esaminate in dettaglio.

http://ralphschindler.com/2010/09/15/exception-best-practices-in-php-5-3


1

Di solito ci pensi in questo modo.

Una classe potrebbe generare molti tipi di eccezioni che non corrisponderanno. Quindi crei una classe di eccezione per quella classe o quel tipo di classe e la lanci.

Quindi il codice che utilizza la classe deve catturare solo un tipo di eccezione.


1
Ehi, puoi fornire qualche dettaglio in più o un link dove posso leggere di più su questo approccio.
Rahul Prasad,
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.