Avere una bandiera per indicare se dovremmo lanciare errori


64

Di recente ho iniziato a lavorare in un posto con alcuni sviluppatori molto più vecchi (circa 50+ anni). Hanno lavorato su applicazioni critiche che si occupano di aviazione in cui il sistema non potrebbe andare in crash. Di conseguenza il programmatore più anziano tende a programmare in questo modo.

Tende a inserire un booleano negli oggetti per indicare se un'eccezione debba essere lanciata o meno.

Esempio

public class AreaCalculator
{
    AreaCalculator(bool shouldThrowExceptions) { ... }
    CalculateArea(int x, int y)
    {
        if(x < 0 || y < 0)
        {
            if(shouldThrowExceptions) 
                throwException;
            else
                return 0;
        }
    }
}

(Nel nostro progetto il metodo può fallire perché stiamo cercando di utilizzare un dispositivo di rete che al momento non può essere presente. L'esempio di area è solo un esempio del flag di eccezione)

Per me questo sembra un odore di codice. La scrittura di unit test diventa leggermente più complessa poiché è necessario testare ogni volta il flag di eccezione. Inoltre, se qualcosa va storto, non vorresti saperlo subito? Non dovrebbe essere responsabilità del chiamante determinare come continuare?

La sua logica / ragionamento è che il nostro programma deve fare 1 cosa, mostrare i dati all'utente. Qualsiasi altra eccezione che non ci impedisce di farlo dovrebbe essere ignorata. Concordo sul fatto che non dovrebbero essere ignorati, ma dovrebbero fare le bolle ed essere gestiti dalla persona appropriata, e non devono occuparsi delle bandiere per quello.

È un buon modo di gestire le eccezioni?

Modifica : solo per dare più contesto alla decisione di progettazione, sospetto che sia perché se questo componente fallisce, il programma può ancora funzionare e svolgere il suo compito principale. Quindi non vorremmo lanciare un'eccezione (e non gestirla?) E farla smontare il programma quando per l'utente sta funzionando bene

Modifica 2 : per dare ancora più contesto, nel nostro caso viene chiamato il metodo per ripristinare una scheda di rete. Il problema sorge quando la scheda di rete viene disconnessa e ricollegata, gli viene assegnato un indirizzo IP diverso, quindi Reset genererà un'eccezione perché proveremmo a ripristinare l'hardware con il vecchio IP.


22
c # ha una convenzione per questo scalpiccio Try-Parse. ulteriori informazioni: docs.microsoft.com/en-us/dotnet/standard/design-guidelines/… La bandiera non corrisponde a questo modello.
Peter,

18
Questo è fondamentalmente un parametro di controllo e cambia il modo in cui i metodi interni eseguono. Questo è negativo, indipendentemente dallo scenario. martinfowler.com/bliki/FlagArgument.html , softwareengineering.stackexchange.com/questions/147977/… , medium.com/@amlcurran/…
bic

1
Oltre al commento Try-Parse di Peter, ecco un bell'articolo sulle eccezioni di Vexing: blogs.msdn.microsoft.com/ericlippert/2008/09/10/…
Linaith

2
"Quindi non vorremmo lanciare un'eccezione (e non gestirla?) E farla smontare il programma quando per l'utente sta funzionando bene" - sai che puoi catturare le eccezioni, giusto?
user253751

1
Sono abbastanza sicuro che questo sia stato trattato prima da qualche altra parte, ma dato il semplice esempio di Area sarei più propenso a chiedermi da dove verrebbero quei numeri negativi e se potresti essere in grado di gestire quella condizione di errore da qualche altra parte (ad es. qualunque cosa leggesse il file contenente lunghezza e larghezza, per esempio); Tuttavia, "stiamo cercando di utilizzare un dispositivo di rete che al momento non può essere presente". punto potrebbe meritare una risposta totalmente diversa, è questa un'API di terze parti o qualcosa di standard industriale come TCP / UDP?
giovedì

Risposte:


73

Il problema con questo approccio è che mentre le eccezioni non vengono mai generate (e quindi l'applicazione non si arresta in modo anomalo a causa di eccezioni non rilevate), i risultati restituiti non sono necessariamente corretti e l'utente potrebbe non sapere mai che esiste un problema con i dati (o qual è il problema e come correggerlo).

Affinché i risultati siano corretti e significativi, il metodo chiamante deve verificare il risultato per numeri speciali, ovvero valori di ritorno specifici utilizzati per indicare i problemi emersi durante l'esecuzione del metodo. I numeri negativi (o zero) restituiti per quantità definite positive (come l'area) ne sono un esempio lampante nel codice precedente. Se il metodo di chiamata non sa (o dimentica!) Di verificare questi numeri speciali, l'elaborazione può continuare senza mai realizzare un errore. I dati vengono quindi visualizzati all'utente mostrando un'area di 0, che l'utente conosce non è corretta, ma non hanno alcuna indicazione di cosa sia andato storto, dove o perché. Si chiedono quindi se qualcuno degli altri valori è sbagliato ...

Se viene generata l'eccezione, l'elaborazione si interrompe, l'errore (idealmente) verrebbe registrato e l'utente potrebbe essere avvisato in qualche modo. L'utente può quindi correggere qualsiasi errore e riprovare. Una corretta gestione delle eccezioni (e dei test!) Garantirà che le applicazioni critiche non si arrestino in modo anomalo o finiscano in uno stato non valido.


1
@Quirk È impressionante il modo in cui Chen è riuscito a violare il principio della responsabilità singola in sole 3 o 4 righe. Questo è il vero problema. Inoltre, il problema di cui sta parlando (il programmatore non riesce a pensare alle conseguenze degli errori su ciascuna riga) è sempre una possibilità con eccezioni non controllate e solo a volte una possibilità con eccezioni verificate. Penso di aver visto tutti gli argomenti mai fatti contro le eccezioni verificate, e nessuno di essi è valido.
No U

@TKK personalmente ci sono alcuni casi in cui mi sarei imbattuto in eccezioni verificate in .NET. Sarebbe bello se ci fossero alcuni strumenti di analisi statica di fascia alta in grado di assicurarsi che ciò che un'API documenta come le sue eccezioni generate sia accurato, anche se probabilmente sarebbe praticamente impossibile, soprattutto quando si accede alle risorse native.
giovedì

1
@jrh Sì, sarebbe bello se qualcosa includesse un po 'di sicurezza delle eccezioni in .NET, simile a come i kludge di TypeScript digitano la sicurezza in JS.
No U

47

È un buon modo di gestire le eccezioni?

No, penso che questa sia una brutta pratica. Generare un'eccezione rispetto alla restituzione di un valore è un cambiamento fondamentale nell'API, cambiare la firma del metodo e far sì che il metodo si comporti in modo abbastanza diverso dal punto di vista dell'interfaccia.

In generale, quando progettiamo le classi e le loro API, dovremmo considerarlo

  1. potrebbero esserci più istanze della classe con diverse configurazioni fluttuanti nello stesso programma contemporaneamente e,

  2. a causa dell'iniezione di dipendenza e di un numero qualsiasi di altre pratiche di programmazione, un cliente utilizzatore può creare gli oggetti e passarli a un altro utilizzandoli, quindi spesso abbiamo una separazione tra creatori di oggetti e utenti di oggetti.

Considera ora cosa deve fare il chiamante del metodo per utilizzare un'istanza che gli è stata consegnata, ad es. Per chiamare il metodo di calcolo: il chiamante dovrebbe sia controllare che l'area sia zero sia catturare le eccezioni - ahi! Le considerazioni sui test vanno non solo alla classe stessa, ma anche alla gestione degli errori dei chiamanti ...

Dovremmo sempre rendere le cose il più facili possibile per il cliente consumatore; questa configurazione booleana nel costruttore che modifica l'API di un metodo di istanza è l'opposto di far cadere il successo del programmatore client (forse tu o il tuo collega).

Per offrire entrambe le API, sei molto meglio e più normale fornire due classi diverse: una che genera sempre errori e una che restituisce sempre 0 in caso di errore oppure fornisce due metodi diversi con una singola classe. In questo modo il cliente consumatore può facilmente sapere esattamente come verificare e gestire gli errori.

Utilizzando due classi diverse o due metodi diversi, è possibile utilizzare gli IDE per trovare gli utenti del metodo e le caratteristiche del refactor, ecc. Molto più facilmente poiché i due casi d'uso non sono più integrati. Anche la lettura, la scrittura, la manutenzione, le revisioni e i test del codice sono più semplici.


In un'altra nota, personalmente sento che non dovremmo prendere parametri di configurazione booleani, in cui tutti i chiamanti passano semplicemente una costante . Tale parametrizzazione della configurazione comprende due casi d'uso separati senza alcun vantaggio reale.

Dai un'occhiata alla tua base di codice e vedi se una variabile (o espressione non costante) è mai stata usata per il parametro di configurazione booleana nel costruttore! Ne dubito.


E ulteriore considerazione è chiedere perché il calcolo dell'area può fallire. La cosa migliore potrebbe essere quella di lanciare il costruttore, se il calcolo non può essere effettuato. Tuttavia, se non sai se il calcolo può essere effettuato fino a quando l'oggetto non viene ulteriormente inizializzato, allora forse considera l'utilizzo di classi diverse per differenziare quegli stati (non pronto per calcolare l'area rispetto pronto per calcolare l'area).

Ho letto che la tua situazione di fallimento è orientata verso il remoto, quindi potrebbe non essere applicabile; solo qualche spunto di riflessione.


Non dovrebbe essere responsabilità del chiamante determinare come continuare?

Si, sono d'accordo. Sembra prematuro per il chiamante decidere che l'area di 0 è la risposta giusta in condizioni di errore (soprattutto perché 0 è un'area valida, quindi non c'è modo di dire la differenza tra errore e 0 effettivo, anche se potrebbe non essere applicabile alla tua app).


Non è necessario verificare l'eccezione perché è necessario verificare gli argomenti prima di chiamare il metodo. La verifica del risultato con zero non distingue tra gli argomenti legali 0, 0 e quelli negativi illegali. L'API è davvero orribile IMHO.
BlackJack,

Le spinte MS dell'allegato K per gli iostreams C99 e C ++ sono esempi di API in cui un gancio o un flag cambia radicalmente la reazione ai guasti.
Deduplicatore

37

Hanno lavorato su applicazioni critiche che si occupano di aviazione in cui il sistema non potrebbe andare in crash. Di conseguenza ...

Questa è un'introduzione interessante, che mi dà l'impressione che la motivazione dietro questo progetto sia quella di evitare di generare eccezioni in alcuni contesti "perché il sistema potrebbe andare in rovina" . Ma se il sistema "può andare in crash a causa di un'eccezione", ciò indica chiaramente che

  • le eccezioni non sono gestite correttamente , almeno o rigidamente.

Quindi, se il programma che utilizza il AreaCalculatorbuggy, il tuo collega preferisce non avere il programma "crash in anticipo", ma restituire un valore errato (sperando che nessuno se ne accorga o che nessuno fa qualcosa di importante con esso). Questo in realtà sta mascherando un errore e, nella mia esperienza, prima o poi porterà a follow-up di bug per i quali diventa difficile trovare la causa principale.

L'IMHO che scrive un programma che non si arresta in modo anomalo in nessun caso ma mostra dati errati o risultati di calcolo non è in genere migliore di lasciarlo in crash. L'unico approccio giusto è quello di dare al chiamante la possibilità di notare l'errore, affrontarlo, lasciargli decidere se l'utente deve essere informato del comportamento sbagliato e se è sicuro continuare qualsiasi elaborazione o se è più sicuro per interrompere completamente il programma. Pertanto, consiglierei una delle seguenti:

  • rendere difficile trascurare il fatto che una funzione può generare un'eccezione. La documentazione e gli standard di codifica sono i tuoi amici qui e le revisioni periodiche del codice dovrebbero supportare l'uso corretto dei componenti e la corretta gestione delle eccezioni.

  • formare il team ad aspettarsi e gestire le eccezioni quando usano componenti "scatola nera" e hanno in mente il comportamento globale del programma.

  • se per alcuni motivi ritieni di non riuscire a far sì che il codice chiamante (o gli sviluppatori che lo scrivono) utilizzino correttamente la gestione delle eccezioni, quindi, come ultima risorsa, potresti progettare un'API con variabili di output degli errori esplicite e senza eccezioni, come

    CalculateArea(int x, int y, out ErrorCode err)

    quindi diventa davvero difficile per il chiamante trascurare la funzione potrebbe fallire. Ma questo è IMHO estremamente brutto in C #; è una vecchia tecnica di programmazione difensiva di C dove non ci sono eccezioni e normalmente non dovrebbe essere necessario per funzionare al giorno d'oggi.


3
"scrivere un programma che non si schianta in nessun caso ma che mostra dati errati o risultati di calcolo di solito non è in alcun modo meglio che lasciare che il programma si blocchi" Sono pienamente d'accordo in generale, anche se potrei immaginare che probabilmente avrei preferito l'aereo andare ancora con gli strumenti che mostrano valori errati rispetto ad avere un arresto del computer dell'aeroplano. Per tutte le applicazioni meno critiche è sicuramente meglio non mascherare gli errori.
Trilarion

18
@Trilarion: se un programma per computer di volo non contiene una corretta gestione delle eccezioni, "risolvere questo problema" facendo in modo che i componenti non generino eccezioni è un approccio molto fuorviante. Se il programma si arresta in modo anomalo, dovrebbe esserci un sistema di backup ridondante che può subentrare. Se il programma non si arresta in modo anomalo e mostra un'altezza sbagliata, ad esempio, i piloti potrebbero pensare "tutto bene" mentre l'aereo si precipita sulla montagna successiva.
Doc Brown,

7
@Trilarion: se il computer di volo mostra un'altezza sbagliata e l'aereo si schianterà a causa di ciò, non ti aiuterà neanche (soprattutto quando il sistema di backup è lì e non si informa che deve subentrare). I sistemi di backup per computer per aerei non sono una nuova idea, google per "sistemi di backup per computer per aerei", sono abbastanza sicuro che gli ingegneri di tutto il mondo hanno sempre costruito sistemi ridondanti in qualsiasi sistema critico per la vita reale (e se fosse solo per non perdere assicurazione).
Doc Brown,

4
Questo. Se non puoi permetterti che il programma vada in crash, non puoi permetterti di dare silenziosamente risposte sbagliate. La risposta corretta è avere un'adeguata gestione delle eccezioni in tutti i casi. Per un sito Web, ciò significa un gestore globale che converte errori imprevisti in 500. Potresti anche avere gestori aggiuntivi per situazioni più specifiche, come avere un try/ catchall'interno di un ciclo se hai bisogno di elaborazione per continuare se un elemento fallisce.
jpmc26

2
Ottenere il risultato sbagliato è sempre il peggior tipo di fallimento; questo mi ricorda la regola dell'ottimizzazione: "ottenerla corretta prima di ottimizzare, perché ottenere una risposta sbagliata più velocemente non giova a nessuno".
Toby Speight,

13

La scrittura di unit test diventa leggermente più complessa poiché è necessario testare ogni volta il flag di eccezione.

Qualsiasi funzione con n parametri sarà più difficile da testare rispetto a una con n-1 parametri. Estendilo all'assurdo e l'argomento diventa che le funzioni non dovrebbero avere parametri perché ciò li rende più facili da testare.

Sebbene sia un'ottima idea scrivere codice che sia facile da testare, è una pessima idea mettere la semplicità del test sopra la scrittura di codice utile per le persone che devono chiamarlo. Se l'esempio nella domanda ha un interruttore che determina se viene generata o meno un'eccezione, è possibile che i chiamanti che desiderano quel comportamento meritino di aggiungerlo alla funzione. Dove il confine tra bugie complesse e troppo complesse è un appello di giudizio; chiunque cerchi di dirti che esiste una linea brillante che si applica in tutte le situazioni dovrebbe essere guardato con sospetto.

Inoltre, se qualcosa va storto, non vorresti saperlo subito?

Dipende dalla tua definizione di sbagliato. L'esempio nella domanda definisce errato "dato una dimensione inferiore a zero ed shouldThrowExceptionsè vero". Assegnare una dimensione se meno di zero non è sbagliato quando shouldThrowExceptionsè falso perché l'interruttore induce un comportamento diverso. Questa, semplicemente, non è una situazione eccezionale.

Il vero problema qui è che l'interruttore è stato chiamato male perché non è descrittivo di ciò che fa fare la funzione. Se gli fosse stato dato un nome migliore treatInvalidDimensionsAsZero, avresti fatto questa domanda?

Non dovrebbe essere responsabilità del chiamante determinare come continuare?

Il chiamante non determinano il modo per continuare. In questo caso, lo fa in anticipo impostando o cancellando shouldThrowExceptionse la funzione si comporta in base al suo stato.

L'esempio è patologicamente semplice perché esegue un singolo calcolo e restituisce. Se lo rendi leggermente più complesso, come calcolare la somma delle radici quadrate di un elenco di numeri, generare eccezioni può dare ai chiamanti problemi che non possono risolvere. Se passo un elenco di [5, 6, -1, 8, 12]e la funzione genera un'eccezione sopra -1, non ho modo di dire alla funzione di continuare perché sarà già stata interrotta e getta via la somma. Se l'elenco è un enorme set di dati, generare una copia senza numeri negativi prima di chiamare la funzione potrebbe non essere pratico, quindi sono costretto a dire in anticipo come devono essere trattati i numeri non validi, sia sotto forma di "semplicemente ignorarli "cambia o magari fornisci un lambda che viene chiamato per prendere quella decisione.

La sua logica / ragionamento è che il nostro programma deve fare 1 cosa, mostrare i dati all'utente. Qualsiasi altra eccezione che non ci impedisce di farlo dovrebbe essere ignorata. Concordo sul fatto che non dovrebbero essere ignorati, ma dovrebbero fare le bolle ed essere gestiti dalla persona appropriata, e non devono occuparsi delle bandiere per quello.

Ancora una volta, non esiste una soluzione unica per tutti. Nell'esempio, la funzione è stata, presumibilmente, scritta in una specifica che dice come gestire le dimensioni negative. L'ultima cosa che vuoi fare è abbassare il rapporto segnale-rumore dei tuoi log riempiendoli con messaggi che dicono "normalmente, un'eccezione verrebbe lanciata qui, ma il chiamante ha detto di non disturbare".

E come uno di quei programmatori molto più vecchi, ti chiederei che ti allontani gentilmente dal mio prato. ;-)


