Dovrei aggiungere il codice ridondante ora nel caso in cui possa essere necessario in futuro?


114

Giustamente o erroneamente, al momento sono convinto che dovrei sempre provare a rendere il mio codice il più robusto possibile, anche se questo significa aggiungere codice / controlli ridondanti che so che non saranno utili in questo momento, ma loro potrebbe essere un numero di anni x lungo la linea.

Ad esempio, sto attualmente lavorando su un'applicazione mobile con questo pezzo di codice:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
    }

    //2. Is rows empty? - This will be the case if this is the first appointment / some other unknown reason.
    if(rows.Count == 0)
    {
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

Guardando specificamente alla sezione due, so che se la sezione uno è vera, allora anche la sezione due sarà vera. Non riesco a pensare a nessun motivo per cui la sezione uno sarebbe falsa e la sezione due valuti vero, il che rende la seconda ifaffermazione ridondante.

Tuttavia, in futuro potrebbe esserci un caso in cui questa seconda ifaffermazione è effettivamente necessaria e per un motivo noto.

Alcune persone potrebbero guardarlo inizialmente e pensare che sto programmando pensando al futuro, il che è ovviamente una buona cosa. Ma conosco alcuni casi in cui questo tipo di codice mi ha "nascosto" bug. Significa che mi ci è voluto ancora di più per capire perché la funzione xyzsta facendo abcquando dovrebbe effettivamente farlo def.

D'altra parte, ci sono stati anche numerosi casi in cui questo tipo di codice ha reso molto, molto più semplice migliorare il codice con nuovi comportamenti, perché non devo tornare indietro e assicurarmi che tutti i controlli pertinenti siano in atto.

Ci sono generale regola empirica linee guida per questo tipo di codice? (Sarei anche interessato a sapere se questo sarebbe considerato una buona o cattiva pratica?)

NB: Questo potrebbe essere considerato simile a questa domanda, tuttavia a differenza di quella domanda, vorrei una risposta supponendo che non ci siano scadenze.

TLDR: dovrei spingermi al punto di aggiungere codice ridondante per renderlo potenzialmente più robusto in futuro?



95
Nello sviluppo del software cose che non dovrebbero mai accadere accadono continuamente.
Roman Reiner,

34
Se sai if(rows.Count == 0)che non accadrà mai, allora potresti sollevare un'eccezione quando succede - e quindi verificare perché la tua ipotesi è sbagliata.
Knut

9
Non pertinente alla domanda, ma sospetto che il tuo codice abbia un bug. Quando le righe sono nulle, viene creato un nuovo elenco e (immagino) gettato via. Ma quando le righe non sono nulle, viene modificato un elenco esistente. Una progettazione migliore potrebbe essere quella di insistere sul fatto che il client passi in un elenco che può essere vuoto o meno.
Theodore Norvell,

9
Perché rowsmai sarebbe nullo? Non c'è mai una buona ragione, almeno in .NET, per rendere nulla una raccolta. Vuoto , sicuro, ma non nullo . Darei un'eccezione se rowsè nullo, perché significa che c'è un intervallo logico da parte del chiamante.
Kyralessa,

Risposte:


176

Come esercizio, prima verifichiamo la tua logica. Sebbene, come vedremo, hai problemi più grandi di qualsiasi problema logico.

Chiamare la prima condizione A e la seconda condizione B.

Prima dici:

Guardando specificamente alla sezione due, so che se la sezione uno è vera, allora anche la sezione due sarà vera.

Cioè: A implica B, o, in termini più basilari (NOT A) OR B

E poi:

Non riesco a pensare a nessun motivo per cui la sezione uno sarebbe falsa e la sezione due valuterà il vero, il che rende la seconda istruzione if ridondante.

Cioè: NOT((NOT A) AND B). Applicare la legge di Demorgan per ottenere (NOT B) OR Aquale è B implica A.

Pertanto, se entrambe le affermazioni sono vere, allora A implica B e B implica A, il che significa che devono essere uguali.

Pertanto sì, i controlli sono ridondanti. Sembra che tu abbia quattro percorsi di codice attraverso il programma, ma in realtà ne hai solo due.

Quindi ora la domanda è: come scrivere il codice? La vera domanda è: qual è il contratto dichiarato del metodo ? La questione se le condizioni siano ridondanti è un'aringa rossa. La vera domanda è "ho progettato un contratto ragionevole e il mio metodo implementa chiaramente quel contratto?"

Diamo un'occhiata alla dichiarazione:

public static CalendarRow AssignAppointmentToRow(
    Appointment app,    
    List<CalendarRow> rows)

È pubblico, quindi deve essere robusto per i dati errati da chiamanti arbitrari.

Restituisce un valore, quindi dovrebbe essere utile per il suo valore di ritorno, non per il suo effetto collaterale.

Eppure il nome del metodo è un verbo, suggerendo che è utile per il suo effetto collaterale.

Il contratto del parametro list è:

  • Un elenco null è OK
  • Un elenco con uno o più elementi è OK
  • Un elenco senza elementi è sbagliato e non dovrebbe essere possibile.

Questo contratto è pazzo . Immagina di scrivere la documentazione per questo! Immagina di scrivere casi di prova!

Il mio consiglio: ricominciare. Questa API ha un'interfaccia Candy Machine scritta dappertutto. (L'espressione proviene da una vecchia storia delle macchine per caramelle di Microsoft, in cui sia i prezzi che le selezioni sono numeri a due cifre, ed è super facile digitare "85" che è il prezzo dell'articolo 75, e ottieni l'articolo sbagliato. Curiosità: sì, in realtà l'ho fatto per errore quando stavo cercando di far uscire la gomma da un distributore automatico di Microsoft!)

Ecco come progettare un contratto ragionevole:

Rendi il tuo metodo utile sia per il suo effetto collaterale o il suo valore di ritorno, non per entrambi.

Non accettare tipi mutabili come input, come elenchi; se hai bisogno di una sequenza di informazioni, prendi un IEnumerable. Leggi solo la sequenza; non scrivere in una raccolta passata a meno che non sia molto chiaro che questo è il contratto del metodo. Prendendo IEnumerable invii un messaggio al chiamante che non hai intenzione di mutare la loro raccolta.

Non accettare mai null; una sequenza nulla è un abominio. Richiede al chiamante di passare una sequenza vuota se ciò ha senso, mai e poi mai null.

Arresta immediatamente se il chiamante viola il tuo contratto, per insegnare loro che intendi affari e in modo che possano catturare i loro bug nei test, non nella produzione.

Progettare innanzitutto il contratto in modo che sia il più ragionevole possibile, quindi implementare chiaramente il contratto. Questo è il modo di progettare a prova di futuro.

Ora, ho parlato solo del tuo caso specifico e hai fatto una domanda generale. Quindi, ecco alcuni consigli generali aggiuntivi:

  • Se c'è un fatto che tu come sviluppatore puoi dedurre ma il compilatore non può, allora usa un'asserzione per documentare quel fatto. Se un altro sviluppatore, come il futuro tu o uno dei tuoi colleghi, viola tale ipotesi, l'affermazione vi dirà.

  • Ottieni uno strumento di copertura del test. Assicurati che i tuoi test coprano ogni riga di codice. Se c'è un codice scoperto, allora hai un test mancante o hai un codice morto. Il codice morto è sorprendentemente pericoloso perché di solito non è destinato a essere morto! Mi viene subito in mente l'incredibile difetto di sicurezza "goto fail" di Apple di un paio di anni fa.

  • Ottieni uno strumento di analisi statica. Cavolo, prendine diversi; ogni strumento ha la sua specialità particolare e nessuno è un superset degli altri. Fai attenzione a quando ti dice che c'è un codice irraggiungibile o ridondante. Ancora una volta, quelli sono probabilmente bug.

Se sembra che io stia dicendo: in primo luogo, progettare bene il codice e, in secondo luogo, testarlo per accertarsi che sia corretto oggi, beh, è ​​quello che sto dicendo. Fare queste cose renderà molto più facile affrontare il futuro; la parte più difficile del futuro è occuparsi di tutto il codice buggy candy machine che le persone hanno scritto in passato. Fallo subito oggi e i costi saranno più bassi in futuro.


24
Non ho mai pensato a metodi come questo prima, pensandoci ora mi sembra sempre di provare a coprire ogni eventualità, quando in realtà se la persona / metodo che chiama il mio metodo non mi passa ciò di cui ho bisogno, allora non dovrei Non tentare di correggere i loro errori (quando in realtà non so cosa intendessero), dovrei semplicemente schiantarmi. Grazie per questo, è stata appresa una lezione preziosa!
Kid

4
Il fatto che un metodo restituisca un valore non implica che non dovrebbe essere utile anche per il suo effetto collaterale. Nel codice concorrente, la capacità delle funzioni di restituire entrambe è spesso vitale (immagina CompareExchangesenza tale capacità!), E anche in scenari non simultanei qualcosa come "aggiungi un record se non esiste, e restituisce il pass-in record o quello esistente "può essere più conveniente di qualsiasi approccio che non impieghi sia un effetto collaterale che un valore di ritorno.
supercat

5
@KidCode Sì, Eric è davvero bravo a spiegare chiaramente le cose, anche alcuni argomenti davvero complicati. :)
Mason Wheeler,

