Devo convalidare il valore restituito di una chiamata di metodo anche se so che il metodo non può restituire input errati?


30

Mi chiedo se dovrei difendermi dal valore di ritorno di una chiamata di metodo confermando che soddisfano le mie aspettative anche se so che il metodo che sto chiamando soddisferà tali aspettative.

DATO

User getUser(Int id)
{
    User temp = new User(id);
    temp.setName("John");

    return temp;
}

DOVREI

void myMethod()
{
    User user = getUser(1234);
    System.out.println(user.getName());
}

O

void myMethod()
{
    User user = getUser(1234);

    // Validating
    Preconditions.checkNotNull(user, "User can not be null.");
    Preconditions.checkNotNull(user.getName(), "User's name can not be null.");

    System.out.println(user.getName());
}

Lo sto chiedendo a livello concettuale. Se conosco il funzionamento interno del metodo che sto chiamando. O perché l'ho scritto o l'ho ispezionato. E la logica dei possibili valori che restituisce soddisfa i miei presupposti. È "migliore" o "più appropriato" saltare la convalida o dovrei ancora difendermi da valori errati prima di procedere con il metodo che sto attualmente implementando, anche se dovrebbe sempre passare.


La mia conclusione da tutte le risposte (sentiti libero di venire da solo):

Asserire quando
  • Il metodo ha dimostrato di avere un comportamento anomalo in passato
  • Il metodo proviene da una fonte non attendibile
  • Il metodo viene utilizzato da altri luoghi e non indica esplicitamente le sue post-condizioni
