Come devo gestire input utente non valido?


12

Ho pensato a questo problema per un po 'e sarei curioso di avere opinioni da altri sviluppatori.

Tendo ad avere uno stile di programmazione molto difensivo. Il mio blocco o metodo tipico è simile al seguente:

T foo(par1, par2, par3, ...)
{
    // Check that all parameters are correct, return undefined (null)
    // or throw exception if this is not the case.

    // Compute and (possibly) return result.
}

Inoltre, durante il calcolo, controllo tutti i puntatori prima di dereferenziarli. La mia idea è che, se c'è qualche bug e qualche puntatore NULL dovrebbe apparire da qualche parte, il mio programma dovrebbe gestirlo bene e semplicemente rifiutare di continuare il calcolo. Naturalmente può notificare il problema con un messaggio di errore nel registro o qualche altro meccanismo.

Per dirla in modo più astratto, il mio approccio è

if all input is OK --> compute result
else               --> do not compute result, notify problem

Altri sviluppatori, tra cui alcuni miei colleghi, usano un'altra strategia. Ad esempio, non controllano i puntatori. Presumono che un pezzo di codice debba ricevere un input corretto e non dovrebbe essere responsabile di ciò che accade se l'input è errato. Inoltre, se un'eccezione del puntatore NULL arresta in modo anomalo il programma, verrà trovato più facilmente un bug durante il test e avrai maggiori possibilità di essere risolto.

La mia risposta è normalmente: ma cosa succede se il bug non viene rilevato durante il test e appare quando il prodotto è già utilizzato dal cliente? Qual è il modo preferito in cui il bug si manifesta? Dovrebbe essere un programma che non esegue una determinata azione, ma può comunque continuare a funzionare o un programma che si arresta in modo anomalo e deve essere riavviato?

Riassumendo

Quale dei due approcci alla gestione di input errati consiglieresti?

Inconsistent input --> no action + notification

o

Inconsistent input --> undefined behaviour or crash

modificare

Grazie per le risposte e i suggerimenti. Sono un fan del design anche per contratto. Ma anche se mi fido della persona che ha scritto il codice chiamando i miei metodi (forse sono io), ci possono essere ancora dei bug, che portano a input sbagliati. Quindi il mio approccio è quello di non dare per scontato che un metodo abbia passato l'input corretto.

Inoltre, utilizzerei un meccanismo per rilevare il problema e informarlo. Su un sistema di sviluppo, ad esempio, si aprirà una finestra di dialogo per avvisare l'utente. In un sistema di produzione scriverebbe solo alcune informazioni nel registro. Non credo che controlli extra possano portare a problemi di prestazioni. Non sono sicuro che le affermazioni siano sufficienti, se sono disattivate in un sistema di produzione: forse si verificherà una situazione nella produzione che non si era verificata durante i test.

Ad ogni modo, sono rimasto davvero sorpreso dal fatto che molte persone seguano l'approccio opposto: hanno lasciato che l'applicazione si arrestasse in modo "intenzionale" perché sostengono che ciò faciliterà la ricerca di bug durante i test.


Codifica sempre in modo difensivo. Alla fine, per motivi di prestazioni, è possibile inserire un interruttore per disabilitare alcuni test in modalità di rilascio.
deadalnix,

Oggi ho corretto un bug relativo a un controllo puntatore NULL mancante. Alcuni oggetti sono stati creati durante il logout dell'applicazione e il costruttore ha utilizzato un getter per accedere ad altri oggetti che non c'erano più. L'oggetto non doveva essere creato a quel punto. È stato creato a causa di un altro bug: alcuni timer non sono stati arrestati durante la disconnessione -> è stato inviato un segnale -> il destinatario ha tentato di creare un oggetto -> il costruttore ha richiesto e utilizzato un altro oggetto -> puntatore NULL -> crash ). Non mi piacerebbe davvero che una situazione così brutta causasse un arresto anomalo della mia applicazione.
Giorgio,

1
Regola di riparazione: quando devi fallire, fallisci rumorosamente e il prima possibile.
deadalnix,

"Regola di riparazione: quando devi fallire, fallisci rumorosamente e il più presto possibile.": Immagino che tutti quei BSOD di Windows siano un'applicazione di questa regola. :-)
Giorgio,

Risposte:


8

Hai capito bene. Sii paranoico. Non fidarti di altro codice, anche se è il tuo codice. Dimentichi le cose, apporti modifiche, il codice si evolve. Non fidarti del codice esterno.

Un buon punto sopra è stato sottolineato: cosa succede se gli input non sono validi ma il programma non si arresta in modo anomalo? Quindi ottieni immondizia nel database ed errori lungo la linea.

Quando viene richiesto un numero (ad es. Prezzo in dollari o numero di unità), mi piace inserire "1e9" e vedere cosa fa il codice. Può succedere.