3
@supercat Certo, ma è anche più difficile ragionare. In primo luogo, probabilmente vorrai guardare qualcosa che non modifica uno stato globale, evitando così sia i problemi di concorrenza che la corruzione dello stato. Quando questo non è ragionevole, vorrai comunque isolare i due: ciò ti dà chiari luoghi in cui la concorrenza è un problema (ed è quindi considerata extra pericolosa) e luoghi in cui viene gestita. Questa è stata una delle idee fondamentali nel documento OOP originale ed è al centro dell'utilità degli attori. Nessuna regola religiosa - preferisce solo separare i due quando ha senso. Di solito lo fa.
Luaan,

5
Questo è un post molto utile per quasi tutti!
Adrian Buzea,

89

Quello che stai facendo nel codice che stai mostrando sopra non è tanto una prova del futuro quanto una codifica difensiva .

Entrambe le ifaffermazioni testano cose diverse. Entrambi sono test appropriati a seconda delle tue esigenze.

La sezione 1 verifica e corregge un nulloggetto. Nota a margine : la creazione dell'elenco non crea alcun elemento figlio (ad esempio, CalendarRow).

La sezione 2 verifica e corregge gli errori dell'utente e / o di implementazione. Solo perché hai un List<CalendarRow>non significa che hai degli elementi nell'elenco. Gli utenti e gli implementatori faranno cose che non puoi immaginare solo perché sono autorizzati a farlo, che abbia senso per te o no.


1
In realtà sto prendendo "stupidi trucchi per gli utenti" per indicare l'input. SÌ, non dovresti mai fidarti dell'input. Anche appena fuori dalla tua classe. Convalidare! Se pensi che questa sia solo una preoccupazione futura, sarei felice di hackerarti oggi.
candied_orange

