Il controllo delle condizioni ridondanti rispetto alle migliori pratiche?


16

Ho sviluppato software negli ultimi tre anni, ma mi sono appena svegliato di recente con la mia ignoranza riguardo alle buone pratiche. Questo mi ha portato a iniziare a leggere il libro Clean Code , che sta cambiando la mia vita in meglio, ma sto lottando per ottenere informazioni su alcuni dei migliori approcci per la scrittura dei miei programmi.

Ho un programma Python in cui ...

  1. usa argparse required=Trueper imporre due argomenti, che sono entrambi nomi di file. il primo è il nome del file di input, il secondo è il nome del file di output
  2. avere una funzione readFromInputFileche controlla per prima cosa che sia stato inserito un nome per il file di input
  3. avere una funzione writeToOutputFileche controlla per prima cosa che sia stato inserito un nome per il file di output

Il mio programma è abbastanza piccolo da essere portato a credere che il check in # 2 e # 3 sia ridondante e debba essere rimosso, liberando così entrambe le funzioni da una ifcondizione non necessaria . Tuttavia, sono stato anche indotto a credere che "il doppio controllo è corretto" e potrebbe essere la soluzione giusta in un programma in cui le funzioni potrebbero essere chiamate da una posizione diversa in cui non si verifica l'analisi degli argomenti.

(Inoltre, se la lettura o la scrittura falliscono, ho una try exceptfunzione per ciascuna per sollevare un messaggio di errore appropriato.)

La mia domanda è: è meglio evitare tutti i controlli delle condizioni ridondanti? La logica di un programma dovrebbe essere così solida che i controlli devono essere fatti solo una volta? Ci sono dei buoni esempi che illustrano questo o il contrario?

EDIT: Grazie a tutti per le risposte! Ho imparato qualcosa da ciascuno. Vedere così tante prospettive mi dà una comprensione molto migliore di come affrontare questo problema e determinare una soluzione in base alle mie esigenze. Grazie!


Ecco una versione fortemente generalizzata della tua domanda: softwareengineering.stackexchange.com/questions/19549/… . Non direi che è duplicato poiché ha un focus piuttosto più ampio, ma forse aiuta.
Doc Brown,

Risposte:


15

Quello che stai chiedendo si chiama "robustezza" e non esiste una risposta giusta o sbagliata. Dipende dalle dimensioni e dalla complessità del programma, dal numero di persone che vi lavorano e dall'importanza di rilevare i guasti.

Nei piccoli programmi che scrivi da solo e solo per te stesso, la robustezza è in genere una preoccupazione molto più piccola rispetto a quando hai intenzione di scrivere un programma complesso che consiste di più componenti, magari scritti da un team. In tali sistemi, ci sono confini tra i componenti in forma di API pubbliche e, ad ogni limite, è spesso una buona idea convalidare i parametri di input, anche se "la logica del programma dovrebbe essere così solida che tali controlli sono ridondanti ". Ciò rende il rilevamento dei bug abbastanza più semplice e aiuta a ridurre i tempi di debug.

Nel tuo caso, devi decidere da solo quale tipo di ciclo di vita ti aspetti per il tuo programma. È un programma che prevedi di essere utilizzato e mantenuto per anni? Quindi aggiungere un controllo ridondante è probabilmente migliore, poiché non sarà improbabile che il codice venga refactored in futuro e che tu reade le tue writefunzioni possano essere utilizzate in un contesto diverso.

O è un piccolo programma solo per scopi di apprendimento o divertimento? Quindi quei doppi controlli non saranno necessari.

Nel contesto di "Clean Code", si potrebbe chiedere se un doppio controllo viola il principio DRY. In realtà, a volte lo fa, almeno in misura minore: la convalida dell'input può essere interpretata come parte della logica aziendale di un programma, e avere questo in due punti potrebbe portare ai soliti problemi di manutenzione causati dalla violazione di DRY. Robustezza rispetto a DRY è spesso un compromesso: la robustezza richiede ridondanza nel codice, mentre DRY cerca di minimizzare la ridondanza. E con l'aumentare della complessità del programma, la robustezza diventa sempre più importante dell'essere DRY nella convalida.

Infine, lasciami fare un esempio di cosa significhi nel tuo caso. Supponiamo che le tue esigenze cambino in qualcosa del genere

  • il programma deve funzionare anche con un argomento, il nome del file di input, se non viene fornito un nome per il file di output, viene automaticamente costruito dal nome del file di input sostituendo il suffisso.

Ciò rende probabilmente necessario modificare la doppia convalida in due punti? Probabilmente no, tale requisito comporta una modifica durante la chiamata argparse, ma nessuna modifica in writeToOutputFile: tale funzione richiederà comunque un nome file. Quindi, nel tuo caso, voterei per aver effettuato la convalida dell'input due volte, il rischio di ottenere problemi di manutenzione a causa della presenza di due punti di modifica è molto più basso del rischio di ottenere problemi di manutenzione a causa di errori mascherati causati da un numero insufficiente di controlli.