Concordo sul fatto che la denominazione e l'intenzione sono estremamente importanti e in questo caso il nome del parametro corretto può davvero cambiare le tabelle, quindi +1, ma 1. la our program needs to do 1 thing, show data to user. Any other exception that doesn't stop us from doing so should be ignoredmentalità può portare l'utente a prendere una decisione sulla base di dati errati (perché in realtà il programma deve fare 1 cosa - aiutare l'utente a prendere decisioni informate) 2. casi simili come di bool ExecuteJob(bool throwOnError = false)solito sono estremamente soggetti a errori e portano a codice di cui è difficile ragionare semplicemente leggendolo.
Eugene Podskal

@EugenePodskal Penso che il presupposto sia che "mostra dati" significa "mostra dati corretti". L'interrogante non dice che il prodotto finito non funziona, solo che potrebbe essere scritto "sbagliato". Dovrei vedere alcuni dati concreti sul secondo punto. Ho una manciata di funzioni molto usate nel mio progetto attuale che hanno un interruttore di lancio / non lancio e non sono più difficili da ragionare rispetto a qualsiasi altra funzione, ma questo è un punto dati.
Blrfl,

Buona risposta, penso che questa logica si applichi a una gamma molto più ampia di circostanze rispetto ai soli PO. A proposito, il nuovo di cpp ha una versione di lancio / no-lancio esattamente per questi motivi. So che ci sono alcune piccole differenze ma ...
drjpizzle,

