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

30

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

Data

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

    return temp;
}

DOVREI FARE

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 ritorna soddisfano le mie precondizioni. È "migliore" o "più appropriato" saltare la convalida, o dovrei ancora difendermi dai valori sbagliati prima di procedere con il metodo che sto implementando, anche se dovrebbe sempre passare.

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

Afferma quando
  • Il metodo ha mostrato di comportarsi male nel passato
  • Il metodo proviene da una fonte non attendibile
  • Il metodo viene utilizzato da altre posizioni e non indica esplicitamente le sue condizioni post
Non asserire quando:
  • Il metodo vive a stretto contatto con il tuo (vedi la risposta scelta per i dettagli)
  • Il metodo definisce esplicitamente il suo contratto con qualcosa come doc, tipo sicurezza, test unitario o controllo post-condizione
  • Le prestazioni sono fondamentali (nel qual caso, una modalità di debug asserire potrebbe funzionare come un approccio ibrido)
posta Didier A. 28.04.2015 - 22:22
fonte

7 risposte

42

Dipende da quanto probabilmente getUser e myMethod devono cambiare e, soprattutto, quanto è probabile che cambino indipendentemente l'uno dall'altro .

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

  • Queste due funzioni "convivono"? In altre parole, hanno le stesse persone che li mantengono, fanno parte dello stesso file e quindi sono probabilmente in grado di rimanere "sincronizzati" l'uno con l'altro da soli? In questo caso è probabilmente eccessivo aggiungere controlli di convalida, poiché ciò significa semplicemente più codice che deve cambiare (e potenzialmente generare bug) ogni volta che le due funzioni cambiano o vengono rifatte in un numero diverso di funzioni.

  • La funzione getUser è 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 devi 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 rende chiaro, chiedi al manutentore di risolverlo.

  • Infine, se questa particolare funzione ha improvvisamente e inaspettatamente cambiato 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.

Nota che tutto quanto sopra si applica anche se sei l'autore originale di entrambe le funzioni. Non sappiamo se queste due funzioni dovrebbero "vivere insieme" per il resto della loro vita, o se si separeranno lentamente in moduli separati, o se in qualche modo ti sei sparato a un piede con un insetto in età avanzata versioni di getUser. Ma probabilmente puoi fare un'ipotesi abbastanza decente.

    
risposta data 28.04.2015 - 22:38
fonte
22

La risposta di Ixrec è buona, ma avrò un approccio diverso perché credo che valga la pena di essere preso in considerazione. Ai fini di questa discussione parlerò delle asserzioni, dal momento che questo è il nome con cui il Preconditions.checkNotNull() è stato tradizionalmente conosciuto.

Quello che vorrei suggerire è che i programmatori spesso sovrastimano il grado con il quale sono certi che qualcosa si comporterà in un certo modo e sottovaluteranno le conseguenze di ciò che non si comporta in quel modo. Inoltre, i programmatori spesso non realizzano il potere delle asserzioni come documentazione. Successivamente, nella mia esperienza, i programmatori affermano le cose molto meno spesso di quanto dovrebbero.

Il mio motto è:

The question you should always be asking yourself is not "should I assert this?" but "is there anything I forgot to assert?"

Naturalmente, se sei assolutamente sicuro che qualcosa si comporta in un certo modo, ti asterrai dal sostenere che in effetti si è comportato in quel modo, e questo è per il la maggior parte 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; con un'affermazione che i è effettivamente all'interno di un certo intervallo. Questa è la situazione in cui i è suscettibile di essere alterato da qualcuno che sta provando vari scenari ipotetici, e il codice che segue si basa su i con un valore all'interno di un certo intervallo, e non è immediatamente evidente da subito guardando il seguente codice qual è l'intervallo accettabile. Quindi, int i = 3; assert i >= 0 && i < 5; potrebbe a prima vista sembrare insensato, ma se ci pensi, ti dice che puoi giocare con i , e ti dice anche l'intervallo entro il quale devi rimanere. Un'asserzione è il miglior tipo di documentazione, perché è applicata dalla macchina , quindi è una garanzia perfetta e viene rifattorizzato insieme a il codice , quindi rimane sempre pertinente.