1
@CandiedOrange, quello era l'intento, ma il testo non trasmetteva il tentato umorismo. Ho modificato il testo.
Adam Zuckerman,

3
Solo una domanda veloce qui, se si tratta di un errore di implementazione / dati errati, non dovrei semplicemente andare in crash, invece di cercare di correggere i loro errori?
Kid

4
@KidCode Ogni volta che si tenta di ripristinare, "correggere i loro errori", è necessario fare due cose, tornare a un buono stato noto e non perdere silenziosamente input preziosi. Seguire questa regola in questo caso pone la domanda: un elenco di zero righe è un input prezioso?
candied_orange,

7
Non sono d'accordo con questa risposta. Se una funzione riceve un input non valido, significa per definizione che c'è un bug nel programma. L'approccio corretto è quello di generare un'eccezione su input non validi, in modo da scoprire il problema e correggere il bug. L'approccio che descrivi nasconderà solo bug che li rendono più insidiosi e più difficili da rintracciare. La codifica difensiva significa che non ti fidi automaticamente dell'input ma lo convalidi, ma non significa che dovresti fare ipotesi casuali per "correggere" input non validi o imprevisti.
JacquesB,

35

Immagino che questa domanda sia fondamentalmente da assaggiare. Sì, è una buona idea scrivere codice robusto, ma il codice nel tuo esempio è una leggera violazione del principio KISS (come sarà un sacco di tale codice "a prova di futuro").

Personalmente probabilmente non mi preoccuperei di rendere il codice a prova di proiettile per il futuro. Non conosco il futuro e, come tale, qualsiasi codice "futuro a prova di proiettile" è destinato a fallire miseramente quando il futuro arriva comunque.

Preferirei invece un approccio diverso: rendere esplicite le ipotesi rese esplicite utilizzando la assert()macro o strutture simili. In questo modo, quando il futuro si avvicina alla porta, ti dirà esattamente dove i tuoi presupposti non valgono più.


4
Mi piace il tuo punto di non sapere cosa riserva il futuro. In questo momento tutto quello che sto facendo è indovinare quale potrebbe essere un problema, quindi indovinare di nuovo per la soluzione.
Kid

3
@KidCode: buona osservazione. Il tuo pensiero qui è in realtà molto più intelligente di molte delle risposte qui, compresa quella che hai accettato.
Jacques

1
Mi piace questa risposta. Mantenere il codice minimo in modo che sia facile per un futuro lettore capire perché sono necessari eventuali controlli. Se un futuro lettore vede controlli per cose che sembrano inutili, potrebbe perdere tempo cercando di capire perché il controllo è lì. Un futuro umano potrebbe modificare questa classe, piuttosto che altri che la usano. Inoltre, non scrivere codice di cui non è possibile eseguire il debug , che sarà il caso se si tenta di gestire casi che al momento non possono verificarsi. (A meno che tu non scriva unit test che esercitano percorsi di codice che il programma principale non ha.)
Peter Cordes

23

Un altro principio a cui potresti voler pensare è l'idea di fallire velocemente . L'idea è che quando qualcosa va storto nel tuo programma, vuoi fermarlo del tutto immediatamente, almeno mentre lo stai sviluppando prima di rilasciarlo. In base a questo principio, vuoi scrivere molti controlli per assicurarti che i tuoi presupposti siano validi, ma considera seriamente che il tuo programma si fermi nelle sue tracce ogni volta che i presupposti vengono violati.

Per dirla in modo audace, se c'è anche un piccolo errore nel tuo programma, vuoi che si blocchi completamente mentre lo guardi!

Questo potrebbe sembrare controintuitivo, ma ti consente di scoprire i bug il più rapidamente possibile durante lo sviluppo di routine. Se stai scrivendo un pezzo di codice e pensi che sia finito, ma si arresta in modo anomalo quando lo collaudi, non c'è dubbio che non hai ancora finito. Inoltre, la maggior parte dei linguaggi di programmazione offre eccellenti strumenti di debug che sono più facili da usare quando il programma si arresta in modo anomalo invece di cercare di fare del suo meglio dopo un errore. L'esempio più grande e più comune è che se si arresta in modo anomalo il programma generando un'eccezione non gestita, il messaggio di eccezione ti dirà un'incredibile quantità di informazioni sul bug, incluso quale riga di codice non è riuscita e il percorso attraverso il codice che il programma ha intrapreso si dirige verso quella riga di codice (la traccia dello stack).

Per ulteriori riflessioni, leggi questo breve saggio: Non inchiodare il tuo programma in posizione verticale


Questo è rilevante per te perché è possibile che a volte i controlli che stai scrivendo siano lì perché vuoi che il tuo programma tenti di continuare a funzionare anche dopo che qualcosa è andato storto. Ad esempio, considera questa breve implementazione della sequenza di Fibonacci:

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

Funziona, ma cosa succede se qualcuno passa un numero negativo alla tua funzione? Allora non funzionerà! Quindi ti consigliamo di aggiungere un segno di spunta per assicurarti che la funzione venga chiamata con un input non negativo.

Potrebbe essere allettante scrivere la funzione in questo modo:

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    // Make sure the input is nonnegative
    if(n < 0) {
        n = 1; // Replace the negative input with an input that will work
    }

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

Tuttavia, se lo fai, in seguito chiamerai accidentalmente la tua funzione Fibonacci con un input negativo, non te ne accorgerai mai! Peggio ancora, il tuo programma probabilmente continuerà a funzionare ma inizierà a produrre risultati senza senso senza darti indizi su dove qualcosa sia andato storto. Questi sono i tipi più difficili di bug da correggere.