8

Un codice critico e "normale" sulla sicurezza può portare a idee molto diverse su come siano le "buone pratiche". C'è molta sovrapposizione - alcune cose sono rischiose e dovrebbero essere evitate in entrambi - ma ci sono ancora differenze significative. Se aggiungi un requisito per assicurarti di essere reattivo, queste deviazioni diventano piuttosto sostanziali.

Questi spesso riguardano cose che ti aspetteresti:

  • Per git, la risposta sbagliata potrebbe essere molto negativa rispetto a: prendere-a-lungo / interrompere / sospendere o addirittura arrestarsi in modo anomalo (che sono effettivamente non-problemi relativi, ad esempio, alla modifica accidentale del codice del check-in).

    Tuttavia: per un cruscotto che ha un calcolo della forza g in stallo e che impedisce l'esecuzione di un calcolo della velocità dell'aria, potrebbe essere inaccettabile.

Alcuni sono meno ovvi:

  • Se hai testato molto , i risultati del primo ordine (come le risposte giuste) non sono così preoccupanti relativamente parlando. Sai che i tuoi test avranno coperto questo. Tuttavia, se ci fosse stato nascosto o flusso di controllo, non sai che questo non sarà la causa di qualcosa di molto più sottile. Questo è difficile da escludere con i test.

  • Essere dimostrabilmente sicuri è relativamente importante. Non molti clienti si siederanno a ragionare sul fatto che la fonte che stanno acquistando sia sicura o meno. Se invece sei nel mercato dell'aviazione ...

