Convalida del parametro di input nel chiamante: duplicazione del codice?


16

Qual è il posto migliore per convalidare i parametri di input della funzione: nel chiamante o nella funzione stessa?

Poiché vorrei migliorare il mio stile di codifica, provo a trovare le migliori pratiche o alcune regole per questo problema. Quando e cosa è meglio.

Nei miei progetti precedenti, eravamo soliti controllare e trattare tutti i parametri di input all'interno della funzione (ad esempio se non è null). Ora, ho letto qui in alcune risposte e anche nel libro del programmatore pragmatico, che la convalida del parametro di input è responsabilità del chiamante.

Quindi significa che dovrei convalidare i parametri di input prima di chiamare la funzione. Ovunque viene chiamata la funzione. E ciò solleva una domanda: non crea una duplicazione della condizione di controllo ovunque venga chiamata la funzione?

Non sono interessato solo a condizioni nulle, ma alla convalida di eventuali variabili di input (valore negativo da sqrtfunzionare, divisione per zero, combinazione errata di stato e codice postale o qualsiasi altra cosa)

Ci sono alcune regole su come decidere dove controllare la condizione di input?

Sto pensando ad alcuni argomenti:

  • quando il trattamento della variabile non valida può variare, è bene convalidarlo nel lato chiamante (ad es. sqrt()funzione - in alcuni casi potrei voler lavorare con un numero complesso, quindi tratto la condizione nel chiamante)
  • quando la condizione di controllo è la stessa in tutti i chiamanti, è meglio controllarlo all'interno della funzione, per evitare duplicazioni
  • la convalida del parametro di input nel chiamante ha luogo solo una prima di chiamare molte funzioni con questo parametro. Pertanto, la convalida di un parametro in ciascuna funzione non è efficace
  • la soluzione giusta dipende dal caso particolare

Spero che questa domanda non sia duplicata da nessun'altra, ho cercato questo problema e ho trovato domande simili ma non menzionano esattamente questo caso.

Risposte:


15

Dipende. La decisione su dove inserire la convalida dovrebbe essere basata sulla descrizione e sulla forza del contratto implicito (o documentato) dal metodo. La convalida è un buon modo per rafforzare l'aderenza a un contratto specifico. Se per qualsiasi motivo il metodo ha un contratto molto rigido, allora sì, spetta a te controllare prima di chiamare.

Questo è un concetto particolarmente importante quando crei un metodo pubblico , perché sostanzialmente stai pubblicizzando che un metodo esegue alcune operazioni. È meglio fare quello che dici che fa!

Prendi il seguente metodo come esempio:

public void DeletePerson(Person p)
{            
    _database.Delete(p);
}

Cosa implica il contratto DeletePerson? Il programmatore può solo supporre che se ne Personviene passato uno, verrà eliminato. Tuttavia, sappiamo che questo non è sempre vero. E se pfosse un nullvalore? Cosa succede se pnon esiste nel database? Cosa succede se il database è disconnesso? Pertanto, DeletePerson non sembra soddisfare bene il contratto. A volte, elimina una persona, a volte genera una NullReferenceException o DatabaseNotConnectedException o talvolta non fa nulla (ad esempio se la persona è già eliminata).

Le API come questa sono notoriamente difficili da usare, perché quando si chiama questa "scatola nera" di un metodo, possono accadere cose terribili di ogni genere.

