Selezionato vs Non selezionato vs Nessuna eccezione ... Una buona pratica di credenze contrarie

10

Ci sono molti requisiti necessari affinché un sistema trasmetta e gestisca correttamente le eccezioni. Ci sono anche molte opzioni per una lingua tra cui scegliere per implementare il concetto.

Requisiti per le eccezioni (in nessun ordine particolare):

  1. Documentazione : una lingua deve avere un mezzo per documentare le eccezioni che un'API può generare. Idealmente, questo supporto di documentazione dovrebbe essere utilizzabile dalla macchina per consentire ai compilatori e agli IDE di fornire supporto al programmatore.

  2. Trasmetti situazioni eccezionali : questo è ovvio, per consentire a una funzione di comunicare situazioni che impediscono alla funzionalità chiamata di eseguire l'azione prevista. A mio parere ci sono tre grandi categorie di tali situazioni:

    2.1 Bug nel codice che fanno sì che alcuni dati non siano validi.

    2.2 Problemi nella configurazione o altre risorse esterne.

    2.3 Risorse che sono intrinsecamente inaffidabili (rete, file system, database, utenti finali ecc.). Questi sono un caso un po 'in quanto la loro natura inaffidabile dovrebbe farci aspettare i loro fallimenti sporadici. In questo caso, queste situazioni sono da considerare eccezionali?

  3. Fornisci informazioni sufficienti per il codice per gestirlo : le eccezioni dovrebbero fornire sufficienti informazioni al destinatario affinché possa reagire e possibilmente gestire la situazione. le informazioni dovrebbero anche essere sufficienti in modo che, una volta registrate, queste eccezioni forniscano un contesto sufficiente a un programmatore per identificare e isolare le dichiarazioni incriminate e fornire una soluzione.

  4. Fornisci confidenza al programmatore sullo stato corrente dello stato di esecuzione del suo codice : le funzionalità di gestione delle eccezioni di un sistema software dovrebbero essere sufficienti a fornire le necessarie protezioni pur rimanendo fuori dal modo del programmatore in modo che possa rimanere concentrato sul compito a portata di mano.

Per coprire questi sono stati implementati i seguenti metodi in varie lingue:

  1. Eccezioni controllate Fornisce un ottimo metodo per documentare le eccezioni e, teoricamente, se implementato correttamente dovrebbe fornire un'ampia rassicurazione sul fatto che tutto sia buono. Tuttavia, il costo è tale che molti ritengono più produttivo semplicemente bypassare o deglutendo le eccezioni o rilanciarle come eccezioni non controllate. Se usato in modo inappropriato, le eccezioni controllate perdono praticamente tutte le sue utilità. Inoltre, le eccezioni controllate rendono difficile la creazione di un'API stabile nel tempo. Le implementazioni di un sistema generico all'interno di un dominio specifico porteranno il suo carico di situazioni eccezionali che diventerebbero difficili da mantenere utilizzando solo eccezioni controllate.

  2. Eccezioni non contrassegnate - molto più versatili dell'eccezione verificata, non riescono a documentare correttamente le possibili situazioni eccezionali di una determinata implementazione. Fanno affidamento sulla documentazione ad hoc, se non del tutto. Ciò crea situazioni in cui la natura inaffidabile di un supporto è mascherata da un'API che dà l'aspetto di affidabilità. Anche quando lanciate queste eccezioni perdono il loro significato mentre risalgono attraverso i livelli di astrazione. Dal momento che sono scarsamente documentati, un programmatore non può indirizzarli in modo specifico e spesso ha bisogno di gettare una rete molto più ampia del necessario per garantire che i sistemi secondari, in caso di fallimento, non abbattano l'intero sistema. Il che ci riporta direttamente al problema di deglutizione verificando le eccezioni fornite.

  3. Tipi di ritorno multistato Ecco qui fare affidamento su un insieme disgiunto, una tupla o un altro concetto simile per restituire il risultato atteso o un oggetto che rappresenta l'eccezione. Qui nessuno stack si sgancia, non taglia il codice, tutto viene eseguito normalmente ma il valore restituito deve essere convalidato per errore prima di continuare. Non ho ancora lavorato in questo modo, quindi non posso commentare per esperienza. Riconosco che risolve alcune eccezioni di problemi scavalcando il flusso normale, ma comunque soffrirà molto degli stessi problemi delle eccezioni controllate come noiosi e costantemente "in faccia".