Non affermare quando:
  • Il metodo è strettamente legato al tuo (vedi la risposta scelta per i dettagli)
  • Il metodo definisce esplicitamente il contratto con qualcosa di simile a un documento appropriato, una sicurezza del tipo, un test unitario o un controllo post-condition
  • Le prestazioni sono fondamentali (nel qual caso, un'asserzione della modalità di debug potrebbe funzionare come un approccio ibrido)

1
Punta a quella copertura del codice% 100!
Jared Burrows,

9
Perché ti stai concentrando su un solo problema ( Null)? Probabilmente è ugualmente problematico se il nome è ""(non null ma la stringa vuota) o "N/A". O puoi fidarti del risultato o devi essere adeguatamente paranoico.
MSalters il

3
Nel nostro codebase abbiamo uno schema di denominazione: getFoo () promette di non restituire null e getFooIfPresent () potrebbe restituire null.
pjc50,

Da dove viene la tua presunzione di sanità mentale? Supponete che il valore sarà sempre diverso da zero perché è convalidato al momento dell'iscrizione o a causa della struttura con cui viene recuperato il valore? E, cosa ancora più importante, stai testando ogni possibile scenario del caso limite? Compresa la possibilità di script dannosi?
Zibbobz,

Risposte:


42

Ciò dipende dalla probabilità che getUser e myMethod cambino e, soprattutto, dalla probabilità che cambino indipendentemente l'uno dall'altro .

Se in qualche modo sai con certezza che getUser non cambierà mai e poi mai in futuro, allora sì, è una perdita di tempo convalidarlo, tanto quanto perdere tempo convalida che iha un valore di 3 immediatamente dopo i = 3;un'istruzione. In realtà, non lo sai. Ma ci sono alcune cose che sai:

  • Queste due funzioni "vivono insieme"? In altre parole, hanno le stesse persone che li mantengono, fanno parte dello stesso file e quindi probabilmente resteranno "sincronizzati" tra loro? In questo caso è probabilmente eccessivo aggiungere controlli di validazione, dal momento che ciò significa semplicemente più codice che deve cambiare (e potenzialmente generare bug) ogni volta che le due funzioni cambiano o vengono refactored in un diverso numero di funzioni.

  • La funzione getUser fa parte di un'API documentata con un contratto specifico e myMethod è semplicemente un client di detta API in un'altra base di codice? In tal caso, puoi leggere quella documentazione per scoprire se dovresti convalidare i valori di ritorno (o pre-validare i parametri di input!) O se è davvero sicuro seguire ciecamente il percorso felice. Se la documentazione non lo chiarisce, chiedere al manutentore di risolverlo.

  • Infine, se questa particolare funzione ha cambiato improvvisamente e inaspettatamente il suo comportamento in passato, in un modo che ha infranto il tuo codice, hai tutto il diritto di essere paranoico al riguardo. I bug tendono a raggrupparsi.

Si noti che tutto quanto sopra si applica anche se si è l'autore originale di entrambe le funzioni. Non sappiamo se ci si aspetta che queste due funzioni "vivano insieme" per il resto della loro vita, o se si allontaneranno lentamente in moduli separati, o se in qualche modo ti sei sparato al piede con un insetto versioni di getUser. Ma probabilmente puoi fare un'ipotesi abbastanza decente.


2
Inoltre: se dovessi cambiare in getUserseguito, in modo che potesse restituire null, il controllo esplicito ti darebbe informazioni che non potresti ottenere da "NullPointerException" e il numero di riga?
user253751

Sembra che ci siano due cose che puoi validare: (1) Che il metodo funzioni correttamente, come in, non ha bug. (2) Che il metodo rispetti il ​​tuo contratto. Vedo che la convalida per il numero 1 è eccessiva. Dovresti invece fare dei test n. 1. Ecco perché non convaliderei i = 3 ;. Quindi la mia domanda riguarda il n. 2. Mi piace la tua risposta per questo, ma allo stesso tempo, è un uso la tua risposta di giudizio. Vedi qualche altro problema derivante dalla convalida esplicita dei tuoi contratti oltre a perdere un po 'di tempo a scrivere l'asserzione?
Didier A.

L'unico altro "problema" è che se la tua interpretazione del contratto è errata o si sbaglia, allora devi andare a cambiare tutta la convalida. Ma questo vale anche per tutti gli altri codici.
Ixrec,

1
Non posso andare contro la folla. Questa risposta è sicuramente la più corretta nel trattare l'argomento della mia domanda. Vorrei aggiungere che leggendo tutte le risposte, in particolare @MikeNakis, ora penso che dovresti affermare le tue condizioni preliminari, almeno in modalità debug, quando hai uno degli ultimi due punti elenco di Ixrec. Se ti trovi di fronte al primo punto elenco, probabilmente è eccessivo far valere i tuoi presupposti. Infine, dato un contratto esplicito, come un documento adeguato, la sicurezza del tipo, un test unitario o un controllo post-condition, non dovresti far valere i tuoi presupposti, sarebbe eccessivo.
Didier A.

22

La risposta di Ixrec è buona, ma adotterò un approccio diverso perché ritengo che valga la pena prendere in considerazione. Ai fini di questa discussione parlerò di asserzioni, poiché questo è il nome con il quale il tuo Preconditions.checkNotNull()è stato tradizionalmente conosciuto.

Quello che vorrei suggerire è che i programmatori spesso sopravvalutano il grado con cui sono certi che qualcosa si comporterà in un certo modo e sottovalutando le conseguenze che esso non si comporta in quel modo. Inoltre, i programmatori spesso non si rendono conto del potere delle asserzioni come documentazione. Successivamente, nella mia esperienza, i programmatori affermano le cose molto meno spesso di quanto dovrebbero.

Il mio motto è:

La domanda che dovresti sempre porti non è "dovrei affermarlo?" ma "c'è qualcosa che ho dimenticato di affermare?"

Naturalmente, se ne sei assolutamente sicuro che qualcosa si comporti in un certo modo, ti asterrai dall'affermare che in effetti si è comportato in quel modo, e questo è per la maggior parte ragionevole. Non è davvero possibile scrivere software se non ci si può fidare di nulla.

Ma ci sono eccezioni anche a questa regola. Curiosamente, per prendere l'esempio di Ixrec, esiste un tipo di situazione in cui è perfettamente desiderabile seguire int i = 3;un'affermazione che iè effettivamente entro un certo intervallo. Questa è la situazione in cui iè suscettibile di essere modificato da qualcuno che sta provando vari scenari what-if e il codice che segue si basa isull'avere un valore entro un certo intervallo, e non è immediatamente ovvio guardando rapidamente il seguente codice l'intervallo accettabile è. Quindi, all'inizio int i = 3; assert i >= 0 && i < 5;potrebbe sembrare insensato, ma se ci pensate, vi dice che si può giocare coni , e ti dice anche il raggio entro cui devi rimanere. Un'affermazione è il miglior tipo di documentazione, perchéviene applicato dalla macchina , quindi è una garanzia perfetta e viene refactored insieme al codice , quindi rimane sempre pertinente.

Il tuo getUser(int id)esempio non è un parente concettuale molto distante assign-constant-and-assert-in-rangedell'esempio. Vedi, per definizione, i bug si verificano quando le cose mostrano un comportamento diverso da quello che ci aspettavamo. È vero, non possiamo affermare tutto, quindi a volte ci sarà un po 'di debug. Ma è un fatto consolidato nel settore che quanto più rapidamente viene rilevato un errore, tanto meno costa. Le domande che dovresti porre non sono solo quanto sei sicuro che getUser()non restituirà mai null (e non restituirà mai un utente con un nome null), ma anche quali tipi di cose cattive accadranno con il resto del codice se lo fa in infatti un giorno restituiscono qualcosa di inaspettato e quanto è facile osservando rapidamente il resto del codice per sapere esattamente cosa ci si aspettava getUser().

Se il comportamento imprevisto provocherebbe la corruzione del database, forse dovresti affermarlo anche se sei sicuro al 101% che non accadrà. Se un nullnome utente provocherebbe uno strano errore criptico non rintracciabile migliaia di righe più in basso e miliardi di cicli di clock più tardi, allora forse è meglio fallire il prima possibile.

Quindi, anche se non sono in disaccordo con la risposta di Ixrec, suggerirei di considerare seriamente il fatto che le asserzioni non costano nulla, quindi non c'è davvero nulla da perdere, solo da guadagnare, usandole liberamente.


2
Sì. Affermalo. Sono venuto qui per dare più o meno questa risposta. ++
RubberDuck,

2
@SteveJessop Vorrei correggere questo ... && sureness < 1).
Mike Nakis,