Come si applica al tuo esempio:

Non lo so. Ci sono un certo numero di processi di pensiero che potrebbero avere regole guida come "Nessun lancio nel codice di produzione" adottati in un codice critico per la sicurezza che sarebbe piuttosto sciocco in situazioni più comuni.

Alcuni riguarderanno l'incorporamento, un po 'di sicurezza e forse altri ... Alcuni sono buoni (erano necessari limiti di memoria / prestazioni ristretti) altri sono cattivi (non gestiamo correttamente le eccezioni, quindi è meglio non rischiare). Il più delle volte, anche sapendo perché lo hanno fatto, non risponderà davvero alla domanda. Ad esempio, se ha a che fare con la facilità di controllare il codice più che renderlo effettivamente migliore, è una buona pratica? Non puoi davvero dirlo. Sono animali diversi e devono essere trattati in modo diverso.

Detto questo, mi sembra un po 'sospetto MA :

Probabilmente le decisioni sulla progettazione di software e software critici per la sicurezza non dovrebbero essere prese da estranei nello scambio di stack di ingegneria del software. Potrebbe esserci una buona ragione per farlo anche se fa parte di un cattivo sistema. Non leggere molto in nessuno di questi se non come "spunti di riflessione".


7

A volte lanciare un'eccezione non è il metodo migliore. Non da ultimo a causa dello srotolamento dello stack, ma a volte perché catturare un'eccezione è problematico, in particolare per quanto riguarda il linguaggio o le interfacce.

Un buon modo per gestirlo è restituire un tipo di dati arricchito. Questo tipo di dati ha uno stato sufficiente per descrivere tutti i percorsi felici e tutti i percorsi infelici. Il punto è che se interagisci con questa funzione (membro / globale / altrimenti) sarai costretto a gestire il risultato.

Detto questo, questo tipo di dati arricchito non dovrebbe forzare l'azione. Immagina nella tua zona qualcosa di simile var area_calc = new AreaCalculator(); var volume = area_calc.CalculateArea(x, y) * z;. Sembra utile volumedovrebbe contenere l'area moltiplicata per la profondità - che potrebbe essere un cubo, un cilindro, ecc.

E se il servizio area_calc fosse inattivo? Quindi ha area_calc .CalculateArea(x, y)restituito un tipo di dati avanzato contenente un errore. È legale moltiplicarlo per z? È una buona domanda. È possibile forzare gli utenti a gestire immediatamente il controllo. Ciò tuttavia interrompe la logica con la gestione degli errori.

