È cattivo stile controllare una condizione in modo ridondante?


10

Ricevo spesso posizioni nel mio codice in cui mi ritrovo a controllare ripetutamente una condizione specifica.

Voglio darvi un piccolo esempio: supponiamo che ci sia un file di testo che contiene linee che iniziano con "a", linee che iniziano con "b" e altre linee e in realtà voglio lavorare solo con i primi due tipi di linee. Il mio codice sarebbe simile a questo (usando python, ma leggendolo come pseudocodice):

# ...
clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if (line.startsWith("a")):
        # do stuff
    elif (line.startsWith("b")):
        # magic
    else:
        # this else is redundant, I already made sure there is no else-case
        # by using clear_lines()
# ...

Potete immaginare che non controllerò solo questa condizione qui, ma forse anche in altre funzioni e così via.

Lo pensi come un rumore o aggiunge un valore al mio codice?


5
Fondamentalmente si tratta di codificare in modo difensivo o meno. Vedi questo codice essere modificato molto? È probabile che questo farà parte di un sistema che deve essere estremamente affidabile? Non vedo molto male spingendoci assert()dentro per aiutare con i test, ma oltre a ciò è probabilmente eccessivo. Detto questo, varierà a seconda della situazione.
Latty,

il tuo caso 'else' è essenzialmente un codice morto / irraggiungibile. Verificare che non vi siano requisiti di sistema che lo vietino.
NWS,

@NWS: stai dicendo che dovrei tenere il caso contrario? Mi dispiace non ti capisco completamente.
marktani,

2
non particolarmente legato alla domanda - ma trasformerei questa 'asserzione' in un invariante - che richiederebbe una nuova classe "Linea" (forse con classi derivate per A e B), piuttosto che trattare le linee come stringhe e dire loro cosa rappresentano dall'esterno. Sarei felice di approfondire su CodeReview
MattDavey il

intendevi elif (line.startsWith("b"))? a proposito, puoi tranquillamente rimuovere quelle parentesi circostanti dalle condizioni, non sono idiomatiche in Python.
tokland

Risposte:


14

Questa è una pratica eccezionalmente comune e il modo di affrontarla è attraverso filtri di ordine superiore .

In sostanza, si passa una funzione al metodo di filtro, insieme all'elenco / sequenza su cui si desidera filtrare e l'elenco / sequenza risultante contiene solo gli elementi desiderati.

Non ho familiarità con la sintassi di Python (sebbene contenga una funzione come quella mostrata nel link sopra), ma in c # / f # sembra che:

C #:

var linesWithAB = lines.Where(l => l.StartsWith("a") || l.StartsWith("b"));
foreach (var line in linesWithAB)
{
    /* line is guaranteed to ONLY start with a or b */
}

f # (presuppone che sia numerabile, altrimenti verrebbe utilizzato List.filter):

let linesWithAB = lines
    |> Seq.filter (fun l -> l.StartsWith("a") || l.StartsWith("b"))

for line in linesWithAB do
    /* line is guaranteed to ONLY start with a or b */

Quindi, per essere chiari: se usi codice / schemi provati e testati, è uno stile cattivo. Ciò, e la mutazione dell'elenco in memoria nel modo in cui appari tramite clear_lines () perde la sicurezza del thread e ogni speranza di parallelismo che avresti potuto avere.


3
Come una nota, la sintassi Python per questo sarebbe un generatore di espressione: (line for line in lines if line.startswith("a") or line.startswith("b")).
Latty,

1
+1 per sottolineare che l'implementazione (non necessaria) imperativa clear_linesè davvero una cattiva idea. In Python probabilmente useresti generatori per evitare di caricare il file completo in memoria.
tokland

Cosa succede quando il file di input è più grande della memoria disponibile?
Blrfl,

@Blrfl: beh, se il termine generatore è coerente tra c # / f # / python, ciò che @tokland e @Lattyware traducono in c # / f # rendimento e / o rendimento! dichiarazioni. È un po 'più ovvio nel mio esempio f # perché Seq.filter può essere applicato solo alle raccolte di IEnumerable <T> ma entrambi gli esempi di codice funzioneranno se linesè una raccolta generata.
Steven Evers,

@mcwise: quando inizi a guardare tutte le altre funzioni disponibili che funzionano in questo modo, inizia a diventare davvero sexy e incredibilmente espressiva perché possono essere tutte incatenate e composte insieme. Guarda skip, take, reduce( aggregateNET), map( selectNET), e non c'è più, ma questo è un inizio davvero solido.
Steven Evers,

14

Di recente ho dovuto implementare un programmatore di firmware utilizzando il formato record S Motorola , molto simile a quello che descrivi. Dal momento che abbiamo avuto un po 'di pressione, la mia prima bozza ha ignorato i licenziamenti e ha semplificato in base al sottoinsieme che effettivamente avevo bisogno di usare nella mia applicazione. Superò facilmente i miei test, ma fallì non appena qualcun altro lo provò. Non c'era idea di quale fosse il problema. È arrivato fino in fondo ma alla fine è fallito.

Quindi non ho avuto altra scelta che implementare tutti i controlli ridondanti, al fine di restringere il problema. Successivamente, mi ci sono voluti circa due secondi per trovare il problema.