2
@MikeNakis: è sempre un incubo quando si scrivono unit test, il valore di ritorno che è consentito dall'interfaccia ma impossibile da generare effettivamente da qualsiasi input ;-) Comunque, quando sei sicuro al 101% di avere ragione, allora sicuramente sbagli !
Steve Jessop,

2
+1 per la segnalazione di cose che ho completamente dimenticato di menzionare nella mia risposta.
Ixrec,

1
@DavidZ È corretto, la mia domanda ha assunto la convalida del runtime. Ma dire che non ti fidi del metodo nel debug e che ti fidi nel prod è forse un buon punto di vista sull'argomento. Quindi un'affermazione in stile C potrebbe essere un buon modo per affrontare questa situazione.
Didier A.

8

Un preciso no.

Un chiamante non dovrebbe mai verificare se la funzione che sta chiamando rispetta il suo contratto. Il motivo è semplice: ci sono potenzialmente molti chiamanti ed è impraticabile e probabilmente anche sbagliato da un punto di vista concettuale per ogni singolo chiamante duplicare la logica per controllare i risultati di altre funzioni.

Se devono essere effettuati controlli, ciascuna funzione dovrebbe verificare le proprie condizioni post. Le post-condizioni ti danno la sicurezza di aver implementato correttamente la funzione e gli utenti saranno in grado di fare affidamento sul contratto della tua funzione.

Se una determinata funzione non è destinata a restituire mai nulla, questa dovrebbe essere documentata e diventare parte del contratto di quella funzione. Questo è il punto di questi contratti: offrire agli utenti alcuni invarianti su cui poter fare affidamento in modo da affrontare meno ostacoli quando si scrivono le proprie funzioni.


Sono d'accordo con il sentimento generale, ma ci sono circostanze in cui ha sicuramente senso convalidare i valori di ritorno. Ad esempio, se si chiama un servizio Web esterno, a volte si desidera almeno convalidare il formato della risposta (xsd, ecc ...)
AK_

Sembra che tu stia sostenendo uno stile di Design by Contract su uno di programmazione difensiva. Penso che questa sia un'ottima risposta. Se il metodo avesse un contratto rigoroso, potrei fidarmi di esso. Nel mio caso non lo è, ma potrei cambiarlo per farlo. Dire che non potrei però? Diresti che dovrei fidarmi dell'implementazione? Anche se non indica esplicitamente nel suo documento che non restituisce null o che in realtà ha un controllo post-condition?
Didier A.

A mio avviso, dovresti usare il tuo miglior giudizio e ponderare aspetti diversi come quanto ti fidi dell'autore a fare la cosa giusta, quanto è probabile che l'interfaccia della funzione cambi e quanto siano stretti i tuoi vincoli di tempo. Detto questo, il più delle volte, l'autore di una funzione ha un'idea abbastanza chiara di quale dovrebbe essere il contratto della funzione, anche se non lo documenta. Per questo motivo, dico che dovresti prima fidarti degli autori per fare la cosa giusta e diventare difensivi solo quando sai che la funzione non è ben scritta.
Paolo,

Trovo che prima fidarmi degli altri per fare la cosa giusta mi aiuti a fare il lavoro più velocemente. Detto questo, il tipo di applicazioni che scrivo sono facili da risolvere quando si verifica un problema. Qualcuno che lavora con software più critici per la sicurezza potrebbe pensare che io sia troppo indulgente e preferisca un approccio più difensivo.
Paolo,