var area_calc = new AreaCalculator();
var area_result = area_calc.CalculateArea(x, y);
if (area_result.bad())
{
    //handle unhappy path
}
var volume = area_result.value() * z;

vs

var area_calc = new AreaCalculator();
var volume = area_calc.CalculateArea(x, y) * z;
if (volume.bad())
{
    //handle unhappy path
}

La logica essenzialmente è distribuita su due righe e divisa per la gestione degli errori nel primo caso, mentre nel secondo caso è presente tutta la logica pertinente su una riga seguita dalla gestione degli errori.

In quel secondo caso volumeè un tipo di dati avanzato. Non è solo un numero. Ciò rende l'archiviazione più grande e volumedovrà comunque essere esaminata per una condizione di errore. Inoltre volumepotrebbe alimentare altri calcoli prima che l'utente scelga di gestire l'errore, consentendogli di manifestarsi in diverse posizioni diverse. Questo potrebbe essere positivo o negativo a seconda delle specifiche della situazione.

In alternativa volumepotrebbe essere solo un semplice tipo di dati - solo un numero, ma poi cosa succede alla condizione di errore? È possibile che il valore si converta implicitamente se si trova in una condizione felice. Se si trova in una condizione infelice, potrebbe restituire un valore predefinito / di errore (per l'area 0 o -1 potrebbe sembrare ragionevole). In alternativa, potrebbe generare un'eccezione da questo lato dell'interfaccia / lingua.

... foo() {
   var area_calc = new AreaCalculator();
   return area_calc.CalculateArea(x, y) * z;
}
var volume = foo();
if (volume <= 0)
{
    //handle error
}

vs.

... foo() {
   var area_calc = new AreaCalculator();
   return area_calc.CalculateArea(x, y) * z;
}

try { var volume = foo(); }
catch(...)
{
    //handle error
}

Distribuendo un valore negativo o probabilmente negativo, l'utente si impegna molto a convalidare i dati. Questa è una fonte di bug, perché per quanto riguarda il compilatore il valore restituito è un numero intero legittimo. Se qualcosa non è stato verificato, lo scoprirai quando le cose andranno male. Il secondo caso mescola il meglio di entrambi i mondi consentendo alle eccezioni di gestire percorsi infelici, mentre percorsi felici seguono la normale elaborazione. Purtroppo costringe l'utente a gestire le eccezioni con saggezza, il che è difficile.

Giusto per essere chiari, un percorso infelice è un caso sconosciuto alla logica aziendale (il dominio dell'eccezione), non riuscire a convalidare è un percorso felice perché sai come gestirlo dalle regole aziendali (il dominio delle regole).

La soluzione definitiva sarebbe quella che consente tutti gli scenari (entro limiti ragionevoli).

  • L'utente dovrebbe essere in grado di richiedere una cattiva condizione e gestirla immediatamente
  • L'utente dovrebbe essere in grado di operare sul tipo arricchito come se il percorso felice fosse stato seguito e propagare i dettagli dell'errore.
  • L'utente dovrebbe essere in grado di estrarre il valore del percorso felice attraverso il cast (implicito / esplicito come ragionevole), generando un'eccezione per i percorsi infelici.
  • L'utente dovrebbe essere in grado di estrarre il valore del percorso felice o utilizzare un valore predefinito (fornito o meno)

Qualcosa di simile a:

Rich::value_type value_or_default(Rich&, Rich::value_type default_value = ...);
bool bad(Rich&);
...unhappy path report... bad_state(Rich&);
Rich& assert_not_bad(Rich&);
class Rich
{
public:
   typedef ... value_type;

   operator value_type() { assert_not_bad(*this); return ...value...; }
   operator X(...) { if (bad(*this)) return ...propagate badness to new value...; /*operate and generate new value*/; }
}

//check
if (bad(x))
{
    var report = bad_state(x);
    //handle error
}

//rethrow
assert_not_bad(x);
var result = (assert_not_bad(x) + 23) / 45;

//propogate
var y = x * 23;

//implicit throw
Rich::value_type val = x;
var val = ((Rich::value_type)x) + 34;
var val2 = static_cast<Rich::value_type>(x) % 3;

//default value
var defaulted = value_or_default(x);
var defaulted_to = value_or_default(x, 55);

@TobySpeight Abbastanza giusto, queste cose sono sensibili al contesto e hanno la loro portata.
Kain0_0

Penso che il problema qui sia i blocchi 'assert_not_bad'. Penso che questi finiranno come nello stesso posto in cui il codice originale ha cercato di risolvere. Nei test, questi devono essere notati, tuttavia, se sono davvero affermazioni, dovrebbero essere messi a nudo prima della produzione su un aereo reale. altrimenti alcuni ottimi punti.
drjpizzle,

@drjpizzle Direi che se fosse abbastanza importante aggiungere una protezione per i test, è abbastanza importante lasciare la protezione in posizione durante l'esecuzione in produzione. La presenza della guardia stessa implica dei dubbi. Se dubiti del codice sufficiente per proteggerlo durante i test, ne dubiti per un motivo tecnico. cioè la condizione potrebbe / si verifica realisticamente. L'esecuzione dei test non dimostra che la condizione non sarà mai raggiunta in produzione. Ciò significa che esiste una condizione nota, che potrebbe verificarsi, che deve essere gestita da qualche parte in qualche modo. Penso che il problema sia come viene gestito.
Kain0_0

3

Risponderò dal punto di vista del C ++. Sono abbastanza sicuro che tutti i concetti chiave siano trasferibili su C #.

Sembra che il tuo stile preferito sia "getta sempre eccezioni":

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        throw Exception("negative side lengths");
    }
    return x * y;
}

Questo può essere un problema per il codice C ++ perché la gestione delle eccezioni è pesante : fa sì che il caso di errore venga eseguito lentamente e il caso di errore alloca memoria (che a volte non è nemmeno disponibile) e generalmente rende le cose meno prevedibili. La pesantezza di EH è uno dei motivi per cui senti persone dire cose come "Non usare eccezioni per il flusso di controllo".