Invece, è meglio scrivere un assegno come questo:

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    // Make sure the input is nonnegative
    if(n < 0) {
        throw new ArgumentException("Can't have negative inputs to Fibonacci");
    }

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

Ora, se mai accidentalmente si chiama la funzione Fibonacci con un input negativo, il programma si interromperà immediatamente e ti farà sapere che qualcosa non va. Inoltre, dandoti una traccia dello stack, il programma ti farà sapere quale parte del tuo programma ha tentato di eseguire la funzione Fibonacci in modo errato, dandoti un eccellente punto di partenza per il debug di ciò che non va.


1
C # non ha un particolare tipo di eccezione per indicare argomenti non validi o argomenti fuori portata?
JDługosz,

@ JDługosz Yep! C # ha ArgumentException e Java ha IllegalArgumentException .
Kevin,

La domanda stava usando c #. Ecco C ++ (link al sommario) per completezza.
JDługosz,

3
"Fallire velocemente allo stato sicuro più vicino" è più ragionevole. Se si effettua l'applicazione in modo che si blocchi in caso di eventi imprevisti, si rischia di perdere i dati dell'utente. I luoghi in cui i dati dell'utente sono a rischio sono ottimi punti per la "penultima" gestione delle eccezioni (ci sono sempre casi in cui non si può fare altro che alzare le mani - il massimo crash). Il solo arresto anomalo del debug è solo l'apertura di un'altra lattina di worm: devi comunque testare ciò che stai distribuendo all'utente e ora stai spendendo metà del tuo tempo di test su una versione che l'utente non vedrà mai.
Luaan,

2
@Luaan: questa non è la responsabilità della funzione di esempio.
whatsisname

11

Dovresti aggiungere un codice ridondante? No.

Ma ciò che descrivi non è un codice ridondante .

Quello che descrivi è una programmazione difensiva contro la chiamata del codice che viola i presupposti della tua funzione. Se lo fai o lasci semplicemente agli utenti la lettura della documentazione ed evitare quelle stesse violazioni, è del tutto soggettivo.

Personalmente, sono un grande sostenitore di questa metodologia, ma come per tutto ciò che devi essere prudente. Prendi, ad esempio, C ++ std::vector::operator[]. Mettendo da parte l'implementazione della modalità debug di VS per un momento, questa funzione non esegue il controllo dei limiti. Se si richiede un elemento che non esiste, il risultato non è definito. Spetta all'utente fornire un indice vettoriale valido. Questo è abbastanza deliberato: puoi "optare" per il controllo dei limiti aggiungendolo al sito di chiamata, ma se l' operator[]implementazione dovesse eseguirlo, non saresti in grado di "rinunciare". Essendo una funzione di livello abbastanza basso, ha senso.

Ma se stavi scrivendo una AddEmployee(string name)funzione per qualche interfaccia di livello superiore, mi aspetto pienamente che questa funzione generi almeno un'eccezione se hai fornito un vuoto name, così come questa condizione preliminare è documentata immediatamente sopra la dichiarazione di funzione. Oggi potresti non fornire input utente non autorizzati a questa funzione, ma renderla "sicura" in questo modo significa che eventuali violazioni delle condizioni preliminari che compaiono in futuro possono essere facilmente diagnosticate, piuttosto che innescare potenzialmente una catena di domino di difficile rilevare bug. Questa non è ridondanza: è diligenza.

Se dovessi elaborare una regola generale (anche se, come regola generale, cerco di evitarli), direi che una funzione che soddisfa uno dei seguenti:

  • vive in un linguaggio di altissimo livello (diciamo JavaScript anziché C)
  • si trova al limite di un'interfaccia
  • non è critico per le prestazioni
  • accetta direttamente l'input dell'utente

... può beneficiare della programmazione difensiva. In altri casi, puoi ancora scrivere assertioni che si attivano durante il test ma sono disabilitati nelle build di rilascio, per aumentare ulteriormente la tua capacità di trovare bug.