Il tuo esempio di getUser(int id) non è un parente concettuale molto distante dell'esempio di assign-constant-and-assert-in-range . Vedete, per definizione, gli errori si verificano quando le cose mostrano un comportamento diverso dal comportamento che ci aspettavamo. È vero, non possiamo affermare tutto, quindi a volte ci sarà un po 'di debug. Ma è un fatto ben noto nel settore che più rapidamente si ottiene un errore, meno costa. Le domande che dovresti porre non solo sono sicure di quanto getUser() non restituirà mai null, (e non restituirà mai un utente con un nome null), ma anche, quali tipi di cose brutte accadranno con il resto del codice se in effetti un giorno restituisce qualcosa di inaspettato e quanto è facile guardare rapidamente il resto del codice per sapere esattamente cosa ci si aspettava da getUser() .

Se il comportamento imprevisto causerebbe il danneggiamento del database, allora forse dovresti affermare anche se sei sicuro al 101% che non accadrà. Se un nome utente null causerebbe qualche strano errore irreversibile criptico, migliaia di righe più in basso e miliardi di cicli di clock successivi, allora forse è meglio fallire il prima possibile.

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

    
risposta data 29.04.2015 - 00:10
fonte
8

Un preciso numero

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

Se devono essere eseguiti controlli, ogni funzione dovrebbe verificare le proprie condizioni. 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 intende restituire null, questo dovrebbe essere documentato 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 che affrontino meno ostacoli quando scrivono le proprie funzioni.

    
risposta data 29.04.2015 - 12:19
fonte
5

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

Se null non è un valore di ritorno valido del metodo getUser, allora c'è un errore in getUser - il metodo getUser dovrebbe lanciare un'eccezione o comunque essere corretto.

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

    
risposta data 29.04.2015 - 01:41
fonte
5

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

Il modello di oggetto nullo è un modo per restituire oggetti che sostanzialmente non fanno nulla: piuttosto che restituire null per il tuo utente, restituire un oggetto che rappresenta "nessun utente".

Questo utente non è attivo, ha un nome vuoto e altri valori non nulli che indicano "niente qui". Il vantaggio principale è che non hai bisogno di sparpagliare il tuo programma: controlli nulli, registrazione, ecc., Basta usare gli oggetti.

Perché un algoritmo dovrebbe preoccuparsi di un valore nullo? I passaggi dovrebbero essere gli stessi. È altrettanto facile e molto più chiaro avere un oggetto nullo che restituisce zero, una 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 nullo:

return getUser().getName();

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

Mentre la domanda è taggata , vale la pena notare che il il modello di oggetto nullo è veramente utile in C ++ quando si usano i riferimenti. I riferimenti Java sono più simili ai puntatori C ++ e consentono null. I riferimenti C ++ non possono contenere nullptr . Se si desidera avere qualcosa di analogo a un riferimento null in C ++, il modello di oggetto nullo è l'unico modo che conosco per ottenerlo.

    
risposta data 29.04.2015 - 00:38
fonte
1

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

  1. robustezza: il metodo di chiamata può far fronte al valore null ? Se un valore null potrebbe risultare in RuntimeException , consiglierei un controllo, a meno che la complessità non sia bassa e i metodi chiamati / chiamanti siano creati dallo stesso autore e null non è previsto (ad esempio, entrambi private in lo stesso pacchetto).

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

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

Inoltre, consiglierei di documentare potenziali valori di ritorno null in un commento JavaDoc. A causa dell'evidenziazione delle descrizioni JavaDoc nella maggior parte degli IDE, questo può essere un utile avvertimento per chi usa 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)
{
    ...
}
    
risposta data 28.04.2015 - 23:08
fonte
-1

Sicuramente non devi.

Ad esempio, se già sai che un ritorno non può mai essere nullo - Perché vorresti aggiungere un assegno nulla? L'aggiunta di un controllo null non ha intenzione di rompere nulla, ma è solo ridondante. E meglio non codificare la logica ridondante fintanto che puoi evitarlo.

    
risposta data 01.05.2015 - 11:44
fonte

Leggi altre domande sui tag