Ecco un paio di modi per migliorare il contratto:

  • Aggiungi la convalida e aggiungi un'eccezione al contratto. Questo rende il contratto più forte , ma richiede che il chiamante esegua la convalida. La differenza, tuttavia, è che ora conoscono le loro esigenze. In questo caso, lo comunico con un commento XML C #, ma è possibile invece aggiungere un throws(Java), utilizzare Asserto uno strumento di contratto come Contratti di codice.

    ///<exception>ArgumentNullException</exception>
    ///<exception>ArgumentException</exception>
    public void DeletePerson(Person p)
    {            
        if(p == null)
            throw new ArgumentNullException("p");
        if(!_database.Contains(p))
            throw new ArgumentException("The Person specified is not in the database.");
    
        _database.Delete(p);
    }
    

    Nota a margine: l'argomento contro questo stile è spesso che causa un'eccessiva pre-validazione da parte di tutto il codice chiamante, ma nella mia esperienza spesso non è così. Pensa a uno scenario in cui stai cercando di eliminare una persona nulla. Come è successo? Da dove viene la persona nulla? Se questa è un'interfaccia utente, ad esempio, perché è stato gestito il tasto Elimina se non è presente alcuna selezione? Se fosse già stato eliminato, non dovrebbe essere già stato rimosso dal display? Ovviamente ci sono eccezioni a questo, ma man mano che un progetto cresce, spesso ringrazierai il codice come questo per impedire ai bug di penetrare in profondità nel sistema.

  • Aggiungi validazione e codice in modo difensivo. Questo rende il contratto più allentato , perché ora questo metodo non si limita a eliminare la persona. Ho cambiato il nome del metodo per riflettere questo, ma potrebbe non essere necessario se sei coerente nella tua API. Questo approccio ha i suoi pro e contro. Il fatto è che ora puoi chiamare TryDeletePersonpassando qualsiasi tipo di input non valido e non preoccuparti mai delle eccezioni. Il contro, ovviamente, è che gli utenti del tuo codice probabilmente chiameranno questo metodo troppo, o potrebbe rendere difficile il debug nei casi in cui p è nullo. Questa potrebbe essere considerata una lieve violazione del Principio della singola responsabilità , quindi tienilo a mente se scoppia una guerra di fiamma.

    public void TryDeletePerson(Person p)
    {            
        if(p == null || !_database.Contains(p))
            return;
    
        _database.Delete(p);
    }
    
  • Combina approcci. A volte vuoi un po 'di entrambi, dove vuoi che i chiamanti esterni seguano da vicino le regole (per costringerli al responsabile del codice), ma vuoi che il tuo codice privato sia flessibile.

    ///<exception>ArgumentNullException</exception>
    ///<exception>ArgumentException</exception>
    public void DeletePerson(Person p)
    {            
        if(p == null)
            throw new ArgumentNullException("p");
        if(!_database.Contains(p))
            throw new ArgumentException("The Person specified is not in the database.");
    
        TryDeletePerson(p);
    }
    
    internal void TryDeletePerson(Person p)
    {            
        if(p == null || !_database.Contains(p))
            return;
    
        _database.Delete(p);
    }
    

Nella mia esperienza, concentrarsi sui contratti che hai insinuato piuttosto che una regola dura funziona meglio. La codifica difensiva sembra funzionare meglio nei casi in cui è difficile o difficile per il chiamante determinare se un'operazione è valida. I contratti rigorosi sembrano funzionare meglio dove ci si aspetta che il chiamante effettui chiamate di metodo solo quando hanno davvero, davvero senso.


Grazie per la bella risposta con l'esempio. Mi piace il punto di approccio "difensivo" e "contratto rigoroso".
srnka,

7

È una questione di convenzione, documentazione e caso d'uso.

Non tutte le funzioni sono uguali. Non tutti i requisiti sono uguali. Non tutta la convalida è uguale.

Ad esempio, se il tuo progetto Java cerca di evitare puntatori null ogni volta che è possibile (vedi i consigli sullo stile Guava , per esempio), convalidi comunque ogni argomento della funzione per assicurarti che non sia nullo? Probabilmente non è necessario, ma è probabile che tu lo faccia ancora, per facilitare la ricerca di bug. Ma puoi usare un'asserzione in cui in precedenza hai lanciato una NullPointerException.

Cosa succede se il progetto è in C ++? La convenzione / tradizione in C ++ è di documentare i presupposti, ma di verificarli (se non del tutto) solo in build di debug.

In entrambi i casi, hai una condizione documentata sulla tua funzione: nessun argomento può essere nullo. È possibile invece estendere il dominio della funzione per includere valori null con comportamento definito, ad esempio "se un argomento è null, genera un'eccezione". Ovviamente, questa è ancora la mia eredità C ++ che parla qui - in Java, è abbastanza comune documentare i presupposti in questo modo.

Ma non tutte le condizioni preliminari possono essere ragionevolmente verificate. Ad esempio, un algoritmo di ricerca binaria ha il presupposto che la sequenza da cercare deve essere ordinata. Ma verificare che sia sicuramente così è un'operazione O (N), quindi farlo su ogni chiamata in qualche modo sconfigge il punto di usare un algoritmo O (log (N)). Se stai programmando in modo difensivo, puoi eseguire controlli minori (ad esempio verificando che per ogni partizione che cerchi, i valori di inizio, metà e fine siano ordinati), ma ciò non rileva tutti gli errori. In genere, dovrai solo fare affidamento sul rispetto del presupposto.