Questo argomento viene approfondito ulteriormente su Wikipedia ( https://en.wikipedia.org/wiki/Defensive_programming ).


9

Due dei dieci comandamenti di programmazione sono rilevanti qui:

  • Non devi presumere che l'input sia corretto

  • Non devi creare codice per usi futuri

Qui, controllare per null non è "fare codice per un uso futuro". Creare codice per un uso futuro è come aggiungere interfacce perché pensi che possano essere utili "un giorno". In altre parole, il comandamento è di non aggiungere strati di astrazione a meno che non siano necessari in questo momento.

Il controllo di null non ha nulla a che fare con un uso futuro. Ha a che fare con il comandamento n. 1: non dare per scontato che l'input sia corretto. Non dare mai per scontato che la tua funzione riceva un sottoinsieme di input. Una funzione dovrebbe rispondere in modo logico, non importa quanto siano falsi e incasinati gli input.


5
Dove sono questi comandamenti di programmazione. Hai un link? Sono curioso perché ho lavorato su diversi programmi che si iscrivono al secondo di quei comandamenti e diversi che non lo hanno fatto. Invariabilmente, coloro che si sono iscritti al comandamento hanno Thou shall not make code for future useincontrato problemi di manutenibilità prima , nonostante la logica intuitiva del comandamento. Ho scoperto, nella codifica della vita reale, che il comandamento è efficace solo nel codice in cui controlli l'elenco delle funzionalità e le scadenze, e assicurati di non aver bisogno di un codice futuro per raggiungerli ... vale a dire, mai.
Cort Ammon,

1
Trivialmente dimostrabile: se si può stimare la probabilità di un uso futuro e il valore previsto del codice "uso futuro" e il prodotto di questi due è maggiore del costo dell'aggiunta del codice "uso futuro", è statisticamente ottimale aggiungi il codice. Penso che il comandamento si presenti in situazioni in cui gli sviluppatori (o i gestori) sono costretti ad ammettere che le loro capacità di stima non sono così affidabili come vorrebbero, quindi come misura difensiva scelgono semplicemente di non stimare affatto l'attività futura.
Cort Ammon,

2
@CortAmmon Non c'è posto per i comandamenti religiosi nella programmazione: le "migliori pratiche" hanno senso solo nel contesto e imparare una "migliore pratica" senza il ragionamento ti rende incapace di adattarti. Ho trovato YAGNI molto utile, ma ciò non significa che non penso a luoghi in cui l'aggiunta di punti di estensione in seguito sarà costosa, significa solo che non devo pensare ai casi semplici in anticipo. Naturalmente, questo cambia anche nel tempo, poiché sempre più presupposti vengono fissati sul tuo codice, aumentando efficacemente l'interfaccia del tuo codice: è un atto di bilanciamento.
Luaan,

2
@CortAmmon Il tuo caso "banalmente dimostrabile" ignora due costi molto importanti: il costo della stima stessa e i costi di manutenzione del punto di estensione (forse non necessario). È qui che le persone ottengono stime estremamente inaffidabili, sottostimando le stime. Per una funzionalità molto semplice, potrebbe essere sufficiente pensarci per qualche secondo, ma è abbastanza probabile che troverai un'intera lattina di worm che segue la funzione inizialmente "semplice". La comunicazione è la chiave: man mano che le cose si fanno grandi, devi parlare con il tuo leader / cliente.
Luaan,

1
@Luaan Stavo cercando di discutere per il tuo punto, non c'è posto per i comandamenti religiosi nella programmazione. Finché esiste un caso aziendale in cui i costi di stima e manutenzione del punto di estensione sono sufficientemente limitati, esiste un caso in cui detto "comandamento" è discutibile. Dalla mia esperienza con il codice, la questione se lasciare tali punti di estensione o meno non si è mai inserita bene in un comandamento a riga singola in un modo o nell'altro.
Cort Ammon,

7

La definizione di "codice ridondante" e "YAGNI" dipende spesso da quanto lontano guardi avanti.

Se si verifica un problema, si tende a scrivere codice futuro in modo tale da evitare tale problema. Un altro programmatore che non ha riscontrato quel particolare problema potrebbe considerare un eccesso di codice ridondante.

Il mio suggerimento è di tenere traccia di quanto tempo dedichi a "cose ​​che non hanno ancora commesso errori" se i suoi carichi e i tuoi colleghi sbattono le funzionalità più velocemente di te, quindi diminuiscile.

Tuttavia, se sei come me, mi aspetto che tu abbia appena scritto tutto 'per impostazione predefinita' e che non ti abbia impiegato molto più tempo.


6

È una buona idea documentare eventuali ipotesi sui parametri. Ed è una buona idea verificare che il codice del client non violi tali presupposti. Vorrei fare questo:

/** ...
*   Precondition: rows is null or nonempty
*/
public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    Assert( rows==null || rows.Count > 0 )
    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

[Supponendo che questo sia C #, Assert potrebbe non essere lo strumento migliore per il lavoro, poiché non è compilato nel codice rilasciato. Ma questo è un dibattito per un altro giorno.]

Perché è meglio di quello che hai scritto? Il tuo codice ha senso se, in un futuro in cui il tuo cliente è cambiato, quando il client passa in un elenco vuoto, la cosa giusta da fare sarà aggiungere una prima riga e aggiungere l'app ai suoi appuntamenti. Ma come fai a sapere che sarà così? È meglio fare meno ipotesi sul futuro.


5

Stimare il costo dell'aggiunta di quel codice ora . Sarà relativamente economico perché è tutto nuovo nella tua mente, quindi sarai in grado di farlo rapidamente. L'aggiunta di unit test è necessaria: non c'è niente di peggio che usare un metodo un anno dopo, non funziona e si capisce che è stato interrotto dall'inizio e non ha mai funzionato.

Stimare il costo dell'aggiunta di quel codice quando è necessario. Sarà più costoso, perché devi tornare al codice, ricordare tutto ed è molto più difficile.

Stimare la probabilità che il codice aggiuntivo sia effettivamente necessario. Quindi fai i conti.

D'altra parte, il codice completo con ipotesi "X non accadrà mai" è terribile per il debug. Se qualcosa non funziona come previsto, significa o uno stupido errore o un'ipotesi sbagliata. La tua "X non accadrà mai" è un presupposto, e in presenza di un bug è sospetto. Ciò costringe il prossimo sviluppatore a perdere tempo con esso. Di solito è meglio non fare affidamento su tali presupposti.


4
Nel tuo primo paragrafo, hai dimenticato di menzionare il costo di mantenimento di quel codice nel tempo, quando si scopre che la funzionalità effettivamente necessaria si esclude a vicenda con la funzionalità aggiunta inutilmente. . .
Ruakh,

È inoltre necessario stimare il costo dei bug che potrebbero insinuarsi nel programma perché non si fallisce l'inserimento non valido. Ma non è possibile stimare il costo dei bug poiché, per definizione, sono inattesi. Quindi l'intero "fai i conti" va in pezzi.
JacquesB,

3

La domanda principale qui è "cosa accadrà se lo fai / non lo fai"?

Come altri hanno sottolineato, questo tipo di programmazione difensiva è buono, ma a volte è anche pericoloso.

Ad esempio, se si fornisce un valore predefinito, il programma verrà mantenuto. Ma il programma ora potrebbe non fare quello che vuoi che faccia. Ad esempio, se scrive quell'array vuoto in un file, ora potresti aver trasformato il tuo bug da "crash perché ho fornito null per errore" a "cancella le righe del calendario perché ho fornito null per errore". (ad esempio se inizi a rimuovere elementi che non compaiono nell'elenco in quella parte che recita "// blah")

La chiave per me non sono mai i dati corrotti . Lasciami ripetere; MAI. Corrotto. DATI. Se il tuo programma fa eccezioni, ricevi una segnalazione di bug che puoi correggere; se scrive dati errati in un file che sarà in seguito, devi seminare il terreno con sale.

Tutte le tue decisioni "inutili" dovrebbero essere prese tenendo conto di questa premessa.


2

Quello di cui hai a che fare qui è fondamentalmente un'interfaccia. Aggiungendo il comportamento "quando l'input è null, inizializza l'input", hai effettivamente esteso l'interfaccia del metodo - ora invece di operare sempre su un elenco valido, l'hai fatto "aggiustare" l'input. Che si tratti della parte ufficiale o non ufficiale dell'interfaccia, puoi scommettere che qualcuno (molto probabilmente incluso te) userà questo comportamento.

Le interfacce dovrebbero essere mantenute semplici e dovrebbero essere relativamente stabili, specialmente in qualcosa di simile a un public staticmetodo. Ottieni un po 'di margine di manovra nei metodi privati, in particolare i metodi di istanza privata. Estendendo implicitamente l'interfaccia, in pratica hai reso il tuo codice più complesso. Ora, immagina di non voler utilizzare quel percorso di codice, quindi evitalo. Ora hai un po 'di codice non testato che finge di far parte del comportamento del metodo. E ora posso dirti che probabilmente ha un bug: quando passi un elenco, tale elenco viene modificato dal metodo. Tuttavia, se non lo fai, crei un localeelenco e gettalo via più tardi. Questo è il tipo di comportamento incoerente che ti farà piangere in sei mesi, mentre cerchi di rintracciare un insetto oscuro.

In generale, la programmazione difensiva è una cosa piuttosto utile. Ma i percorsi del codice per i controlli difensivi devono essere testati, proprio come qualsiasi altro codice. In un caso come questo, stanno complicando il tuo codice senza motivo, e opterei invece per un'alternativa come questa:

if (rows == null) throw new ArgumentNullException(nameof(rows));

Non si desidera un input dove rowssono nulli e si desidera rendere l'errore evidente a tutti i chiamanti il più presto possibile .

Ci sono molti valori che devi destreggiarti quando sviluppi software. Anche la robustezza in sé è una qualità molto complessa - per esempio, non riterrei il tuo controllo difensivo più robusto del lanciare un'eccezione. Le eccezioni sono piuttosto utili per darti un posto sicuro in cui riprovare da un posto sicuro: i problemi di corruzione dei dati sono in genere molto più difficili da rintracciare che riconoscere un problema all'inizio e gestirlo in modo sicuro. Alla fine, tendono solo a darti un'illusione di robustezza, e poi un mese dopo noti che un decimo dei tuoi appuntamenti sono andati, perché non hai mai notato che un elenco diverso è stato aggiornato. Ahia.

Assicurati di distinguere tra i due. La programmazione difensiva è una tecnica utile per rilevare gli errori in un luogo in cui sono i più rilevanti, aiutando in modo significativo i tuoi sforzi di debug e con una buona gestione delle eccezioni, prevenendo la "corruzione subdola". Fallire presto, fallire velocemente. D'altra parte, quello che stai facendo è più simile a "nascondere gli errori": stai manipolando input e facendo ipotesi sul significato del chiamante. Questo è molto importante per il codice rivolto all'utente (ad esempio il controllo ortografico), ma dovresti stare attento quando vedi questo con il codice rivolto agli sviluppatori.

Il problema principale è che qualunque cosa tu faccia astrazione, ti perderà ("Volevo digitare ofre, non prima! Stupido controllo ortografico!"), E il codice per gestire tutti i casi speciali e le correzioni è ancora codice è necessario mantenere e comprendere e il codice che è necessario testare. Confronta lo sforzo per assicurarti che venga passato un elenco non nullo con la correzione del bug che hai un anno dopo nella produzione - non è un buon compromesso. In un mondo ideale, vorresti che ogni metodo funzionasse esclusivamente con i suoi input, restituendo un risultato e modificando nessuno stato globale. Naturalmente, nel mondo reale, troverai molti casi in cui non lo èla soluzione più semplice e chiara (ad esempio quando si salva un file), ma trovo che mantenere i metodi "puri" quando non c'è motivo per loro di leggere o manipolare lo stato globale rende il codice molto più facile da ragionare. Tende anche a darti punti più naturali per dividere i tuoi metodi :)

Ciò non significa che tutto ciò che è inaspettato debba causare il crash dell'applicazione, al contrario. Se si utilizzano bene le eccezioni, formano naturalmente punti di gestione degli errori sicuri, in cui è possibile ripristinare uno stato dell'applicazione stabile e consentire all'utente di continuare ciò che stanno facendo (idealmente evitando qualsiasi perdita di dati per l'utente). In quei punti di gestione, vedrai opportunità per risolvere i problemi ("Nessun numero d'ordine 2212 trovato. Intendevi 2212b?") O dare all'utente il controllo ("Errore durante la connessione al database. Riprovare?"). Anche quando tale opzione non è disponibile, almeno ti darà la possibilità che nulla sia stato corrotto - ho iniziato ad apprezzare il codice che utilizza usinge try... finallymolto più di try...catch, ti offre molte opportunità di mantenere invarianti anche in condizioni eccezionali.

