Le variabili di errore sono un anti-pattern o un buon design?

43

Per gestire diversi possibili errori che non dovrebbero interrompere l'esecuzione, ho una variabile error che i client possono controllare e utilizzare per generare eccezioni. È un anti-pattern? C'è un modo migliore per gestire questo? Per un esempio di questo in azione puoi vedere l'API di PHP mysqli . Supponiamo che i problemi di visibilità (accessor, ambito pubblico e privato, sia la variabile di una classe o globale?) Siano gestiti correttamente.

    
posta Trenton Maki 17.06.2014 - 08:22
fonte

11 risposte

61

Se una lingua supporta intrinsecamente le eccezioni, allora è preferibile generare eccezioni e i client possono rilevare l'eccezione se non vogliono che si traduca in un errore. Infatti, i client del tuo codice si aspettano eccezioni e si imbatteranno in molti bug perché non controlleranno i valori di ritorno.

Ci sono alcuni vantaggi nell'usare le eccezioni se hai una scelta.

Messaggi

Le eccezioni contengono messaggi di errore leggibili dall'utente che possono essere utilizzati dagli sviluppatori per il debug o anche visualizzati agli utenti se lo si desidera. Se il codice che consuma non può gestire l'eccezione, può sempre registrarlo in modo che gli sviluppatori possano passare attraverso i registri senza doversi fermare ad ogni altra traccia per capire quale era il valore restituito e mapparlo in una tabella per capire quale era il vera eccezione.

Con i valori di ritorno, non è possibile fornire facilmente ulteriori informazioni. Alcune lingue supporteranno le chiamate al metodo per ottenere l'ultimo messaggio di errore, quindi questa preoccupazione è un po 'attenuata, ma ciò richiede che il chiamante effettui chiamate aggiuntive e talvolta richieda l'accesso a un "oggetto speciale" che trasporta queste informazioni.

Nel caso di messaggi di eccezione, fornisco il maggior numero possibile di contesto, come ad esempio:

A policy of name "foo" could not be retrieved for user "bar", which was referenced in user's profile.

Confrontalo con un codice di ritorno -85. Quale preferiresti?

Stack di chiamate

Le eccezioni di solito hanno anche stack di chiamate dettagliate che aiutano a eseguire il debug del codice più velocemente e più rapidamente, e possono anche essere registrati dal codice chiamante se lo si desidera. Ciò consente agli sviluppatori di individuare il problema di solito sulla linea esatta, e quindi è molto potente. Ancora una volta, confrontalo con un file di log con valori di ritorno (come -85, 101, 0, ecc.), Quale preferisci?

Approccio di bias rapido fallito

Se un metodo viene chiamato da qualche parte che non riesce, genererà un'eccezione. Il codice chiamante deve sopprimere esplicitamente l'eccezione o fallire. Ho scoperto che questo è davvero sorprendente perché durante lo sviluppo e il collaudo (e anche in produzione) il codice fallisce rapidamente, costringendo gli sviluppatori a risolverlo. Nel caso dei valori di ritorno, se viene omesso un controllo per un valore di ritorno, l'errore viene ignorato silenziosamente e l'errore emerge da qualche parte inaspettato, solitamente con un costo molto più elevato per il debug e la correzione.

Eccezioni avvolgimento e scostamento

Le eccezioni possono essere racchiuse in altre eccezioni e poi scartate se necessario. Ad esempio, il tuo codice potrebbe generare ArgumentNullException che il codice chiamante potrebbe racchiudere all'interno di un UnableToRetrievePolicyException poiché tale operazione non è riuscita nel codice chiamante. Mentre all'utente potrebbe essere mostrato un messaggio simile all'esempio che ho fornito sopra, alcuni codici diagnostici potrebbero scartare l'eccezione e scoprire che un ArgumentNullException ha causato il problema, il che significa che si tratta di un errore di codifica nel codice del tuo consumatore. Questo potrebbe quindi attivare un avviso in modo che lo sviluppatore possa correggere il codice. Tali scenari avanzati non sono facili da implementare con i valori di ritorno.

Semplicità del codice

Questo è un po 'più difficile da spiegare, ma ho imparato attraverso questo codice sia con i valori di ritorno sia con le eccezioni. Il codice che è stato scritto utilizzando i valori restituiti di solito effettuava una chiamata e quindi ha una serie di controlli su quale fosse il valore restituito. In alcuni casi, chiamerebbe un altro metodo e ora avrà un'altra serie di controlli per i valori restituiti da quel metodo. Con le eccezioni, la gestione delle eccezioni è molto più semplice nella maggior parte se non in tutti i casi. Hai un blocco try / catch / finally, con il runtime che prova a fare del suo meglio per eseguire il codice nei blocchi finali per la pulizia. Anche i blocchi try / catch / finally nidificati sono relativamente più facili da seguire e gestire rispetto a nidificati se / else e valori di ritorno associati da più metodi.