Quindi la domanda è:

Qual è la tua esperienza in questa materia e quale, secondo te, è il miglior candidato per creare un buon sistema di gestione delle eccezioni per una lingua?

EDIT: Pochi minuti dopo aver scritto questa domanda mi sono imbattuto in questo post , spettrale!

    
posta Newtopian 30.05.2011 - 18:32
fonte

5 risposte

9

All'inizio del C ++ abbiamo scoperto che senza una sorta di programmazione generica, le lingue strongmente tipizzate erano estremamente ingombranti. Abbiamo anche scoperto che le eccezioni controllate e la programmazione generica non funzionavano bene insieme e che le eccezioni controllate erano sostanzialmente abbandonate.

I tipi di ritorno multiset sono ottimi, ma non sostituiscono le eccezioni. Senza eccezioni il codice è pieno di rumore di controllo degli errori.

L'altro problema con le eccezioni controllate è che una modifica delle eccezioni generate da una funzione di basso livello costringe a una cascata di modifiche in tutti i chiamanti, i loro chiamanti e così via. L'unico modo per evitare che ciò accada è che ogni livello di codice rilevi le eccezioni generate dai livelli inferiori e li avvolgano in una nuova eccezione. Di nuovo, si finisce con un codice molto rumoroso.

    
risposta data 30.05.2011 - 18:46
fonte
7

Per molto tempo i linguaggi OO, l'uso delle eccezioni è stato lo standard di fatto per comunicare errori. Ma i linguaggi di programmazione funzionale offrono la possibilità di un approccio diverso, ad es. usando monadi (che non ho usato), o la più leggera "programmazione orientata alle ferrovie", come descritto da Scott Wlaschin.