5

Se null è un valore restituito valido del metodo getUser, il codice dovrebbe gestirlo con un If, non un'eccezione, non è un caso eccezionale.

Se null non è un valore di ritorno valido del metodo getUser, allora c'è un bug in getUser: il metodo getUser dovrebbe generare un'eccezione o essere corretto in altro modo.

Se il metodo getUser fa parte di una libreria esterna o altrimenti non può essere modificato e si desidera che vengano generate eccezioni in caso di ritorno null, avvolgere la classe e il metodo per eseguire i controlli e generare l'eccezione in modo che ogni istanza di la chiamata a getUser è coerente nel tuo codice.


Null non è un valore di ritorno valido di getUser. Ispezionando getUser, ho visto che l'implementazione non sarebbe mai tornata nulla. La mia domanda è: dovrei fidarmi della mia ispezione e non avere un controllo nel mio metodo, o dovrei dubitare della mia ispezione e avere ancora un controllo. È anche possibile che il metodo cambi in futuro, infrangendo le mie aspettative di non tornare a null.
Didier A.

E se getUser o user.getName cambiano per generare eccezioni? Dovresti avere un controllo, ma non come parte del metodo che utilizza l'utente. Avvolgi il metodo getUser - e persino la classe User - per far rispettare i tuoi contratti, se sei preoccupato per le future modifiche alla rottura del codice getUser. Crea anche test unitari per verificare i tuoi presupposti sul comportamento della classe.
Matteo C

@didibus Per parafrasare la tua risposta ... Un'emoticon smiley non è un valore di ritorno valido di getUser. Ispezionando getUser, ho visto che l'implementazione non avrebbe mai restituito una faccina sorridente . La mia domanda è: dovrei fidarmi della mia ispezione e non avere un controllo nel mio metodo, o dovrei dubitare della mia ispezione e avere ancora un controllo. È anche possibile che il metodo cambi in futuro, infrangendo le mie aspettative di non restituire un'emoticon sorridente . Non è compito del chiamante confermare che la funzione chiamata sta adempiendo al suo contratto.
Vince O'Sullivan,

@ VinceO'Sullivan Sembra che il "contratto" sia al centro del problema. E che la mia domanda riguarda i contratti impliciti, contro espliciti. Un metodo che restituisce int non voglio affermare che non sto ricevendo una stringa. Ma, se voglio solo ints positivi, dovrei? È solo implicito che il metodo non restituisce int negativo, perché l'ho ispezionato. Non è chiaro dalla firma o dalla documentazione che non lo sia.
Didier A.

1
Non importa se il contratto è implicito o esplicito. Non è compito del codice chiamante verificare che il metodo chiamato restituisca dati validi quando vengono dati dati validi. Nel tuo esempio, sai che gli init negativi non sono risposte errate ma sai se int positivi sono corretti? Di quanti test hai davvero bisogno per dimostrare che il metodo funziona? L'unico corso corretto è presumere che il metodo sia corretto.
Vince O'Sullivan,

5

Direi che la premessa della tua domanda è spenta: perché usare un riferimento null per cominciare?

Il modello di oggetti null è un modo per restituire oggetti che essenzialmente non fanno nulla: piuttosto che restituire null per l'utente, restituisce un oggetto che rappresenta "nessun utente".

Questo utente sarebbe inattivo, con un nome vuoto e altri valori non nulli che indicano "qui nulla". Il vantaggio principale è che non è necessario sporcare il programma annullando i controlli, la registrazione, ecc. Basta usare i propri oggetti.

Perché un algoritmo dovrebbe preoccuparsi di un valore nullo? I passaggi dovrebbero essere gli stessi. È altrettanto semplice e molto più chiaro avere un oggetto null che restituisce zero, stringa vuota, ecc.

Ecco un esempio di controllo del codice per null:

User u = getUser();
String displayName = "";
if (u != null) {
  displayName = u.getName();
}
return displayName;

Ecco un esempio di codice che utilizza un oggetto null:

return getUser().getName();

Qual è più chiaro? Credo che il secondo sia.


Mentre la domanda è taggata , vale la pena notare che il modello di oggetti null è davvero utile in C ++ quando si usano i riferimenti. I riferimenti Java sono più simili a puntatori C ++ e consentono null. I riferimenti C ++ non possono essere conservati nullptr. Se uno vorrebbe avere qualcosa di analogo a un riferimento null in C ++, il modello di oggetti null è l'unico modo che conosco per realizzarlo.


