Le eccezioni come flusso di controllo sono considerate un serio antipattern? Se è così, perché?

107

Alla fine degli anni '90 ho lavorato un po 'con un codice base che utilizzava eccezioni come controllo del flusso. Ha implementato una macchina a stati finiti per pilotare le applicazioni di telefonia. Ultimamente mi sono ricordato di quei giorni perché stavo facendo applicazioni web MVC.

Entrambi hanno Controller s che decidono dove andare avanti e forniscono i dati alla logica di destinazione. Le azioni dell'utente dal dominio di un telefono della vecchia scuola, come i toni DTMF, sono diventati parametri per i metodi di azione, ma invece di restituire qualcosa come ViewResult , hanno lanciato un StateTransitionException .

Penso che la differenza principale sia stata che i metodi di azione erano% funzioni% di%. Non ricordo tutte le cose che ho fatto con questo fatto, ma sono stato titubante anche a percorrere la strada del ricordo di molto perché, da quel lavoro, come 15 anni fa, non l'ho mai visto nel codice di produzione in nessun altro lavoro . Presumo che questo fosse un segno che si trattava di un cosiddetto anti-pattern.

È questo il caso, e in tal caso, perché?

Aggiornamento: quando ho posto la domanda, avevo già in mente la risposta di @ MasonWheeler, quindi sono andato con la risposta che più mi è stata aggiunta. Penso che anche la sua sia una buona risposta.

    
posta Aaron Anodide 04.03.2013 - 23:27
fonte

9 risposte

92

C'è una discussione dettagliata su questo Wiki di Ward . Generalmente, l'uso delle eccezioni per il controllo del flusso è un anti-pattern, con le eccezioni tosse di tosse rilevanti per la lingua e per la specificazione linguistica

.

Come rapido riepilogo del motivo, in generale, è un anti-pattern:

  • Le eccezioni sono, in sostanza, sofisticate istruzioni GOTO
  • Programmare con eccezioni, quindi, rende più difficile leggere e comprendere il codice
  • La maggior parte delle lingue ha strutture di controllo esistenti progettate per risolvere i tuoi problemi senza l'uso di eccezioni
  • Gli argomenti per l'efficienza tendono a essere discutibili per i compilatori moderni, che tendono ad ottimizzarsi nell'ipotesi che le eccezioni non vengano utilizzate per il controllo del flusso.

Leggi la discussione sul wiki di Ward per informazioni molto più approfondite.

    
risposta data 04.03.2013 - 23:40
fonte
95

Il caso d'uso per cui sono state progettate delle eccezioni è "Ho appena incontrato una situazione che non riesco a gestire correttamente a questo punto, perché non ho abbastanza contesto per gestirlo, ma la routine che mi ha chiamato (o qualcosa di più lo stack delle chiamate) dovrebbe sapere come gestirlo. "

Il caso d'uso secondario è "Ho appena riscontrato un errore grave, e in questo momento uscire da questo flusso di controllo per prevenire il danneggiamento dei dati o altri danni è più importante che provare a proseguire in avanti."

Se non utilizzi le eccezioni per uno di questi due motivi, probabilmente c'è un modo migliore per farlo.

    
risposta data 04.03.2013 - 23:41
fonte
26

Le eccezioni sono potenti come Continuazioni e GOTO . Sono un costrutto di flusso di controllo universale.

In alcune lingue, sono il costrutto del flusso di controllo universale solo . JavaScript, ad esempio, non ha né Continuazioni né GOTO , non ha nemmeno Chiamate Coda Corrette. Quindi, se vuoi implementare un flusso di controllo sofisticato in JavaScript, hai per usare Eccezioni.

Il progetto Microsoft Volta era un progetto di ricerca (ora dismesso) per compilare codice .NET arbitrario su JavaScript. .NET ha delle eccezioni la cui semantica non si adatta esattamente a JavaScript, ma soprattutto ha Thread e devi mapparle in qualche modo su JavaScript. Volta ha implementato Volta Continuations utilizzando le eccezioni JavaScript e implementando tutti i costrutti del flusso di controllo .NET in termini di Volta Continuations. Loro avevano di usare Eccezioni come flusso di controllo, perché non esiste un altro costrutto del flusso di controllo abbastanza potente.