Quindi alcune librerie (come <filesystem>) usano ciò che C ++ chiama una "doppia API" o ciò che C # chiama il Try-Parsemodello (grazie Peter per il suggerimento!)

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        throw Exception("negative side lengths");
    }
    return x * y;
}

bool TryCalculateArea(int x, int y, int& result) {
    if (x < 0 || y < 0) {
        return false;
    }
    result = x * y;
    return true;
}

int a1 = CalculateArea(x, y);
int a2;
if (TryCalculateArea(x, y, a2)) {
    // use a2
}

Puoi vedere subito il problema con le "doppie API": un sacco di duplicazione del codice, nessuna guida per gli utenti su quale API sia quella "giusta" da usare e l'utente deve fare una scelta difficile tra utili messaggi di errore ( CalculateArea) e speed ( TryCalculateArea) perché la versione più veloce prende la nostra utile "negative side lengths"eccezione e la appiattisce in un inutile false- "qualcosa è andato storto, non chiedermi cosa o dove". (Alcune API doppie utilizzano un tipo di errore più espressivo, come int errnoo C ++ std::error_code, ma ciò non ti dice ancora dove si è verificato l'errore - solo che si è verificato da qualche parte.)

Se non riesci a decidere come dovrebbe comportarsi il tuo codice, puoi sempre dare una decisione al chiamante!

template<class F>
int CalculateArea(int x, int y, F errorCallback) {
    if (x < 0 || y < 0) {
        return errorCallback(x, y, "negative side lengths");
    }
    return x * y;
}

int a1 = CalculateArea(x, y, [](auto...) { return 0; });
int a2 = CalculateArea(x, y, [](int, int, auto msg) { throw Exception(msg); });
int a3 = CalculateArea(x, y, [](int, int, auto) { return x * y; });

Questo è essenzialmente ciò che il tuo collega sta facendo; tranne che sta prendendo in considerazione il "gestore degli errori" in una variabile globale:

std::function<int(const char *)> g_errorCallback;

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        return g_errorCallback("negative side lengths");
    }
    return x * y;
}

g_errorCallback = [](auto) { return 0; };
int a1 = CalculateArea(x, y);
g_errorCallback = [](const char *msg) { throw Exception(msg); };
int a2 = CalculateArea(x, y);

Spostare parametri importanti da parametri di funzioni esplicite nello stato globale è quasi sempre una cattiva idea. Non te lo consiglio. (Il fatto che non sia uno stato globale nel tuo caso, ma semplicemente uno stato membro a livello di istanza mitiga un po 'la cattiveria, ma non molto.)

Inoltre, il collega limita inutilmente il numero di possibili comportamenti di gestione degli errori. Invece di consentire qualsiasi lambda di gestione degli errori, ha deciso solo due:

bool g_errorViaException;

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        return g_errorViaException ? throw Exception("negative side lengths") : 0;
    }
    return x * y;
}

g_errorViaException = false;
int a1 = CalculateArea(x, y);
g_errorViaException = true;
int a2 = CalculateArea(x, y);

Questo è probabilmente il "punto debole" di una di queste possibili strategie. Hai tolto tutta la flessibilità all'utente finale costringendolo a utilizzare esattamente uno dei due callback di gestione degli errori esattamente ; e hai tutti i problemi di stato globale condiviso; e stai ancora pagando per quel ramo condizionale ovunque.

Infine, una soluzione comune in C ++ (o in qualsiasi linguaggio con compilazione condizionale) sarebbe quella di costringere l'utente a prendere la decisione per l'intero programma, a livello globale, al momento della compilazione, in modo che il codepath non preso possa essere completamente ottimizzato:

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
#ifdef NEXCEPTIONS
        return 0;
#else
        throw Exception("negative side lengths");
#endif
    }
    return x * y;
}

// Now these two function calls *must* have the same behavior,
// which is a nice property for a program to have.
// Improves understandability.
//
int a1 = CalculateArea(x, y);
int a2 = CalculateArea(x, y);

Un esempio di qualcosa che funziona in questo modo è la assertmacro in C e C ++, che condiziona il suo comportamento sulla macro del preprocessore NDEBUG.


Se si restituisce invece un std::optionalda TryCalculateArea(), è semplice unificare l'implementazione di entrambe le parti della doppia interfaccia in un unico modello-funzione con un flag di compilazione-tempo.
Deduplicatore

@Deduplicatore: forse con a std::expected. Con solo std::optional, a meno che non fraintenda la tua soluzione proposta, soffrirebbe comunque di quello che ho detto: l'utente deve fare una scelta difficile tra messaggi di errore utili e velocità, perché la versione più veloce prende la nostra utile "negative side lengths"eccezione e la appiattisce in un inutile false- " qualcosa è andato storto, non chiedermi cosa o dove ".
Quuxplusone

Questo è il motivo per cui libc ++ <filesystem> in realtà fa qualcosa di molto vicino al modello del collega di OP: std::error_code *ecpassa attraverso tutti i livelli dell'API, e poi in fondo fa l'equivalente morale di if (ec == nullptr) throw something; else *ec = some error code. ( ifErrorHandler
Estrae

Bene, sarebbe un'opzione per mantenere le informazioni di errore estese senza lanciare. Può essere appropriato o non vale il potenziale costo aggiuntivo.
Deduplicatore

1
Così tanti buoni pensieri contenuti in questa risposta ... Sicuramente hanno bisogno di più voti :-)
cmaster

1

Sento che dovrebbe essere menzionato da dove il tuo collega ha tratto il suo modello.

Al giorno d'oggi, C # ha il modello TryGet public bool TryThing(out result). Ciò ti consente di ottenere il tuo risultato, pur facendoti sapere se quel risultato è anche un valore valido. (Ad esempio, tutti i intvalori sono risultati validi per Math.sum(int, int), ma se il valore dovesse traboccare, questo particolare risultato potrebbe essere immondizia). Questo è un modello relativamente nuovo però.