"... confini tra componenti in forma di API pubbliche ..." Osservo che "le classi superano i limiti" per così dire. Quindi è necessaria una classe; una classe di dominio aziendale coerente. Da questo PO sto deducendo che il principio onnipresente di "è semplice, quindi non serve una classe" è al lavoro qui. Potrebbe esserci una semplice classe che avvolge l '"oggetto primario", applicando regole di business come "un file deve avere un nome" che non solo ASCIUTTA il codice esistente ma lo mantiene ASCIUTTO in futuro.
radarbob,

@radarbob: ciò che ho scritto non è limitato a OOP o componenti in forma di classi. Questo vale anche per le librerie arbitrarie con un'API pubblica, orientata agli oggetti o meno.
Doc Brown,

5

La ridondanza non è il peccato. La ridondanza inutile è.

  1. Se readFromInputFile()e writeToOutputFile()sono funzioni pubbliche (e secondo le convenzioni di denominazione di Python lo sono poiché i loro nomi non iniziano con due caratteri di sottolineatura), un giorno le funzioni potrebbero essere utilizzate da qualcuno che ha evitato del tutto l'argparse. Ciò significa che quando tralasciano gli argomenti non riescono a vedere il tuo messaggio di errore argparse personalizzato.

  2. Se readFromInputFile()e writeToOutputFile()controlla i parametri stessi, puoi nuovamente mostrare un messaggio di errore personalizzato che spiega la necessità di nomi di file.

  3. Se readFromInputFile()e writeToOutputFile()non verificare i parametri stessi, non viene visualizzato alcun messaggio di errore personalizzato. L'utente dovrà capire da solo l'eccezione risultante.

Tutto si riduce a 3. Scrivi del codice che utilizza effettivamente queste funzioni evitando argparse e produce il messaggio di errore. Immagina di non aver guardato affatto dentro queste funzioni e di fidarti solo dei loro nomi per fornire abbastanza comprensione da usare. Quando è tutto ciò che sai, c'è un modo per essere confuso dall'eccezione? È necessario un messaggio di errore personalizzato?

Disattivare la parte del cervello che ricorda l'interno di quelle funzioni è difficile. Tanto che alcuni consigliano di scrivere il codice di utilizzo prima del codice che viene utilizzato. In questo modo si arriva al problema già sapendo come appaiono le cose dall'esterno. Non devi fare TDD per farlo, ma se fai TDD verrai già dall'esterno per primo.


4

La misura in cui rendi i tuoi metodi autonomi e riutilizzabili è una buona cosa. Ciò significa che i metodi dovrebbero perdonare ciò che accettano e dovrebbero avere risultati ben definiti (precisi in ciò che restituiscono). Ciò significa anche che dovrebbero essere in grado di gestire con garbo tutto ciò che è passato a loro e non fare ipotesi sulla natura dell'input, della qualità, dei tempi, ecc.

Se un programmatore ha l'abitudine di scrivere metodi che fanno ipotesi su ciò che è passato, sulla base di idee come "se questo è rotto, abbiamo cose più grandi di cui preoccuparsi" o "il parametro X non può avere valore Y perché il resto di il codice lo impedisce ", poi all'improvviso non hai più componenti indipendenti e disaccoppiati. I componenti dipendono essenzialmente dal sistema più ampio. Si tratta di una sorta di sottile accoppiamento stretto che porta all'aumento esponenziale del costo totale di proprietà all'aumentare della complessità del sistema.

Tieni presente che ciò può significare che stai convalidando le stesse informazioni più di una volta. Ma va bene. Ogni componente è responsabile della propria convalida a modo suo . Questa non è una violazione di DRY, perché le convalide sono fatte da componenti indipendenti disaccoppiati e una modifica alla convalida in uno non deve necessariamente essere replicata esattamente nell'altro. Non c'è ridondanza qui. X ha la responsabilità di controllare i propri input per le proprie esigenze e di passarne alcuni a Y. Y ha la propria responsabilità di verificare i propri input per le proprie esigenze .


1

Supponiamo di avere una funzione (in C)

void readInputFile (const char* path);

E non puoi trovare alcuna documentazione sul percorso. E poi guardi l'implementazione e dice