Hai citato macchine statali. Gli SM sono banali da implementare con le opportune chiamate di coda: ogni stato è una subroutine, ogni transizione di stato è una chiamata di subroutine. Gli SM possono anche essere facilmente implementati con GOTO o Coroutine o Continuazioni. Tuttavia, Java non ha nessuno di questi quattro, ma ha ha Eccezioni. Quindi, è perfettamente accettabile utilizzare quelli come flusso di controllo. (Beh, in effetti, la scelta corretta sarebbe probabilmente quella di usare una lingua con il costrutto del flusso di controllo appropriato, ma a volte potresti rimanere bloccato con Java.)

    
risposta data 05.03.2013 - 02:07
fonte
17

Come altri hanno menzionato numerosi, ( ad esempio in questa domanda di overflow dello stack ), il principio del minimo stupore vieterà di utilizzare eccezioni eccessivamente per il solo flusso di controllo. D'altra parte, nessuna regola è corretta al 100%, e ci sono sempre quei casi in cui un'eccezione è "lo strumento giusto" - molto simile a goto stesso, a proposito, che viene fornito sotto forma di break e continue in lingue come Java, che sono spesso il modo perfetto per saltare fuori da loop strongmente annidati, che non sono sempre evitabili.

Il seguente post sul blog spiega un caso d'uso piuttosto complesso ma anche piuttosto interessante per un non locale ControlFlowException :

Spiega come all'interno di jOOQ (una libreria di astrazione SQL per Java) (disclaimer: lavoro per il venditore), tali eccezioni sono occasionalmente utilizzato per abortire il processo di rendering SQL in anticipo quando si verifica una condizione "rara".

Esempi di tali condizioni sono:

  • Sono stati rilevati troppi valori di bind. Alcuni database non supportano numeri arbitrari di valori di bind nelle loro istruzioni SQL (SQLite: 999, Ingres 10.1.0: 1024, Sybase ASE 15.5: 2000, SQL Server 2008: 2100). In questi casi, jOOQ interrompe la fase di rendering SQL e ridisegna l'istruzione SQL con valori di binding integrati. Esempio:

    // Pseudo-code attaching a "handler" that will
    // abort query rendering once the maximum number
    // of bind values was exceeded:
    context.attachBindValueCounter();
    String sql;
    try {
    
      // In most cases, this will succeed:
      sql = query.render();
    }
    catch (ReRenderWithInlinedVariables e) {
      sql = query.renderWithInlinedBindValues();
    }
    

    Se estrassimo esplicitamente i valori di bind dalla query AST per contarli ogni volta, sprecheremmo preziosi cicli della CPU per il 99,9% delle query che non soffrono di questo problema.

  • Alcune logiche sono disponibili solo indirettamente tramite un'API che vogliamo eseguire solo "parzialmente". Il metodo UpdatableRecord.store() genera un'istruzione INSERT o UPDATE , a seconda di i flag interni di Record . Dal "fuori", non sappiamo quale tipo di logica è contenuta in store() (ad esempio blocco ottimistico, gestione di listener di eventi, ecc.), Quindi non vogliamo ripetere quella logica quando memorizziamo diversi record in un istruzione batch, in cui vorremmo che store() generi solo l'istruzione SQL, in realtà non la esegue. Esempio:

    // Pseudo-code attaching a "handler" that will
    // prevent query execution and throw exceptions
    // instead:
    context.attachQueryCollector();
    
    // Collect the SQL for every store operation
    for (int i = 0; i < records.length; i++) {
      try {
        records[i].store();
      }
    
      // The attached handler will result in this
      // exception being thrown rather than actually
      // storing records to the database
      catch (QueryCollectorException e) {
    
        // The exception is thrown after the rendered
        // SQL statement is available
        queries.add(e.query());                
      }
    }
    

    Se avessimo esternalizzato la logica store() in un'API "riutilizzabile" che può essere personalizzata per eseguire facoltativamente non l'esecuzione dell'SQL, dovremmo cercare di creare una soluzione piuttosto difficile da mantenere, API difficilmente riutilizzabile.

Conclusione

In sostanza, il nostro utilizzo di questi% non locali goto è proprio sulla falsariga di ciò che Mason Wheeler ha detto nella sua risposta:

"I just encountered a situation that I cannot deal with properly at this point, because I don't have enough context to handle it, but the routine that called me (or something further up the call stack) ought to know how to handle it."

Entrambi gli usi di ControlFlowExceptions erano piuttosto facili da implementare rispetto alle loro alternative, permettendoci di riutilizzare una vasta gamma di logiche senza refactoring dagli interni pertinenti.

Tuttavia, rimane la sensazione che questa sia una sorpresa per i futuri manutentori. Il codice sembra piuttosto delicato e, anche se in questo caso era la scelta giusta, preferiremmo sempre non utilizzare eccezioni per il flusso di controllo local , dove è facile evitare l'uso di branching ordinario attraverso if - else .

    
risposta data 11.01.2015 - 09:16
fonte
9