Prima della outparola chiave, dovevi o lanciare un'eccezione (costosa e il chiamante deve catturarla o uccidere l'intero programma), creare una struttura speciale (la classe prima della classe o i generici erano davvero una cosa) per ogni risultato per rappresentare il valore e possibili errori (tempo necessario per creare e gonfiare il software) o restituire un valore di "errore" predefinito (che potrebbe non essere stato un errore).

L'approccio utilizzato dal collega fornisce loro il vantaggio iniziale di eccezioni durante il test / debug di nuove funzionalità, fornendo loro la sicurezza e le prestazioni di runtime (le prestazioni erano un problema critico per tutto il tempo ~ 30 anni fa) di restituire un valore di errore predefinito. Ora questo è lo schema in cui è stato scritto il software e lo schema previsto che va avanti, quindi è naturale continuare a farlo in questo modo anche se ora ci sono modi migliori. Molto probabilmente questo modello è stato ereditato dall'età del software, o un modello che i tuoi college non hanno mai sviluppato (le vecchie abitudini sono difficili da rompere).

Le altre risposte coprono già il motivo per cui questa è considerata una cattiva pratica, quindi finirò solo per consigliarti di leggere sul modello TryGet (forse anche l'incapsulamento per ciò che promette un oggetto dovrebbe fare al suo chiamante).


Prima della outparola chiave, scrivevi una boolfunzione che porta un puntatore al risultato, cioè un refparametro. Potresti farlo in VB6 nel 1998. La outparola chiave semplicemente acquista la certezza in fase di compilazione che il parametro è assegnato quando la funzione ritorna, questo è tutto quello che c'è da fare. Si tratta di un modello bello e utile però.
Mathieu Guindon,

@MathieuGuindon Yeay, ma Get Try non era ancora uno schema ben noto / consolidato, e anche se lo fosse, non sono del tutto sicuro che sarebbe stato usato. Dopotutto, parte del vantaggio fino a Y2K era che la memorizzazione di qualcosa di più grande di 0-99 era inaccettabile.
Tezra

0

Ci sono momenti in cui vuoi seguire il suo approccio, ma non li considero una situazione "normale". La chiave per determinare in quale caso ti trovi è:

La sua logica / ragionamento è che il nostro programma deve fare 1 cosa, mostrare i dati all'utente. Qualsiasi altra eccezione che non ci impedisce di farlo dovrebbe essere ignorata.

Verifica i requisiti. Se i tuoi requisiti in realtà dicono che hai un lavoro, che è quello di mostrare i dati all'utente, allora ha ragione. Tuttavia, nella mia esperienza, la maggior parte delle volte l'utente si preoccupa anche dei dati mostrati. Vogliono i dati corretti. Alcuni sistemi vogliono semplicemente fallire silenziosamente e lasciare che un utente capisca che qualcosa è andato storto, ma li considererei l'eccezione alla regola.

La domanda chiave che vorrei porre dopo un errore è "Il sistema è in uno stato in cui le aspettative dell'utente e gli invarianti del software sono valide?" Se è così, allora semplicemente ritorna e continua. In pratica questo non è ciò che accade nella maggior parte dei programmi.

Per quanto riguarda la bandiera stessa, la bandiera delle eccezioni è generalmente considerata odore di codice perché un utente deve in qualche modo sapere quale modalità è il modulo per capire come funziona la funzione. Se è in !shouldThrowExceptionsmodalità, l'utente deve sapere di essere responsabile del rilevamento degli errori e del mantenimento delle aspettative e degli invarianti quando si verificano. Sono anche responsabili proprio allora e là, sulla linea in cui viene chiamata la funzione. Una bandiera come questa è in genere molto confusa.

Tuttavia, succede. Considera che molti processori consentono di modificare il comportamento in virgola mobile all'interno del programma. Un programma che desidera avere standard più rilassati può farlo semplicemente cambiando un registro (che è effettivamente una bandiera). Il trucco è che dovresti essere molto cauto, per evitare di calpestare accidentalmente gli altri piedi. Il codice controlla spesso il flag corrente, lo imposta sull'impostazione desiderata, esegue le operazioni, quindi lo ripristina. In questo modo nessuno viene sorpreso dal cambiamento.


0

Questo esempio specifico ha una caratteristica interessante che può influenzare le regole ...

CalculateArea(int x, int y)
{
    if(x < 0 || y < 0)
    {
        if(shouldThrowExceptions) 
            throwException;
        else
            return 0;
    }
}

Quello che vedo qui è un controllo preliminare . Un controllo delle condizioni preliminari fallito implica un errore più in alto nello stack di chiamate. Quindi, la domanda diventa: questo codice è responsabile della segnalazione di bug situati altrove?

Parte della tensione qui è attribuibile al fatto che questa interfaccia mostra un'ossessione primitiva - xe ypresumibilmente si suppone rappresenti reali misurazioni della lunghezza. In un contesto di programmazione in cui i tipi specifici di domini sono una scelta ragionevole, in effetti spostiamo il controllo dei presupposti più vicino alla fonte dei dati - in altre parole, aumentiamo la responsabilità dell'integrità dei dati nello stack di chiamate, dove abbiamo un migliore senso per il contesto.

Detto questo, non vedo nulla di fondamentalmente sbagliato nell'avere due diverse strategie per gestire un controllo fallito. La mia preferenza sarebbe quella di usare la composizione per determinare quale strategia è in uso; il flag di funzionalità verrebbe utilizzato nella radice della composizione, piuttosto che nell'implementazione del metodo della libreria.

// Configurable dependencies
AreaCalculator(PreconditionFailureStrategy strategy)

CalculateArea(int x, int y)
{
    if (x < 0 || y < 0) {
        return this.strategy.fail(0);
    }
    // ...
}

Hanno lavorato su applicazioni critiche che si occupano di aviazione in cui il sistema non potrebbe andare in crash.

Il National Traffic and Safety Board è davvero buono; Potrei suggerire tecniche di implementazione alternative ai graybeards, ma non sono propenso a discutere con loro sulla progettazione di paratie nel sottosistema di segnalazione degli errori.

Più in generale: qual è il costo per l'azienda? Arrestare un sito web è molto più economico di quanto non sia un sistema critico per la vita.


Mi piace il suggerimento di un modo alternativo per mantenere la flessibilità desiderata pur continuando a modernizzare.
drjpizzle,

-1

I metodi gestiscono le eccezioni o no, non sono necessarie bandiere in linguaggi come C #.

public int Method1()
{
  ...code

 return 0;
}

Se qualcosa va storto nel ... codice allora quell'eccezione dovrà essere gestita dal chiamante. Se nessuno gestisce l'errore, il programma verrà chiuso.

public int Method1()
{
try {  
...code
}
catch {}
 ...Handle error 
}
return 0;
}