void readInputFile (const char* path)
{
    assert (path != NULL && strlen (path) > 0);

Questo non solo verifica l'input della funzione, ma dice anche all'utente della funzione che il percorso non può essere NULL o una stringa vuota.


0

In generale, il doppio controllo non è sempre positivo o negativo. Ci sono sempre molti aspetti della domanda nel tuo caso particolare da cui dipende la questione. Nel tuo caso:

  • Quanto è grande il programma? Più è piccolo, più è ovvio che il chiamante fa la cosa giusta. Quando il tuo programma diventa più grande, diventa più importante specificare esattamente quali sono i presupposti e i postcondizioni di ogni routine.
  • gli argomenti sono già controllati dal argparsemodulo. Spesso è una cattiva idea usare una biblioteca e poi fare il suo lavoro da soli. Perché usare la libreria allora?
  • Quanto è probabile che il tuo metodo venga riutilizzato in un contesto in cui il chiamante non controlla gli argomenti? Più è probabile, più è importante convalidare gli argomenti.
  • Che cosa succede se un argomento non va manca? Non trovare un file di input probabilmente interromperà l'elaborazione. Questa è probabilmente un'ovvia modalità di errore che è facile da correggere. Gli insidiosi tipi di errori sono quelli in cui il programma continua a funzionare allegramente e produce risultati errati senza che te ne accorga .

0

I tuoi doppi controlli sembrano essere in luoghi in cui vengono usati raramente. Quindi questi controlli stanno semplicemente rendendo il tuo programma più robusto:

Un controllo troppo non farà male, uno in meno potrebbe.

Tuttavia, se si esegue il controllo all'interno di un ciclo che si ripete spesso, è necessario pensare alla rimozione della ridondanza, anche se il controllo stesso non è nella maggior parte dei casi costoso rispetto a quanto segue dopo il controllo.


E dal momento che ce l'hai già, non vale la pena rimuoverlo, a meno che non sia in un ciclo o qualcosa del genere.
StarWeaver,

0

Forse potresti cambiare il tuo punto di vista:

Se qualcosa va storto, qual è il risultato? Danneggerà l'applicazione / l'utente?

Ovviamente si potrebbe sempre discutere, se più o meno controlli siano migliori o peggiori, ma questa è una domanda piuttosto scolastica. E poiché hai a che fare con software del mondo reale , ci sono conseguenze nel mondo reale.

Dal contesto che stai dando:

  • un file di input A
  • un file di output B

Presumo che si sta facendo trasformazione da A a B . Se A e B sono piccoli e la trasformazione è piccola, quali sono le conseguenze?

1) Hai dimenticato di specificare da dove leggere: Quindi il risultato è nulla . E i tempi di esecuzione saranno più brevi del previsto. Guardi il risultato - o meglio: cerchi un risultato mancante, vedi che hai invocato il comando in modo sbagliato, ricomincia da capo e tutto va di nuovo bene

2) Hai dimenticato di specificare il file di output. Ciò si traduce in diversi scenari:

a) L'ingresso viene letto immediatamente. La trasformazione ha inizio e il risultato dovrebbe essere scritto, ma invece si riceve un errore. A seconda del tempo, l'utente deve attendere (dipende dalla massa di dati che potrebbero essere elaborati) questo potrebbe essere fastidioso.

b) L'input viene letto passo dopo passo. Quindi il processo di scrittura termina immediatamente come in (1) e l'utente ricomincia da capo.

Il controllo sciatto potrebbe essere visto come OK in alcune circostanze. Dipende interamente dal tuo caso d'uso e dalle tue intenzioni.

Inoltre: dovresti evitare la paranoia e non fare troppi controlli doppi.


0

Direi che i test non sono ridondanti.

  • Hai due funzioni pubbliche che richiedono un nome file come parametro di input. È opportuno convalidare i loro parametri. Le funzioni potrebbero essere potenzialmente utilizzate in qualsiasi programma che necessiti della loro funzionalità.
  • Hai un programma che richiede due argomenti che devono essere nomi di file. Capita di usare le funzioni. È opportuno che il programma controlli i suoi parametri.

Mentre i nomi dei file vengono controllati due volte, vengono controllati per scopi diversi. In un piccolo programma in cui è possibile verificare che i parametri delle funzioni siano stati verificati, i controlli nelle funzioni potrebbero essere considerati ridondanti.

Una soluzione più solida avrebbe uno o due validatori di nome file.

  • Per un file di input, potresti voler verificare che il parametro abbia specificato un file leggibile.
  • Per un file di output, potresti voler verificare che il parametro sia un file scrivibile o un nome file valido che può essere creato e scritto.

Uso due regole per quando eseguire le azioni:

  • Falli il prima possibile. Questo funziona bene per le cose che saranno sempre richieste. Dal punto di vista di questo programma, questo è il controllo sui valori argv e le successive convalide nella logica dei programmi sarebbero ridondanti. Se le funzioni vengono spostate in una libreria, non sono più ridondanti, poiché la libreria non può fidarsi del fatto che tutti i chiamanti hanno convalidato i parametri.
  • Falli il più tardi possibile. Questo funziona estremamente bene per le cose che raramente saranno necessarie. Dal punto di vista di questo programma, si tratta dei controlli sui parametri della funzione.

0

Il controllo è ridondante. Per risolvere questo problema, è necessario rimuovere readFromInputFile e writeToOutputFile e sostituirli con readFromStream e writeToStream.

Nel punto in cui il codice riceve il flusso di file, sai di avere un flusso valido collegato a un file valido o qualsiasi altra cosa a cui un flusso può essere collegato. Questo evita controlli ridondanti.

Potresti quindi chiedere, beh, devi comunque aprire lo stream da qualche parte. Sì, ma ciò accade internamente nel metodo di analisi degli argomenti. Ci sono due controlli lì, uno per verificare che sia richiesto un nome file, l'altro è un controllo che il file indicato dal nome file sia valido nel contesto dato (es. Esiste un file di input, la directory di output è scrivibile). Questi sono diversi tipi di controlli, quindi non sono ridondanti e si verificano all'interno del metodo di analisi degli argomenti (perimetro dell'applicazione) piuttosto che all'interno dell'applicazione principale.

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.