L'uso di eccezioni per il controllo del flusso è generalmente considerato un anti-modello, ma ci sono eccezioni (non è previsto il gioco di parole).

È stato detto migliaia di volte, che le eccezioni sono pensate per condizioni eccezionali. Una connessione al database interrotta è una condizione eccezionale. Un utente che immette lettere in un campo di input che dovrebbe consentire solo numeri non .

Un bug nel tuo software che fa sì che una funzione venga chiamata con argomenti illegali, ad es. null dove non è consentito, è una condizione eccezionale.

Usando eccezioni per qualcosa che non è eccezionale, stai usando astrazioni inappropriate per il problema che stai cercando di risolvere.

Ma può esserci anche una penalità per le prestazioni. Alcune lingue hanno un'implementazione della gestione delle eccezioni più o meno efficiente, quindi se la tua lingua preferita non ha un'eccezionale gestione delle eccezioni, può essere molto costosa, in termini di prestazioni *.

Ma altri linguaggi, ad esempio Ruby, hanno una sintassi simile all'eccezione per il controllo del flusso. Le situazioni eccezionali sono gestite dagli operatori raise / rescue . Ma puoi usare throw / catch per costrutti di flusso di controllo di tipo eccezione **.

Quindi, sebbene le eccezioni generalmente non siano utilizzate per il controllo del flusso, la tua lingua preferita potrebbe avere altri idiomi.

* En esempio di un uso costoso delle prestazioni di eccezioni: una volta sono stato impostato per ottimizzare un'applicazione ASP.NET Web con prestazioni scarse. Si è scoperto che il rendering di un grande tavolo stava chiamando int.Parse() su ca. un migliaio di stringhe vuote su una pagina media, con un risultato di ca. sono state gestite migliaia di eccezioni. Sostituendo il codice con int.TryParse() , mi sono rasato un secondo! Per ogni singola richiesta di pagina!

** Questo può essere molto confuso per un programmatore che arriva in Ruby da altre lingue, poiché sia throw che catch sono parole chiave associate a eccezioni in molti altri linguaggi.

    
risposta data 20.10.2014 - 14:25
fonte
8

È completamente possibile gestire le condizioni di errore senza l'uso di eccezioni. Alcune lingue, in particolare C, non hanno nemmeno eccezioni e le persone riescono ancora a creare applicazioni abbastanza complesse con esso. Il motivo per cui le eccezioni sono utili è che consentono di specificare in modo succinto due flussi di controllo essenzialmente indipendenti nello stesso codice: uno se si verifica un errore e uno se non lo fa. Senza di loro, si finisce con il codice in tutto il luogo che assomiglia a questo:

status = getValue(&inout);
if (status < 0)
{
    logError("message");
    return status;
}

doSomething(*inout);

O equivalente nella tua lingua, come restituire una tupla con un valore come stato di errore, ecc. Spesso le persone che sottolineano come la gestione delle eccezioni "costosa", trascurano tutte le dichiarazioni extra di if come sopra che sei richiesto da aggiungere se non si utilizzano eccezioni.

Sebbene questo modello si verifichi più spesso quando si gestiscono errori o altre "condizioni eccezionali", a mio parere se si inizia a vedere il codice standard in altre circostanze, si ha un buon argomento per l'utilizzo delle eccezioni. A seconda della situazione e dell'implementazione, posso vedere le eccezioni utilizzate validamente in una macchina a stati, perché hai due flussi di controllo ortogonali: uno che sta cambiando lo stato e uno per gli eventi che si verificano all'interno degli stati.

Tuttavia, queste situazioni sono rare e se hai intenzione di fare un'eccezione (gioco di parole) alla regola, è meglio essere preparati a mostrare la sua superiorità rispetto ad altre soluzioni. Una deviazione priva di tale giustificazione è giustamente definita anti-modello.

    
risposta data 05.03.2013 - 00:22
fonte
5

In Python, le eccezioni vengono utilizzate per la terminazione di generatore e iterazione. Python ha un try / except molto efficiente, ma in realtà sollevare un'eccezione ha qualche overhead.

A causa della mancanza di interruzioni multi-livello o di un'istruzione goto in Python, talvolta ho usato eccezioni:

class GOTO(Exception):
  pass

try:
  # Do lots of stuff
  # in here with multiple exit points
  # each exit point does a "raise GOTO()"
except GOTO:
  pass