È davvero una variante del tipo di risultato multistato.

  • Una funzione restituisce un successo o un errore. Non può restituire entrambi (come nel caso di una tupla).
  • Tutti gli errori possibili sono stati sinteticamente documentati (almeno in F # con tipi di risultati come unioni discriminate).
  • Il chiamante non può utilizzare il risultato senza prendere in considerazione se il risultato è stato un successo o un fallimento.

Il tipo di risultato potrebbe essere dichiarato come questo

type Result<'TSuccess,'TFail> =
| Success of 'TSuccess
| Fail of 'TFail

Quindi il risultato di una funzione che restituisce questo tipo potrebbe essere un Success o un Fail type. Non può essere entrambi.

In più linguaggi di programmazione orientati all'imperativo, questo tipo di stile potrebbe richiedere una grande quantità di codice sul sito del chiamante. Ma la programmazione funzionale consente di costruire funzioni o operatori vincolanti per unire più funzioni in modo che la verifica degli errori non richieda metà del codice. Ad esempio:

// Create an updateUser function that takes an id, and new state
// as input, and updates an existing user.
let updateUser id input =
    validateInput input
    >>= loadUser id
    >>= updateUser input
    >>= saveUser id
    >>= notifyAboutUserUpdated

La funzione updateUser chiama ciascuna di queste funzioni in successione e ognuna di esse potrebbe fallire. Se tutti riescono, viene restituito il risultato dell'ultima funzione chiamata. Se una delle funzioni fallisce, il risultato di tale funzione sarà il risultato della funzione updateUser complessiva. Tutto questo è gestito dall'operatore personalizzato > > =.

Nell'esempio precedente, i tipi di errore potrebbero essere

type UserValidationErrorType =
| InvalidEmail of string
| MissingFirstName of string
... etc

type DbErrorType =
| RecordNotFound of int
| ConcurrencyError of int

type UpdateUserErrorType =
| InvalidInput of UserValidationErrorType
| DbError of DbErrorType

Se il chiamante di updateUser non gestisce esplicitamente tutti gli errori possibili dalla funzione, il compilatore emetterà un avviso. Quindi hai tutto documentato.

In Haskell esiste una notazione do che può rendere il codice ancora più pulito.

    
risposta data 28.06.2014 - 09:54
fonte
4

Trovo molto utile la risposta di Pete e vorrei aggiungere qualche considerazione e un esempio. Una discussione molto interessante sull'uso delle eccezioni rispetto alla restituzione di valori di errore speciali può essere trovata in Programmazione in Standard ML, di Robert Harper , alla fine della Sezione 29.3, pagina 243, 244.

Il problema è implementare una funzione parziale f che restituisce un valore di qualche tipo t . Una soluzione è avere la funzione avere tipo

f : ... -> t

e genera un'eccezione quando non c'è alcun risultato possibile. La seconda soluzione è implementare una funzione con tipo

f : ... -> t option

e restituisce SOME v in caso di successo e NONE in caso di fallimento.

Ecco il testo del libro, con un piccolo adattamento fatto da me stesso per rendere il testo più generale (il libro si riferisce a un particolare esempio). Il testo modificato è scritto in corsivo .

What are the trade-offs between the two solutions?

  1. The solution based on option types makes explicit in the type of the function f the possibility of failure. This forces the programmer to explicitly test for failure using a case analysis on the result of the call. The type checker will ensure that one cannot use t option where a t is expected. The solution based on exceptions does not explicitly indicate failure in its type. However, the programmer is nevertheless forced to handle the failure, for otherwise an uncaught exception error would be raised at run-time, rather than compile-time.
  2. The solution based on option types requires an explicit case analysis on the result of each call. If “most” results are successful, the check is redundant and therefore excessively costly. The solution based on exceptions is free of this overhead: it is biased towards the “normal” case of returning a t, rather than the “failure” case of not returning a result at all. The implementation of exceptions ensures that the use of a handler is more efficient than an explicit case analysis in the case that failure is rare compared to success.

[cut] In general, if efficiency is paramount, we tend to prefer exceptions if failure is a rarity, and to prefer options if failure is relatively common. If, on the other hand, static checking is paramount, then it is advantageous to use options since the type checker will enforce the requirement that the programmer check for failure, rather than having the error arise only at run-time.

Questo riguarda la scelta tra le eccezioni e i tipi di restituzione delle opzioni.

Per quanto riguarda l'idea che la rappresentazione di un errore nel tipo di ritorno porti a un controllo degli errori diffuso in tutto il codice: non è necessario. Ecco un piccolo esempio in Haskell che illustra questo.

Supponiamo di voler analizzare due numeri e poi dividere il primo per il secondo. Quindi potrebbe esserci un errore durante l'analisi di ciascun numero o durante la divisione (divisione per zero). Quindi dobbiamo controllare un errore dopo ogni passaggio.

import Text.Read

parseInt :: String -> Maybe Int
parseInt s = readMaybe s :: Maybe Int

safeDiv :: Int -> Int -> Maybe Int
safeDiv n d = if d /= 0 then Just (n 'div' d) else Nothing

toString :: Maybe Int -> String
toString (Just i) = show i
toString Nothing  = "error"

main = do
         -- Get two lines from the terminal.
         nStr <- getLine
         dStr <- getLine

         -- Parse each string and divide.
         let r = do n <- parseInt nStr
                    d <- parseInt dStr
                    safeDiv n d

         -- Print the result.
         putStrLn $ toString r

L'analisi e la divisione vengono eseguite nel blocco let ... . Si noti che usando la notazione Maybe monad e la notazione do , viene specificato solo il percorso successo : la semantica della Maybe monad propaga implicitamente il valore dell'errore ( Nothing ). Nessun sovraccarico per il programmatore.

    
risposta data 28.06.2014 - 13:00
fonte
1

Sono diventato un grande fan delle Eccezioni controllate e vorrei condividere la mia regola generale su quando usarle.

Sono giunto alla conclusione che ci sono fondamentalmente 2 tipi di errori che il mio codice deve gestire. Esistono errori verificabili prima dell'esecuzione del codice e errori non verificabili prima dell'esecuzione del codice. Un semplice esempio di errore verificabile prima dell'esecuzione del codice in NullPointerException.

//... bad code below.  the runnable variable
// tries to call the run() method before the variable
// is instantiated.  Running the code below will cause
// a NullPointerException.
Runnable runnable = null;
runnable.run();

Un semplice test potrebbe aver evitato l'errore come ...

Runnable runnable = null;
...
if (runnable != null)
{   runnable.run(); }

Ci sono momenti in informatica in cui è possibile eseguire 1 o più test prima di eseguire il codice per assicurarsi che tu sia sicuro E SARÀ ANCORA OTTENUTO UN ECCEZIONE. Ad esempio, è possibile testare un file system per assicurarsi che ci sia abbastanza spazio su disco sul disco rigido prima di scrivere i dati nell'unità. In un sistema operativo multiprocessing, come quelli utilizzati oggi, il tuo processo potrebbe testare lo spazio su disco e il file system restituirà un valore che dice che c'è abbastanza spazio, quindi un interruttore di contesto in un altro processo potrebbe scrivere i byte rimanenti disponibili per il funzionamento sistema. Quando il contesto del sistema operativo ritorna al processo in esecuzione in cui scrivi i tuoi contenuti su disco, si verificherà un'eccezione semplicemente perché non c'è abbastanza spazio su disco nel file system.

Considero lo scenario sopra come un caso perfetto per un'eccezione controllata. È un'eccezione nel codice che ti costringe ad affrontare qualcosa di brutto anche se il tuo codice potrebbe essere scritto perfettamente. Se scegli di fare cose cattive come "ingoiare l'eccezione", sei il programmatore cattivo. A proposito, ho trovato casi in cui è ragionevole ingoiare l'eccezione, ma per favore lascia un commento nel codice sul motivo per cui l'eccezione è stata ingerita. Il meccanismo di gestione delle eccezioni non è da incolpare. Di solito scherzo che preferirei che il pacemaker cardiaco fosse scritto con un linguaggio che ha Eccezioni controllate.

Ci sono momenti in cui diventa difficile decidere se il codice è testabile o meno. Ad esempio, se si sta scrivendo un interprete e viene generata una SyntaxException quando il codice non viene eseguito per qualche motivo sintattico, la SyntaxException dovrebbe essere un'eccezione controllata o (in Java) una RuntimeException? Risponderei se l'interprete controlla la sintassi del codice prima dell'esecuzione del codice, quindi l'eccezione dovrebbe essere una RuntimeException. Se l'interprete esegue semplicemente il codice "hot" e fa semplicemente clic su un errore di sintassi, direi che l'eccezione dovrebbe essere un'eccezione controllata.

Devo ammettere che non sono sempre felice di dover prendere o lanciare un'eccezione controllata perché ci sono momenti in cui non sono sicuro di cosa fare. Le eccezioni controllate sono un modo per forzare un programmatore a essere consapevole del potenziale problema che può verificarsi. Uno dei motivi per cui programma in Java è perché ha Eccezioni controllate.

    
risposta data 28.06.2014 - 07:18
fonte
-1

Al momento sono al centro di un progetto / API OOP piuttosto grande e ho utilizzato questo layout delle eccezioni. Ma tutto dipende davvero da quanto in profondità si vuole andare con la gestione delle eccezioni e simili.

ExpectedException
 - AuthorisedException
 - EmptySetException
 - NoRemainingException
 - NoRowsException
 - NotFoundException
 - ValidationException

UnexpectedException
 - ConnectivityException
 - EnvironmentException
 - ProgrammerException
 - SQLException

ESEMPIO

   $valid_types = array('mysql', 'oracle', 'sqlite');
       if (!in_array($type, $valid_types)) {
           throw new ecProgrammerException(
        'The database type specified, %1$s, is invalid. Must be one of: %2$s.',
    $type,
    join(', ', $valid_types)
    );
}
    
risposta data 31.05.2011 - 07:06
fonte

Leggi altre domande sui tag