Mi ci sono volute forse due ore in più per farlo nel modo giusto, ma ho perso un giorno di tempo anche per la risoluzione dei problemi. È molto raro che alcuni cicli del processore valgano un giorno di sprechi risoluzione dei problemi.

Detto questo, per quanto riguarda la lettura dei file, è spesso utile progettare il software in modo che funzioni con la lettura e l'elaborazione di una riga alla volta, anziché leggere l'intero file in memoria ed elaborarlo in memoria. In questo modo funzionerà ancora su file molto grandi.


"È molto raro che alcuni cicli del processore valgano un giorno di sprechi risoluzione dei problemi". Grazie per la risposta, hai un buon punto.
marktani,

5

È possibile sollevare un'eccezione nel elsecaso. In questo modo non è ridondante. Le eccezioni non sono cose che non dovrebbero accadere ma sono comunque controllate.

clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if (line.startsWith("a)):
        # do stuff
    if (line.startsWith("b")):
        # magic
    else:
        throw BadLineException
# ...

Direi che quest'ultima è una cattiva idea, in quanto è meno esplicita - se in seguito decidi di aggiungere una "c", potrebbe essere meno chiara.
Latty,

Il primo suggerimento ha valore ... il secondo (supponiamo che "b") sia una cattiva idea
Andrew

@Lattyware Ho migliorato la risposta. Grazie per i tuoi commenti
Tulains Córdova,

1
@Andrew ho migliorato la risposta. Grazie per i tuoi commenti
Tulains Córdova,

3

Nella progettazione per contratto , si suppone che ogni funzione debba svolgere il proprio lavoro come descritto nella sua documentazione. Pertanto, ogni funzione ha un elenco di pre-condizioni, ovvero condizioni sugli input della funzione e post-condizioni, ovvero condizioni dell'output della funzione.

La funzione deve garantire ai suoi clienti che, se gli input rispettano le pre-condizioni, l'output sarà come descritto dalle post-condizioni. Se almeno una delle condizioni preliminari non viene rispettata, la funzione può fare quello che vuole (crash, restituire qualsiasi risultato, ...). Pertanto pre e post-condizioni sono una descrizione semantica della funzione.

Grazie al contratto, una funzione è sicura che i suoi clienti la utilizzino correttamente e un client è sicuro che la funzione svolga correttamente il suo lavoro.

Alcune lingue gestiscono i contratti in modo nativo o attraverso un framework dedicato. Per gli altri, la cosa migliore è controllare le condizioni pre e post grazie alle affermazioni, come ha affermato @Lattyware. Ma non chiamerei quella programmazione difensiva, poiché nella mia mente questo concetto è più focalizzato sulla protezione contro gli input (umani) dell'utente.

Se si sfruttano i contratti, è possibile evitare la condizione di ridondanza verificata poiché la funzione chiamata funziona perfettamente e non è necessario il doppio controllo, oppure la funzione chiamata è disfunzionale e la funzione chiamante può comportarsi come vuole.

La parte più difficile è quindi definire quale funzione è responsabile di cosa e documentare rigorosamente questi ruoli.


1

In realtà non hai bisogno di clear_lines () all'inizio. Se la linea non è né "a" o "b", i condizionali semplicemente non si innescheranno. Se vuoi sbarazzarti di quelle linee, trasforma il resto in clear_line (). Così com'è, stai facendo due passaggi nel tuo documento. Se salti clear_lines () all'inizio e lo fai come parte del ciclo foreach, riduci il tempo di elaborazione a metà.

Non è solo cattivo stile, è cattivo dal punto di vista computazionale.


2
Potrebbe essere che quelle linee vengano utilizzate per qualcos'altro, e devono essere trattate prima di occuparsi delle "a"/ "b"righe. Non dire che è probabile (il nome chiaro implica che vengono scartati), solo che c'è una possibilità che sia necessaria. Se tale insieme di linee verrà ripetutamente ripetuto in futuro, potrebbe anche essere utile rimuoverle in anticipo per evitare molte ripetizioni inutili.
Latty,

0

Se davvero vuoi fare qualcosa se trovi una stringa non valida (ad esempio il testo di debug dell'output), direi che va assolutamente bene. Un paio di righe extra e qualche mese lungo quando smette di funzionare per qualche motivo sconosciuto puoi guardare l'output per scoprire perché.

Se, tuttavia, è sicuro ignorarlo, o sai per certo che non otterrai mai una stringa non valida, non è necessario il ramo aggiuntivo.

Personalmente sono sempre per aver inserito almeno un output di traccia per qualsiasi condizione imprevista - rende la vita molto più semplice quando hai un bug con l'output allegato che ti dice esattamente cosa è andato storto.


0

... supponiamo che ci sia un file di testo che contiene linee che iniziano con "a", linee che iniziano con "b" e altre linee e in realtà voglio solo lavorare con i primi due tipi di linee. Il mio codice sarebbe simile a questo (usando python, ma leggendolo come pseudocodice):

# ...
clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if ...

Odio le if...then...elsecostruzioni. Eviterei l'intero problema:

process_lines_by_first_character (lines,  
                                  'a' => { |line| ... a code ... },
                                  'b' => { |line| ... b code ... } )
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.