except Exception as e:
  #display error
    
risposta data 05.03.2013 - 00:36
fonte
4

Analizziamo un utilizzo di questa eccezione:

L'algoritmo esegue ricerche in modo ricorsivo fino a quando non viene trovato qualcosa. Quindi, tornando dalla ricorsione, si deve controllare il risultato per essere trovato, e poi tornare, altrimenti continuare. E che torna ripetutamente da qualche profondità di ricorsione.

Oltre ad aver bisogno di un extra booleano found (da impacchettare in una classe, dove altrimenti potrebbe essere restituito solo un int), e per la profondità di ricorsione si verifica lo stesso postludio.

Un tale svolgimento di uno stack di chiamate è l'unica eccezione. Quindi mi sembra un mezzo non-goto-simile, più immediato e appropriato di codifica. Non necessario, uso raro, forse cattivo stile, ma fino al punto. Paragonabile con l'operazione di taglio Prolog.

    
risposta data 20.10.2014 - 14:49
fonte
4

La programmazione riguarda il lavoro

Penso che il modo più semplice per rispondere a questo è capire i progressi compiuti dall'OOP nel corso degli anni. Tutto ciò che è stato fatto in OOP (e nella maggior parte dei paradigmi di programmazione, per così dire) è modellato intorno a che richiede lavoro svolto .

Ogni volta che viene chiamato un metodo, il chiamante sta dicendo "Non so come fare questo lavoro, ma sai come, quindi lo fai per me."

Questo ha presentato una difficoltà: cosa succede quando il metodo chiamato generalmente sa come fare il lavoro, ma non sempre? Avevamo bisogno di un modo per comunicare "Volevo aiutarti, davvero, ma non posso farlo".

Una prima metodologia per comunicare questo era semplicemente restituire un valore "garbage". Forse ti aspetti un intero positivo, quindi il metodo chiamato restituisce un numero negativo. Un altro modo per farlo era impostare un valore di errore da qualche parte. Sfortunatamente, in entrambi i casi il codice let-me-check-over-here-to-make-sure-kosher è stato sostituito. Man mano che le cose si complicano, questo sistema va in pezzi.

Un'analogia eccezionale

Diciamo che hai un falegname, un idraulico e un elettricista. Vuoi fare l'idraulico per aggiustare il lavandino, così ci dà un'occhiata. Non è molto utile se ti dice solo te, "Scusa, non posso aggiustarlo. È rotto." Diavolo, è ancora peggio se dovesse dare un'occhiata, uscire e mandarti una mail lettera dicendo che non poteva aggiustarlo. Ora devi controllare la tua posta prima ancora di sapere che non ha fatto quello che volevi.

Ciò che preferiresti è che ti dica, "Guarda, non potrei aggiustarlo perché sembra che la tua pompa non funzioni."

Con queste informazioni, puoi concludere che vuoi che l'elettricista dia un'occhiata al problema. Forse l'elettricista troverà qualcosa relativo al falegname e dovrai far aggiustare il falegname.

Diamine, potresti anche non sapere che hai bisogno di un elettricista, potresti non sapere chi hai bisogno. Sei solo una middle management in un'azienda di riparazioni a domicilio e il tuo obiettivo è l'idraulica. Quindi dici che sei il capo del problema, e poi he dice all'elettricista di ripararlo.

Questo è ciò che le eccezioni stanno modellando: modalità di fallimento complesse in modo disaccoppiato. L'elettricista non ha bisogno di sapere di elettricista - non ha nemmeno bisogno di sapere che qualcuno in cima alla catena può risolvere il problema. Riferisce solo sul problema che ha incontrato.

Quindi ... un anti-pattern?

Ok, la comprensione del punto delle eccezioni è il primo passo. Il prossimo è capire che cos'è un anti-pattern.

Per qualificarsi come anti-modello, è necessario

  • risolvi il problema
  • hanno conseguenze definitivamente negative

Il primo punto è facilmente soddisfatto - il sistema ha funzionato, giusto?

Il secondo punto è più appiccicoso. La ragione principale per l'utilizzo di eccezioni come normale flusso di controllo è negativa è perché non è il loro scopo. Qualsiasi dato elemento di funzionalità in un programma dovrebbe avere uno scopo relativamente chiaro e la cooptazione di tale scopo porta a confusione inutile.

Ma questo non è un danno definitivo . È un modo scadente di fare le cose, e strano, ma un anti-modello? No. Solo ... strano.

    
risposta data 29.06.2016 - 01:51
fonte

Leggi altre domande sui tag