In questo caso, se succede qualcosa di brutto nel codice ..., Method1 sta gestendo il problema e il programma dovrebbe procedere.

Dove gestisci le eccezioni dipende da te. Certamente puoi ignorarli catturando e senza fare nulla. Tuttavia, vorrei assicurarmi che stai ignorando solo alcuni tipi specifici di eccezioni che puoi aspettarti che si verifichino. Ignorare ( exception ex) è pericoloso perché alcune eccezioni che non si desidera ignorare come eccezioni di sistema relative alla memoria insufficiente e simili.


3
L'attuale configurazione pubblicata da OP riguarda la decisione se gettare volontariamente un'eccezione. Il codice di OP non porta alla deglutizione indesiderata di cose come eccezioni di memoria insufficiente. Semmai, l'affermazione secondo cui le eccezioni si arrestano in modo anomalo nel sistema implica che la base di codice non rileva le eccezioni e quindi non ingerisce alcuna eccezione; entrambi quelli che erano e non erano stati lanciati intenzionalmente dalla logica commerciale di OP.
Flater

-1

Questo approccio rompe la filosofia "fall fast fast, fail hard".

Perché vuoi fallire velocemente:

  • Più velocemente fallisci, più il sintomo visibile del fallimento si avvicina alla causa effettiva del fallimento. Questo rende il debug molto più semplice: nel migliore dei casi, hai la riga di errore proprio nella prima riga della traccia dello stack.
  • Più velocemente si fallisce (e si rileva l'errore in modo appropriato), meno è probabile che si confonda il resto del programma.
  • Più è difficile fallire (ovvero, generare un'eccezione invece di restituire un codice "-1" o qualcosa del genere), più è probabile che il chiamante si preoccupi effettivamente dell'errore e non continui a lavorare con valori errati.

Svantaggi di non fallire veloce e difficile:

  • Se eviti guasti visibili, cioè fai finta che tutto vada bene, tendi a rendere incredibilmente difficile trovare l'errore reale. Immagina che il valore di ritorno del tuo esempio faccia parte di una routine che calcola la somma di 100 aree; cioè, chiamando quella funzione 100 volte e sommando i valori di ritorno. Se si elimina silenziosamente l'errore, non è possibile trovare dove si verifica l'errore effettivo; e tutti i calcoli seguenti saranno silenziosamente errati.
  • Se ritardi il fallimento (restituendo un valore di ritorno impossibile come "-1" per un'area), aumenti la probabilità che il chiamante della tua funzione non si preoccupi e si dimentichi di gestire l'errore; anche se hanno le informazioni sull'errore a portata di mano.

Infine, l'effettiva gestione degli errori basata su eccezioni ha il vantaggio di poter fornire un messaggio o oggetto di errore "fuori banda", è possibile agganciare facilmente la registrazione di errori, avvisi, ecc. Senza scrivere una singola riga aggiuntiva nel codice del dominio .

Quindi, non ci sono solo ragioni tecniche semplici, ma anche ragioni "di sistema" che rendono molto utile fallire rapidamente.

Alla fine della giornata, non fallire duro e veloce nei nostri tempi attuali, dove la gestione delle eccezioni è leggera e molto stabile è solo a metà criminale. Capisco perfettamente da dove provenga il pensiero che è buono per sopprimere le eccezioni, ma non è più applicabile.

Soprattutto nel tuo caso particolare, in cui dai persino un'opzione sull'opportunità o meno di fare eccezioni: ciò significa che il chiamante deve decidere comunque . Quindi non c'è alcun inconveniente per far sì che il chiamante prenda l'eccezione e la gestisca in modo appropriato.

Un punto che emerge in un commento:

È stato sottolineato in altre risposte che il fallimento rapido e difficile non è desiderabile in un'applicazione critica quando l'hardware su cui si esegue è un aeroplano.

Fallire velocemente e duramente non significa che l'intera applicazione si arresti in modo anomalo. Significa che nel punto in cui si verifica l'errore, localmente non riesce. Nell'esempio del PO, il metodo di basso livello che calcola un'area non dovrebbe sostituire silenziosamente un errore con un valore errato. Dovrebbe fallire chiaramente.

Qualcuno che chiama la catena ovviamente deve catturare quell'errore / eccezione e gestirlo in modo appropriato. Se questo metodo è stato utilizzato in un aereo, ciò dovrebbe probabilmente portare all'accensione di un LED di errore o almeno a visualizzare "area di calcolo dell'errore" invece di un'area errata.


3
È stato sottolineato in altre risposte che il fallimento rapido e difficile non è desiderabile in un'applicazione critica quando l'hardware su cui si esegue è un aeroplano.
HAEM

1
@HAEM, allora questo è un fraintendimento su cosa significhi fallire velocemente e duramente. Ho aggiunto un paragrafo su questo alla risposta.
AnoE

Anche se l'intenzione non è quella di fallire "così duramente", è comunque considerato rischioso giocare con quel tipo di fuoco.
drjpizzle,

Questo è il punto, @drjpizzle. Se sei abituato a fallire rapidamente, nella mia esperienza non è "rischioso" o "giocare con quel tipo di fuoco". Al contrario. Significa che ti abitui a pensare "cosa succederebbe se qui ottenessi un'eccezione", dove "qui" significa ovunque , e tendi a essere consapevole se il punto in cui stai programmando avrà un grosso problema ( incidente aereo, qualunque cosa in quel caso). Giocare con il fuoco significherebbe aspettarsi che tutto vada bene, e ogni componente, nel dubbio, finge che tutto
vada
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.