Null è solo l'esempio di ciò che asserisco. Potrebbe benissimo essere che ho bisogno di un nome non vuoto. La mia domanda sarebbe ancora valida, lo confermeresti? Oppure potrebbe essere qualcosa che restituisce un int e non posso avere un int negativo, oppure deve trovarsi in un determinato intervallo. Non pensarlo come un problema nullo per dire.
Didier A.

1
Perché un metodo dovrebbe restituire un valore errato? L'unico posto in cui questo dovrebbe essere verificato è al limite dell'applicazione: si interfaccia con l'utente e con altri sistemi. Altrimenti, ecco perché abbiamo delle eccezioni.

Immagina di avere: int i = getCount; float x = 100 / i; Se so che getCount non restituisce 0, poiché o ho scritto il metodo o lo ho ispezionato, dovrei saltare il controllo per 0?
Didier A.

@didibus: puoi saltare il controllo se getCount() documenta la garanzia che ora non restituisce 0 e non lo farà in futuro, tranne nei casi in cui qualcuno decide di apportare una modifica definitiva e cerca di aggiornare tutto ciò che lo chiama per far fronte la nuova interfaccia. Vedere cosa fa attualmente il codice non è di aiuto, ma se hai intenzione di ispezionare il codice, il codice da ispezionare è il test unitario getCount(). Se lo verificano per non restituire 0, allora si sa che qualsiasi modifica futura per restituire 0 verrà considerata una modifica di rottura perché i test falliranno.
Steve Jessop,

Non mi è chiaro che gli oggetti null siano migliori dei null. Un'anatra nulla può ciarlare, ma non è un'anatra - in effetti, semanticamente, è l'antitesi di un'anatra. Dopo aver introdotto un oggetto null, qualsiasi codice che utilizza quella classe potrebbe verificare se uno qualsiasi dei suoi oggetti è l'oggetto null - ad esempio, hai un oggetto null in un set, quante cose hai? Questo mi sembra un modo per rinviare l'errore e offuscare gli errori. L'articolo collegato mette specificamente in guardia dall'uso di oggetti null "solo per evitare controlli null e rendere il codice più leggibile".
sdenham,

1

In generale, direi che dipende principalmente dai seguenti tre aspetti:

  1. robustezza: il metodo chiamante può far fronte al nullvalore? Se un nullvalore potrebbe comportare un RuntimeExceptionsuggerimento, raccomanderei un controllo, a meno che la complessità non sia bassa e i metodi chiamati / chiamate siano creati dallo stesso autore e nullnon siano previsti (ad esempio entrambi privatenello stesso pacchetto).

  2. responsabilità del codice: se lo sviluppatore A è responsabile del getUser()metodo e lo sviluppatore B lo utilizza (ad esempio come parte di una libreria), consiglio vivamente di convalidare il suo valore. Solo perché lo sviluppatore B potrebbe non essere a conoscenza di una modifica che si traduca in un potenziale nullvalore di ritorno.

  3. complessità: maggiore è la complessità complessiva del programma o dell'ambiente, più consiglierei di convalidare il valore restituito. Anche se sei sicuro che non può essere nulloggi nel contesto del metodo di chiamata, potrebbe essere che devi cambiare getUser()per un altro caso d'uso. Trascorsi alcuni mesi o anni e aggiunte migliaia di righe di codice, questa può essere una trappola.

Inoltre, consiglierei di documentare i potenziali nullvalori di ritorno in un commento JavaDoc. A causa dell'evidenziazione delle descrizioni JavaDoc nella maggior parte degli IDE, questo può essere un avvertimento utile per chiunque usi getUser().

/** Returns ....
  * @param: id id of the user
  * @return: a User instance related to the id;
  *          null, if no user with identifier id exists
 **/
User getUser(Int id)
{
    ...
}

-1

Non devi assolutamente.

Ad esempio, se sai già che un reso non può mai essere nullo - Perché dovresti aggiungere un controllo null? L'aggiunta di un controllo null non romperà nulla, ma è solo ridondante. E meglio non codificare la logica ridondante fintanto che puoi evitarla.


1
Lo so, ma solo perché ho ispezionato o scritto l'implementazione dell'altro metodo. Il contratto del metodo non dice esplicitamente che non può. Quindi potrebbe, ad esempio, cambiare in futuro. Oppure potrebbe avere un bug che causa la restituzione di un valore nullo anche se stava tentando di non farlo.
Didier A.
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.