Quattro decenni fa, ottenendo il mio BS in Informatica da UCBerkeley, ci hanno detto che un buon programma è la gestione degli errori del 50%. Sii paranoico.


Sì, IMHO questa è una delle poche situazioni in cui essere paranoici è una caratteristica e non un problema.
Giorgio,

"Che cosa succede se gli input non sono validi ma il programma non si arresta in modo anomalo? Quindi si ottiene immondizia nel database ed errori lungo la linea.": Invece di arrestare in modo anomalo il programma potrebbe rifiutare di eseguire l'operazione e restituire un risultato indefinito. Undefined si propagherà attraverso il calcolo e non verrà prodotta immondizia. Ma il programma non ha bisogno di crash per raggiungere questo obiettivo.
Giorgio,

Sì, ma - il mio punto è che il programma deve RILEVARE l'input non valido e farcela. Se l'input non viene controllato, funzionerà nel sistema e le cose brutte verranno dopo. Anche schiantarsi è meglio di così!
Andy Canfield,

Sono totalmente d'accordo con te: il mio metodo o funzione tipica inizia con una sequenza di controlli per assicurarsi che i dati di input siano corretti.
Giorgio,

Oggi ho avuto nuovamente la conferma che la strategia "controlla tutto, non fidarti di niente" è spesso una buona idea. Un mio collega aveva un'eccezione puntatore NULL a causa di un controllo mancante. Si è scoperto che in quel contesto era corretto avere un puntatore NULL perché alcuni dati non erano stati caricati ed era corretto controllare il puntatore e semplicemente non fare nulla quando era NULL. :-)
Giorgio,

7

Hai già l'idea giusta

Quale dei due approcci alla gestione di input errati consiglieresti?

Input incoerente -> nessuna azione + notifica

o meglio

Input incoerente -> azione gestita in modo appropriato

Non puoi davvero adottare un approccio di cookie cutter alla programmazione (potresti) ma finiresti con un design formulaico che fa le cose per abitudine piuttosto che per scelta consapevole.

Dogmatismo di temperamento con pragmatismo.

Steve McConnell l'ha detto meglio

Steve McConnell ha praticamente scritto il libro ( Code Complete ) sulla programmazione difensiva e questo è stato uno dei metodi che ha consigliato di convalidare sempre i tuoi input.

Non ricordo se Steve lo abbia menzionato, tuttavia dovresti considerare di farlo per metodi e funzioni non privati , e solo altri dove ritenuto necessario.


2
Invece del pubblico, suggerirei tutti i metodi non privati ​​per coprire in modo difensivo le lingue che hanno protetto, condiviso o nessun concetto di restrizione dell'accesso (tutto è pubblico, implicitamente).
Giustino

3

Non esiste una risposta "corretta" qui, in particolare senza specificare la lingua, il tipo di codice e il tipo di prodotto in cui il codice potrebbe andare. Ritenere:

  • La lingua conta. In Objective-C, spesso va bene inviare messaggi a zero; non succede nulla, ma neanche il programma si arresta in modo anomalo. Java non ha puntatori espliciti, quindi i puntatori zero non sono una grande preoccupazione lì. In C, devi stare un po 'più attento.

  • Essere paranoici significa sospetto o diffidenza irragionevoli, ingiustificati. Probabilmente non è meglio per il software di quanto non lo sia per le persone.

  • Il livello di preoccupazione dovrebbe essere commisurato al livello di rischio nel codice e alla probabile difficoltà di identificare eventuali problemi che si presentano. Cosa succede nel caso peggiore? L'utente riavvia il programma e continua da dove era stato interrotto? La compagnia perde milioni di dollari?

  • Non è sempre possibile identificare input errati. Puoi confrontare religiosamente i tuoi puntatori con zero, ma questo cattura solo uno dei 2 ^ 32 possibili valori, quasi tutti cattivi.

  • Esistono molti meccanismi diversi per gestire gli errori. Ancora una volta, dipende in una certa misura dalla lingua. È possibile utilizzare macro di asserzione, istruzioni condizionali, unit test, gestione delle eccezioni, progettazione accurata e altre tecniche. Nessuno di questi è infallibile e nessuno è appropriato per ogni situazione.

Quindi, per lo più si riduce a dove si vuole mettere la responsabilità. Se stai scrivendo una libreria per l'utilizzo da parte di altri, probabilmente vuoi essere il più attento possibile agli input che ricevi e fai del tuo meglio per emettere errori utili quando possibile. Nelle tue funzioni e metodi privati, potresti usare assert per catturare errori sciocchi, ma in caso contrario dare la responsabilità al chiamante (che sei tu) di non passare immondizia.


+1 - Buona risposta. La mia principale preoccupazione è che un input errato possa causare un problema che appare su un sistema di produzione (quando è troppo tardi per fare qualcosa al riguardo). Naturalmente, penso che tu abbia perfettamente ragione nel dire che dipende dal danno che un problema può causare all'utente.
Giorgio,