Gli utenti non dovrebbero perdere i loro dati e lavorare. Questo deve ancora essere bilanciato con i costi di sviluppo, ecc., Ma è una buona linea guida generale (se gli utenti decidono se acquistare il tuo software o meno - il software interno di solito non ha quel lusso). Anche l'intero crash dell'applicazione diventa un problema molto meno se l'utente può semplicemente riavviare e tornare a quello che stava facendo. Questa è la vera robustezza: Word salva continuamente il tuo lavoro senza corrompere il tuo documento su disco e offrirti un'opzioneper ripristinare tali modifiche dopo aver riavviato Word dopo un arresto anomalo. È meglio di un bug no in primo luogo? Probabilmente no, anche se non dimenticare che il lavoro speso per la cattura di un raro bug potrebbe essere speso meglio ovunque. Ma è molto meglio delle alternative: ad esempio un documento corrotto su disco, tutto il lavoro dall'ultimo salvataggio perso, documento sostituito automaticamente con le modifiche prima dell'arresto anomalo che erano appena state Ctrl + A ed Elimina.


1

Risponderò a questo in base al tuo presupposto che il robusto codice ti trarrà beneficio da "anni" da adesso. Se i tuoi benefici a lungo termine sono il tuo obiettivo, darei priorità al design e alla manutenibilità piuttosto che alla robustezza.