Conclusione

Se la piattaforma che stai utilizzando supporta eccezioni (come Java o .NET), non dovresti assolutamente pensare che non ci sia altro modo se non lanciare eccezioni perché queste piattaforme hanno linee guida per generare eccezioni, e i tuoi clienti si aspettano così. Se stavo usando la tua libreria, non mi preoccuperei di controllare i valori di ritorno perché mi aspetto che vengano lanciate delle eccezioni, è così che è il mondo in queste piattaforme.

Tuttavia, se fosse C ++, sarebbe un po 'più difficile da determinare perché esiste già una grande base di codici con codici di ritorno, e un gran numero di sviluppatori sono sintonizzati per restituire valori in contrapposizione alle eccezioni (ad es. Windows è diffuso con HRESULT). Inoltre, in molte applicazioni, può anche essere un problema di prestazioni (o almeno percepito come).

    
risposta data 17.06.2014 - 09:20
fonte
20

Le variabili di errore sono relitti da linguaggi come C dove le eccezioni non erano disponibili. Oggi dovresti evitarli, tranne che stai scrivendo una libreria che è potenzialmente utilizzata da un programma C (o una lingua simile senza la gestione di eccezioni).

Naturalmente, se hai un tipo di errore che potrebbe essere classificato come "avvertimento" (= la tua biblioteca può fornire un risultato valido e il chiamante può ignorare l'avviso se pensa che non sia importante), allora uno stato l'indicatore sotto forma di variabile può avere senso anche nelle lingue con eccezioni. Ma attenzione, i chiamanti della biblioteca tendono a ignorare tali avvertimenti anche se non dovrebbero, quindi pensaci due volte prima di introdurre un tale costrutto nella tua lib.

    
risposta data 17.06.2014 - 09:45
fonte
16

Ci sono molti modi per segnalare un errore:

Il problema della variabile di errore è che è facile dimenticare di controllare.

Il problema delle eccezioni è che crea percorsi nascosti di esecuzioni e sebbene try / catch sia facile da scrivere, è molto difficile eseguire un ripristino corretto nella clausola catch (nessun supporto da sistemi / compilatori di tipi).

Il problema dei condition handler è che non compongono bene: se si dispone dell'esecuzione di codice dinamico (funzioni virtuali), è impossibile prevedere quali condizioni dovrebbero essere gestite. Inoltre, se la stessa condizione può essere sollevata in più punti, non si può dire che una soluzione uniforme possa essere applicata ogni volta ... diventa rapidamente disordinata.

I ritorni polimorfici ( Either a b in Haskell) sono la mia soluzione preferita finora:

  • esplicito: nessun percorso nascosto di esecuzione
  • esplicito: completamente documentato nella firma del tipo di funzione (nessuna sorpresa)
  • difficile da ignorare: devi abbinare lo schema per ottenere il risultato desiderato e gestire il caso di errore

L'unico problema è che potrebbero portare a un controllo eccessivo; le lingue che li usano hanno idiomi per concatenare i richiami di funzioni che li usano, ma potrebbe comunque richiedere un po 'più di digitazione / confusione. In Haskell questo sarebbe monadi , tuttavia questo è molto più spaventoso di quanto possa sembrare, vedi < a href="http://fsharpforfunandprofit.com/posts/recipe-part2/"> Programmazione orientata alle ferrovie .

    
risposta data 18.06.2014 - 10:32
fonte
12

Penso che sia terribile. Attualmente eseguo il refactoring di un'app Java che utilizza valori di ritorno anziché eccezioni. Anche se potresti non lavorare affatto con Java, penso che ciò sia comunque valido.

Finisci con un codice come questo:

String result = x.doActionA();
if (result != null) {
  throw new Exception(result);
}
result = x.doActionB();
if (result != null) {
  throw new Exception(result);
}

O questo:

if (!x.doActionA()) {
  throw new Exception(x.getError());
}
if (!x.doActionB()) {
  throw new Exception(x.getError());
}

Preferisco di gran lunga che le azioni generino eccezioni, quindi ti ritroverai con qualcosa del tipo:

x.doActionA();
x.doActionB();

Puoi racchiuderlo in un try-catch e ottenere il messaggio dall'eccezione oppure puoi scegliere di ignorare l'eccezione, ad esempio quando stai eliminando qualcosa che potrebbe già essere stato rimosso. Conserva anche la traccia dello stack, se ne hai uno. Anche i metodi diventano più facili. Invece di gestire le eccezioni, buttano semplicemente ciò che è andato storto.

Codice corrente (orribile):

private String doActionA() {
  try {
    someOperationThatCanGoWrong1();
    someOperationThatCanGoWrong2();
    someOperationThatCanGoWrong3();
    return null;
  } catch(Exception e) {
    return "Something went wrong!";
  }
}

Nuovo e migliorato:

private void doActionA() throws Exception {
  someOperationThatCanGoWrong1();
  someOperationThatCanGoWrong2();
  someOperationThatCanGoWrong3();
}

La traccia dello strack è preservata e il messaggio è disponibile nell'eccezione, piuttosto che nell'inutile "Qualcosa è andato storto!".

Ovviamente puoi fornire messaggi di errore migliori e dovresti. Ma questo post è qui perché il codice corrente su cui sto lavorando è un dolore, e non dovresti fare lo stesso.

    
risposta data 17.06.2014 - 08:53
fonte
5

"Per poter gestire diversi possibili errori, ciò non dovrebbe interrompere l'esecuzione"

Se si intende che gli errori non devono interrompere l'esecuzione della funzione corrente, ma devono essere segnalati al chiamante in qualche modo - allora si hanno alcune opzioni che non sono state menzionate. Questo caso è davvero più un avvertimento che un errore. Lancio / Ritorno non è un'opzione in quanto termina la funzione corrente. Un solo parametro di errore messaggio o ritorno consente solo che si verifichi al più uno di questi errori.

Due motivi che ho usato sono:

  • Una raccolta di errori / avvisi, passati o conservati come variabili membro. A cui aggiungi le cose e continua semplicemente l'elaborazione. Personalmente non mi piace molto questo approccio perché ritengo che disorienta il chiamante.

  • Passa in un oggetto gestore di errori / avvisi (o impostalo come variabile membro). E ogni errore chiama una funzione membro del gestore. In questo modo il chiamante può decidere cosa fare con tali errori non terminanti.

Ciò che si passa a queste raccolte / gestori dovrebbe contenere un contesto sufficiente affinché l'errore venga gestito "correttamente" - Una stringa di solito è troppo piccola, passarla qualche istanza di Eccezione è spesso ragionevole - ma a volte disapprovata (come un abuso di eccezioni).

Il codice tipografico che utilizza un gestore di errori potrebbe apparire come questo

class MyFunClass {
  public interface ErrorHandler {
     void onError(Exception e);
     void onWarning(Exception e);
  }

  ErrorHandler eh;

  public void canFail(int i) {
     if(i==0) {
        if(eh!=null) eh.onWarning(new Exception("canFail shouldn't be called with i=0"));
     }
     if(i==1) {
        if(eh!=null) eh.onError(new Exception("canFail called with i=1 is fatal");
        throw new RuntimeException("canFail called with i=2");
     }
     if(i==2) {
        if(eh!=null) eh.onError(new Exception("canFail called with i=2 is an error, but not fatal"));
     }
  }
}
    
risposta data 18.06.2014 - 03:08
fonte
5

Spesso non c'è niente di sbagliato nell'usare questo schema o quel modello, purché si usi il modello che tutti gli altri usano. Nello sviluppo Objective-C , lo schema preferito è quello di passare un puntatore dove il metodo chiamato può depositare un NSError object. Le eccezioni sono riservate per errori di programmazione e portano ad un crash (a meno che tu non abbia programmatori Java o .NET che scrivono la loro prima app per iPhone ). E questo funziona abbastanza bene.

    
risposta data 17.06.2014 - 18:10
fonte
4

La domanda ha già una risposta, ma non posso farcela.

Non puoi davvero aspettarti che Exception fornisca una soluzione per tutti i casi d'uso. Hammer chiunque?

Ci sono casi in cui le eccezioni non sono la fine e sono tutte, ad esempio, se un metodo riceve una richiesta ed è responsabile della convalida di tutti i campi passati, e non solo il primo, allora devi pensare che dovrebbe essere possibile indicare la causa dell'errore per più di un campo. Dovrebbe essere possibile anche indicare se la natura della validazione impedisce all'utente di andare oltre o meno. Un esempio di ciò sarebbe una password non strong. Puoi mostrare un messaggio all'utente indicando che la password inserita non è molto strong, ma che è abbastanza strong.

Si potrebbe sostenere che tutte queste convalide potrebbero essere generate come eccezione alla fine del modulo di convalida, ma sarebbero codici di errore in qualsiasi cosa tranne che nel nome.

Quindi la lezione qui è: le eccezioni hanno i loro posti, così come i codici di errore. Scegli saggiamente.

    
risposta data 18.06.2014 - 09:07
fonte
4

Ci sono casi d'uso in cui i codici di errore sono preferibili alle eccezioni.

Se il codice può continuare nonostante l'errore, ma deve essere segnalato, un'eccezione è una scelta sbagliata poiché le eccezioni interrompono il flusso. Ad esempio, se stai leggendo un file di dati e scopri che contiene alcuni dati non terminali non validi, potrebbe essere meglio leggere il resto del file e segnalare l'errore piuttosto che fallire a priori.

Le altre risposte hanno spiegato perché le eccezioni dovrebbero essere preferite ai codici di errore in generale.

    
risposta data 18.06.2014 - 13:47
fonte
2

Sicuramente non c'è niente di sbagliato nell'usare le eccezioni quando le eccezioni non sono adatte.

Quando l'esecuzione del codice non deve essere interrotta (ad esempio, intervenendo sull'input dell'utente che può contenere più errori, come un programma da compilare o un modulo da elaborare), trovo che raccogliere errori in variabili di errore come has_errors e error_messages è in effetti un design molto più elegante rispetto all'eliminazione di un'eccezione al primo errore. Permette di trovare tutti gli errori nell'input dell'utente senza costringere l'utente a reinviare inutilmente.

    
risposta data 07.07.2014 - 20:39
fonte
1

In alcuni linguaggi di programmazione dinamica puoi utilizzare sia valori di errore che gestione delle eccezioni . Questa operazione viene eseguita restituendo l'oggetto eccezione non sviluppata al posto del valore di ritorno ordinario, che può essere controllato come un valore di errore, ma genera un'eccezione se non è selezionato.

In Perl 6 viene eseguito tramite fail , che se in corso con no fatal; scope restituisce un'eccezione speciale non rilasciata Failure .

In Perl 5 puoi utilizzare Contestual :: Return puoi farlo con% % co_de.

    
risposta data 17.06.2014 - 15:32
fonte
-1

A meno che non ci sia qualcosa di molto specifico, penso che avere una variabile di errore per la convalida sia una cattiva idea. Lo scopo sembra essere quello di risparmiare tempo speso per la validazione (puoi solo restituire il valore della variabile)

Ma poi se cambi qualcosa, devi comunque ricalcolare quel valore. Però non posso dire altro di fermare e lanciare l'eccezione.

EDIT: non mi ero reso conto che questa è una questione di paradigma software invece di un caso specifico.

Permettetemi di chiarire ulteriormente i miei punti in un mio particolare caso in cui la mia risposta avrebbe senso

  1. Ho una collezione di oggetti entità
  2. Dispongo di servizi web in stile procedurale che funzionano con questi oggetti entità

Ci sono due tipi di errori:

  1. Gli errori durante l'elaborazione si verificano nel livello di servizio
  2. Gli errori perché ci sono incongruenze negli oggetti entità

Nel livello di servizio, non c'è altra scelta che usare l'oggetto risultato come wrapper, che è l'equivalenza della variabile di errore. È possibile simulare un'eccezione tramite la chiamata di servizio su protocollo come http, ma non è sicuramente una buona cosa da fare. Non sto parlando di questo tipo di errore e non penso che questo sia il tipo di errore che viene posto in questa domanda.

Stavo pensando al secondo tipo di errore. E la mia risposta riguarda questo secondo tipo di errori. Negli oggetti entità ci sono delle scelte per noi, alcuni sono

  • utilizzando una variabile di convalida
  • lancia immediatamente un'eccezione quando un campo è impostato in modo errato dal setter

L'utilizzo della variabile di convalida equivale ad avere un singolo metodo di convalida per ciascun oggetto entità. In particolare, l'utente può impostare il valore in un modo che mantenga il setter come purificatore, nessun effetto collaterale (questa è spesso una buona pratica) o uno può incorporare la convalida in ogni setter e quindi salvare il risultato in una variabile di convalida. Il vantaggio di ciò è di risparmiare tempo, il risultato della convalida viene memorizzato nella variabile di convalida in modo che quando l'utente chiama validation () più volte, non è necessario eseguire più convalide.

La cosa migliore da fare in questo caso è l'utilizzo di un singolo metodo di convalida senza nemmeno utilizzare alcuna convalida per memorizzare l'errore di convalida della cache. Questo aiuta a mantenere il setter come setter.

    
risposta data 17.06.2014 - 08:34
fonte