L'unico luogo reale in cui sono necessari controlli espliciti è ai confini. Input esterno al tuo progetto? Convalida, convalida, convalida. Un'area grigia è i confini dell'API. Dipende davvero da quanto vuoi fidarti del codice client, da quanto danno l'input non valido fa e da quanto assistenza vuoi fornire nella ricerca di bug. Qualsiasi limite di privilegio deve essere considerato come esterno, ovviamente - le syscall, ad esempio, devono essere eseguite in un contesto di privilegio elevato e quindi devono essere molto attente alla convalida. Ovviamente tale convalida deve essere interna al syscall.


Grazie per la tua risposta. Puoi, per favore, dare il link alla raccomandazione di stile Guava? Non riesco a google e scopri cosa intendevi con questo. +1 per la convalida dei confini.
srnka,

Link aggiunto In realtà non è una guida di stile completa, solo una parte della documentazione delle utility non nulle.
Sebastian Redl,

6

La validazione dei parametri dovrebbe essere la preoccupazione della funzione chiamata. La funzione dovrebbe sapere cosa è considerato input valido e cosa no. I chiamanti potrebbero non saperlo, soprattutto quando non sanno come implementare la funzione internamente. La funzione dovrebbe essere in grado di gestire qualsiasi combinazione di valori dei parametri dai chiamanti.

Poiché la funzione è responsabile della convalida dei parametri, è possibile scrivere test unitari su questa funzione per assicurarsi che si comporti come previsto con valori di parametro validi e non validi.


Grazie per la risposta. Quindi pensi che quella funzione dovrebbe controllare in ogni caso sia i parametri di input validi che quelli non validi. Qualcosa di diverso dall'affermazione del libro del programmatore pragmatico: "la convalida del parametro di input è responsabilità del chiamante". È una buona idea "La funzione dovrebbe sapere cosa è considerato valido ... I chiamanti potrebbero non saperlo" ... Quindi non ti piace usare le pre-condizioni?
srnka,

1
Se lo desideri, puoi utilizzare le pre-condizioni (vedi la risposta di Sebastian ), ma preferisco essere difensivo e gestire qualsiasi tipo di input possibile.
Bernard,

4

All'interno della funzione stessa. Se la funzione viene utilizzata più di una volta, non si desidera verificare il parametro per ogni chiamata di funzione.

Inoltre, se la funzione viene aggiornata in modo tale da influire sulla convalida del parametro, è necessario cercare ogni occorrenza della convalida del chiamante per aggiornarli. Non è adorabile :-).

Puoi fare riferimento alla clausola di guardia

Aggiornare

Vedi la mia risposta per ogni scenario che hai fornito.

  • quando il trattamento della variabile non valida può variare, è bene convalidarlo nel lato chiamante (ad es. sqrt()funzione - in alcuni casi potrei voler lavorare con un numero complesso, quindi tratto la condizione nel chiamante)

    Risposta

    La maggior parte dei linguaggi di programmazione supporta numeri interi e reali per impostazione predefinita, non numeri complessi, quindi la loro implementazione sqrtaccetta solo numeri non negativi. L'unico caso in cui hai una sqrtfunzione che restituisce numeri complessi è quando usi un linguaggio di programmazione orientato alla matematica, come Mathematica

    Inoltre, sqrtper la maggior parte dei linguaggi di programmazione è già implementato, quindi non è possibile modificarlo, e se provi a sostituire l'implementazione (vedi patching delle scimmie), i tuoi collaboratori saranno completamente scioccati dal motivo per cui sqrtimprovvisamente accetta numeri negativi.

    Se lo desideri, puoi avvolgerlo attorno alla tua sqrtfunzione personalizzata che gestisce il numero negativo e restituisce un numero complesso.

  • quando la condizione di controllo è la stessa in tutti i chiamanti, è meglio controllarlo all'interno della funzione, per evitare duplicazioni

    Risposta

    Sì, questa è una buona pratica per evitare di spargere la convalida dei parametri nel codice.

  • la convalida del parametro di input nel chiamante ha luogo solo una prima di chiamare molte funzioni con questo parametro. Pertanto, la convalida di un parametro in ciascuna funzione non è efficace

    Risposta

    Sarà bello se il chiamante è una funzione, non credi?

    Se le funzioni all'interno del chiamante sono utilizzate da un altro chiamante, cosa ti impedisce di convalidare il parametro all'interno delle funzioni chiamate dal chiamante?

  • la soluzione giusta dipende dal caso particolare

    Risposta

    Obiettivo per il codice gestibile. Lo spostamento della convalida dei parametri garantisce una fonte di verità su ciò che la funzione può accettare o meno.