Il compromesso tra design e robustezza è tempo e attenzione. La maggior parte degli sviluppatori preferirebbe avere un set di codice ben progettato anche se ciò significa passare attraverso alcuni punti problematici e fare qualche condizione aggiuntiva o gestire gli errori. Dopo alcuni anni di utilizzo, i luoghi di cui hai davvero bisogno sono stati probabilmente identificati dagli utenti.

Supponendo che il design abbia la stessa qualità, meno codice è più facile da mantenere. Questo non significa che staremo meglio se lasci andare problemi noti per qualche anno, ma aggiungere cose che sai di non aver bisogno rende difficile. Abbiamo esaminato tutti il ​​codice legacy e trovato parti non necessarie. Devi avere un alto livello di fiducia nel cambiare il codice che funziona da anni.

Quindi, se ritieni che la tua app sia progettata nel miglior modo possibile, sia facile da mantenere e non abbia alcun bug, trova qualcosa di meglio da fare che aggiungere il codice che non ti serve. È il minimo che puoi fare per rispetto per tutti gli altri sviluppatori che lavorano a lungo con funzionalità inutili.


1

No, non dovresti. E in realtà stai rispondendo alla tua domanda quando affermi che questo modo di codificare può nascondere bug . Ciò non renderà il codice più robusto, ma lo renderà più incline ai bug e renderà più difficile il debug.

Dichiarate le vostre attuali aspettative rowssull'argomento: o è nullo o altrimenti ha almeno un elemento. Quindi la domanda è: è una buona idea scrivere il codice per gestire ulteriormente il terzo caso inaspettato in cui rowssono presenti zero elementi?

La risposta è no. Dovresti sempre generare un'eccezione in caso di input imprevisto. Considera questo: se alcune altre parti del codice infrangono le aspettative (cioè il contratto) del tuo metodo , significa che c'è un bug . Se c'è un bug che vuoi conoscere il più presto possibile in modo da poterlo risolvere, e un'eccezione ti aiuterà a farlo.

Ciò che il codice attualmente fa è indovinare come recuperare da un bug che potrebbe esistere o meno nel codice. Ma anche se c'è un bug, non puoi sapere come recuperarlo completamente. I bug per definizione hanno conseguenze sconosciute. Forse un codice di inizializzazione non eseguito come previsto potrebbe avere molte altre conseguenze oltre alla sola riga mancante.

Quindi il codice dovrebbe apparire così:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    if (rows != null && rows.Count == 0) throw new ArgumentException("Rows is empty."); 

    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

Nota: ci sono alcuni casi specifici in cui ha senso "indovinare" come gestire input non validi piuttosto che generare un'eccezione. Ad esempio se gestisci input esterni non hai alcun controllo. I browser Web sono un famigerato esempio perché cercano di gestire con grazia qualsiasi tipo di input non valido e non valido. Ma questo ha senso solo con input esterni, non con chiamate da altre parti del programma.


Modifica: alcune altre risposte affermano che stai facendo una programmazione difensiva . Non sono d'accordo. La programmazione difensiva significa che non ti fidi automaticamente che l'input sia valido. Quindi la convalida dei parametri (come sopra) è una tecnica di programmazione difensiva, ma non significa che dovresti modificare input imprevisti o non validi indovinando. Il solido approccio difensivo consiste nel convalidare l'input e quindi generare un'eccezione in caso di input imprevisto o non valido.


1

Dovrei aggiungere il codice ridondante ora nel caso in cui possa essere necessario in futuro?

Non è necessario aggiungere codice ridondante in qualsiasi momento.

Non aggiungere codice necessario solo in futuro.

Dovresti assicurarti che il tuo codice si comporti bene, qualunque cosa accada.

La definizione di "comportarsi bene" dipende dalle tue esigenze. Una tecnica che mi piace usare sono le eccezioni alla "paranoia". Se sono sicuro al 100% che un determinato caso non può mai accadere, continuo a programmare un'eccezione, ma lo faccio in modo che a) dica chiaramente a tutti che non mi aspetto che ciò accada, e b) sia chiaramente visualizzato e registrato e quindi non porta alla corruzione strisciante in seguito.