La lingua gioca un ruolo importante. In PHP, metà del codice del metodo finisce per verificare il tipo di variabile e intraprendere l'azione appropriata. In Java se il metodo accetta un int non puoi passarlo nient'altro, quindi il tuo metodo diventa più chiaro.
cap.

1

Dovrebbe esserci sicuramente una notifica, come un'eccezione generata. Serve come un avvertimento per gli altri programmatori che potrebbero usare in modo improprio il codice che hai scritto (cercando di usarlo per qualcosa che non era destinato a fare) che il loro input non è valido o provoca errori. Questo è molto utile per rintracciare gli errori, mentre se si restituisce semplicemente null, il loro codice continuerà fino a quando non tentano di utilizzare il risultato e ottenere un'eccezione da un codice diverso.

Se il tuo codice rileva un errore durante una chiamata ad un altro codice (forse un aggiornamento del database non riuscito) che va oltre lo scopo di quel particolare pezzo di codice, non hai davvero alcun controllo su di esso e l'unica soluzione è lanciare un'eccezione che spiega cosa sai (solo quello che ti viene detto dal codice che hai chiamato). Se sai che determinati input porteranno inevitabilmente a un tale risultato, non puoi preoccuparti di eseguire il tuo codice e lanciare un'eccezione affermando che l'input non è valido e perché.

Su una nota più relativa all'utente finale, è meglio restituire qualcosa di descrittivo ma semplice in modo che chiunque possa capirlo. Se il tuo client chiama e dice "il programma si è bloccato, risolvilo", hai molto lavoro a portata di mano per rintracciare cosa è andato storto e perché, e sperando che tu possa riprodurre il problema. L'uso corretto delle eccezioni può non solo prevenire un arresto anomalo, ma fornire informazioni preziose. Una chiamata di un client che dice "Il programma mi sta dando un errore. Dice 'XYZ non è un input valido per il metodo M, perché Z è troppo grande", o qualcosa del genere, anche se non hanno idea di cosa significhi, tu sapere esattamente dove cercare. Inoltre, a seconda delle pratiche commerciali della tua / tua azienda, potrebbe non essere nemmeno tu a risolvere questi problemi, quindi è meglio lasciare loro una buona mappa.

Quindi la versione breve della mia risposta è che la tua prima opzione è la migliore.

Inconsistent input -> no action + notify caller

1

Ho lottato con questo stesso problema durante una lezione universitaria in programmazione. Mi sono sporto verso il lato paranoico e tendo a controllare tutto, ma mi è stato detto che si trattava di un comportamento fuorviante.

Ci veniva insegnato "Design by contract". L'enfasi è che precondizioni, invarianti e post-condizioni devono essere specificati nei commenti e nei documenti di progettazione. Come persona che implementa la mia parte del codice, dovrei avere fiducia nell'architetto del software e potenziarlo seguendo le specifiche che includano i presupposti (quali input i miei metodi devono essere in grado di gestire e quali input non verranno inviati) . Un controllo eccessivo in ogni chiamata di metodo provoca un gonfiamento.

Le asserzioni devono essere utilizzate durante le iterazioni di compilazione per verificare la correttezza del programma (convalida delle condizioni preliminari, invarianti, condizioni post). Le asserzioni verrebbero quindi respinte nella compilazione della produzione.


0

L'uso di "asserzioni" è la strada da percorrere per informare gli altri sviluppatori che stanno sbagliando, solo con metodi "privati" . Abilitarli / disabilitarli è solo un flag da aggiungere / rimuovere in fase di compilazione e come tale è facile rimuovere le asserzioni dal codice di produzione. Ci sono anche un ottimo strumento per sapere se si sono in qualche modo facendo male nei vostri metodi.

Per quanto riguarda la verifica dei parametri di input all'interno di metodi pubblici / protetti, preferisco lavorare sulla difensiva e controllare i parametri e lanciare InvalidArgumentException o simili. Ecco perché ci sono qui per. Dipende anche dal fatto che tu stia scrivendo o meno un'API. Se si tratta di un'API, e ancora di più se è di origine chiusa, convalidare meglio tutto in modo che gli sviluppatori sappiano esattamente cosa è andato storto. Altrimenti, se la fonte è disponibile per altri sviluppatori, non è bianco / nero. Sii coerente con le tue scelte.

Modifica: solo per aggiungere che se guardi ad esempio Oracle JDK, vedrai che non controllano mai "null" e lasciano che il codice si blocchi. Dal momento che genererà comunque una NullPointerException, perché preoccuparsi di controllare null e generare un'eccezione esplicita. Immagino abbia un senso.


In Java si ottiene un'eccezione puntatore null. In C ++ un puntatore null provoca l'arresto anomalo dell'applicazione. Forse ci sono altri esempi: divisione per zero, indice fuori intervallo e così via.
Giorgio,
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.