Grazie per la risposta. Sqrt () era solo un esempio, lo stesso comportamento con il parametro di input può essere utilizzato da molte altre funzioni. "se la funzione viene aggiornata in modo tale da influire sulla convalida del parametro, è necessario cercare ogni occorrenza della convalida del chiamante" - Non sono d'accordo. Possiamo quindi dire lo stesso per il valore restituito: se la funzione viene aggiornata in modo tale da influire sul valore restituito, è necessario correggere ogni chiamante ... Penso che la funzione debba avere un compito ben definito da fare ... Altrimenti la modifica del chiamante è comunque necessaria.
srnka,

2

Una funzione dovrebbe indicare le sue condizioni pre e post.
Le pre-condizioni sono le condizioni che devono essere soddisfatte dal chiamante prima che possa utilizzare correttamente la funzione e possa (e spesso fare) includere la validità dei parametri di input.
Le post-condizioni sono le promesse che la funzione fa ai suoi chiamanti.

Quando la validità dei parametri di una funzione fa parte delle pre-condizioni, è responsabilità del chiamante assicurarsi che tali parametri siano validi. Ciò non significa che ogni chiamante debba controllare esplicitamente ogni parametro prima della chiamata. Nella maggior parte dei casi, non sono necessari test espliciti perché la logica interna e le pre-condizioni del chiamante garantiscono già che i parametri siano validi.

Come misura di sicurezza contro errori di programmazione (bug), è possibile verificare che i parametri passati a una funzione soddisfino realmente le condizioni preliminari indicate. Poiché questi test possono essere costosi, è una buona idea essere in grado di spegnerli per build di rilascio. Se questi test falliscono, allora il programma dovrebbe essere terminato, perché si è verificato un errore.

Anche se a prima vista il check-in del chiamante sembra invitare alla duplicazione del codice, in realtà è il contrario. Il check in the call comporta la duplicazione del codice e un sacco di lavoro non necessario.
Basta pensarci, con quale frequenza si passano i parametri attraverso diversi livelli di funzioni, apportando solo piccole modifiche ad alcuni di essi lungo il percorso. Se si applica costantemente il check-in-callee metodo , ognuna di quelle funzioni intermedie dovrà ripetere il controllo per ciascuno dei parametri.
E ora immagina che uno di quei parametri dovrebbe essere un elenco ordinato.
Con il controllo nel chiamante, solo la prima funzione dovrebbe assicurarsi che l'elenco sia veramente ordinato. Tutti gli altri sanno che l'elenco è già ordinato (in quanto è quello che hanno dichiarato nella loro pre-condizione) e possono trasmetterlo senza ulteriori controlli.


+1 Grazie per la risposta. Bella riflessione: "Il controllo nella chiamata comporta la duplicazione del codice e un sacco di lavoro non necessario". E nella frase: "Nella maggior parte dei casi, non sono necessari test espliciti perché la logica interna e le pre-condizioni del chiamante garantiscono già" - cosa intendi con "logica interna"? La funzionalità DBC?
srnka,

@srnka: Con "logica interna" intendo i calcoli e le decisioni in una funzione. È essenzialmente l'implementazione della funzione.
Bart van Ingen Schenau,

0

Molto spesso non puoi sapere chi, quando e come chiamerà la funzione che hai scritto. È meglio assumere il peggio: la tua funzione verrà chiamata con parametri non validi. Quindi dovresti assolutamente coprirlo.

Tuttavia, se la lingua che usi supporta le eccezioni, potresti non verificare la presenza di alcuni errori ed essere sicuro che verrà generata un'eccezione, ma in questo caso devi essere sicuro di descrivere il caso nella documentazione (devi avere la documentazione). L'eccezione fornirà al chiamante informazioni sufficienti su ciò che è accaduto e attirerà anche l'attenzione sugli argomenti non validi.


In realtà, potrebbe essere meglio convalidare il parametro e, se il parametro non è valido, generare un'eccezione. Ecco perché: i pagliacci che chiamano la tua routine senza preoccuparsi di assicurarsi che abbiano dato dati validi sono gli stessi che non si preoccuperanno di controllare il codice di ritorno dell'errore che indica che hanno passato dati non validi. Lanciare un'eccezione FORZA il problema da risolvere.
John R. Strohm,
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.