Esempio di pseudo-codice:

file = File.open(">", "bla")  or raise "Paranoia: cannot open file 'bla'"

file.write("xytz") or raise "Paranoia: disk full?"

file.close()  or raise "Paranoia: huh?!?!?"

Ciò comunica chiaramente che sono sicuro al 100% di poter sempre aprire, scrivere o chiudere il file, vale a dire che non vado al limite della creazione di un elaborato trattamento degli errori. Ma se (no: quando) non riesco ad aprire il file, il mio programma fallirà comunque in modo controllato.

Naturalmente l'interfaccia utente non visualizzerà tali messaggi all'utente, che verranno registrati internamente insieme alla traccia dello stack. Ancora una volta, si tratta di eccezioni interne alla "Paranoia" che assicurano solo che il codice "si fermi" quando accade qualcosa di imprevisto. Questo esempio è un po 'contorto, in pratica ovviamente implementerei la vera gestione degli errori per errori durante l'apertura dei file, poiché ciò accade regolarmente (nome file errato, chiavetta USB montata in sola lettura, qualunque cosa).

Un termine di ricerca correlato molto importante sarebbe "fail fast", come indicato in altre risposte ed è molto utile per creare software robusto.


-1

Ci sono molte risposte troppo complicate qui. Probabilmente hai fatto questa domanda perché non ti sentivi a tuo agio su quel codice pezzo ma non sapevi perché o come risolverlo. Quindi la mia risposta è che il problema è molto probabile nella struttura del codice (come sempre).

Innanzitutto, l'intestazione del metodo:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)

Assegnare un appuntamento a quale riga? Ciò dovrebbe essere immediatamente chiaro dall'elenco dei parametri. Senza ulteriori conoscenze, mi sarei aspettato le params metodo di simile a questa: (Appointment app, CalendarRow row).

Successivamente, i "controlli di input":

//1. Is rows equal to null? - This will be the case if this is the first appointment.
if (rows == null) {
    rows = new List<CalendarRow> ();
}

//2. Is rows empty? - This will be the case if this is the first appointment / some other unknown reason.
if(rows.Count == 0)
{
    rows.Add (new CalendarRow (0));
    rows [0].Appointments.Add (app);
}

Questa è una cazzata.

  1. check) Il chiamante del metodo deve semplicemente assicurarsi che non passi valori non inizializzati all'interno del metodo. È la responsabilità di un programmatore (da provare) di non essere stupido.
  2. verifica) Se non prendo in considerazione il fatto che il passaggio rowsal metodo è probabilmente errato (vedere il commento sopra), non dovrebbe essere responsabilità del metodo chiamato AssignAppointmentToRowper manipolare le righe in altro modo rispetto all'assegnazione dell'appuntamento da qualche parte.

Ma l'intero concetto di assegnare appuntamenti da qualche parte è strano (a meno che questa non sia una parte della GUI del codice). Il tuo codice sembra contenere (o almeno tenta di) una struttura di dati esplicita che rappresenta un calendario (ovvero List<CalendarRows><- che dovrebbe essere definito come Calendarda qualche parte se vuoi andare in questo modo, quindi passeresti Calendar calendaral tuo metodo). Se vai in questo modo, mi aspetto calendarche sia precompilato con gli slot in cui si posizionano (assegnano) gli appuntamenti in seguito (ad es. calendar[month][day] = appointmentSarebbe il codice appropriato). Ma poi puoi anche scegliere di abbandonare completamente la struttura del calendario dalla logica principale e avere solo List<Appointment>dove gli Appointmentoggetti contengono l'attributodate. Quindi, se è necessario eseguire il rendering di un calendario da qualche parte nella GUI, è possibile creare questa struttura di "calendario esplicito" appena prima del rendering.

Non conosco i dettagli della tua app, quindi parte di questo forse non è applicabile a te, ma entrambi questi controlli (principalmente il secondo) mi dicono che qualcosa da qualche parte è sbagliato nella separazione delle preoccupazioni nel tuo codice.


-2

Per semplicità, supponiamo che alla fine avrai bisogno di questo pezzo di codice in N giorni (non più tardi o prima) o che non ti servirà affatto.

pseudocodice:

let C_now   = cost of implementing the piece of code now
let C_later = ... later
let C_maint = cost of maintaining the piece of code one day
              (note that it can be negative)
let P_need  = probability that you will need the code after N days

if C_now + C_maint * N < P_need*C_later then implement the code else omit it.

Fattori per C_maint:

  • Migliora il codice in generale, rendendolo più auto-documentante, più facile da testare? Se sì, negativo C_maintprevisto
  • Rende il codice più grande (quindi più difficile da leggere, più lungo da compilare, implementare test, ecc.)?
  • Sono in corso rifatturazioni / riprogettazioni? Se sì, bump C_maint. Questo caso richiede una formula più complessa, con più volte variabili come N.

Una cosa così grande che appesantisce il codice e potrebbe essere necessaria solo in 2 anni con bassa probabilità dovrebbe essere lasciata fuori, ma una piccola cosa che produce anche affermazioni utili e il 50% che sarà necessaria in 3 mesi, dovrebbe essere implementata .


È inoltre necessario tenere conto del costo dei bug che potrebbero insinuarsi nel programma perché non si stanno rifiutando input non validi. Quindi, come stimare il costo di potenziali bug difficili da trovare?
JacquesB,
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.