2009-06-07 10 views
6

Disclaimer: Sono un laico che attualmente sta imparando a programmare. Mai stato parte di un progetto, né scritto qualcosa di più lungo di ~ 500 linee.La programmazione difensiva viola il principio DRY?

La mia domanda è: la programmazione difensiva viola il principio Non ripetere te stesso? Supponendo che la mia definizione di programmazione difensiva sia corretta (se la funzione di chiamata convalida l'input anziché l'opposto), non sarebbe dannoso per il tuo codice?

per esempio, è questa cattiva:

int foo(int bar) 
{ 
    if (bar != /*condition*/) 
    { 
     //code, assert, return, etc. 
    } 
} 

int main() 
{ 
    int input = 10; 
    foo(input); //doesn't the extra logic 
    foo(input); //and potentially extra calls 
    foo(input); //work against you? 
} 

rispetto a questo:

int main() 
{ 
    if (input == /*condition*/) 
    { 
     foo(input); 
     foo(input); 
     foo(input); 
    } 
} 

Ancora una volta, come un laico, non so quanto contano le dichiarazioni logiche semplici contro di voi, per quanto come le prestazioni vanno, ma sicuramente la programmazione difensiva non è buona per il programma o l'anima.

+0

Come si riferisce a C++? – xtofl

+0

Spiacente, rimuovi quel tag se vuoi..Perché l'ho usato negli esempi però. – jkeys

+0

Ho scritto questo quando stavo insegnando a me stesso all'età di 15 anni. Probabilmente ora conosco meno parole strani, ma ho scritto programmi più sostanziali. Due passi avanti suppongo ... – jkeys

risposta

6

Tutto si riduce al contratto l'interfaccia fornisce. Ci sono due diversi scenari per questo: input e output.

Gli input, e di conseguenza sostanzialmente i parametri per le funzioni, devono essere controllati dall'implementazione come regola generale.

Le uscite - essendo i risultati di ritorno - dovrebbero essere sostanzialmente attendibili dal chiamante, almeno a mio parere.

Tutto questo è attenuato da questa domanda: cosa succede se una parte rompe il contratto? Ad esempio, consente di dire che hai avuto un'interfaccia:

class A { 
    public: 
    const char *get_stuff(); 
} 

e che contratto specifica che una stringa nulla sarà mai restituito (sarà una stringa vuota nel peggiore dei casi), allora è sicuro di fare questo:

A a = ... 
char buf[1000]; 
strcpy(buf, a.get_stuff()); 

Perché? Bene, se ti sbagli e il callee restituisce un null allora il programma andrà in crash. Quello è effettivamente OK. Se qualche oggetto viola il suo contratto, in generale il risultato dovrebbe essere catastrofico.

Il rischio che si prova ad essere eccessivamente difensivi è che si scrive un sacco di codice non necessario (che può introdurre più bug) o che si potrebbe effettivamente mascherare un problema serio inghiottendo un'eccezione che in realtà non si dovrebbe.

Naturalmente le circostanze possono cambiare questo.

+2

+1 Tutto si riduce al contratto che l'interfaccia fornisce. Programma difensivo! = Problemi di mascheramento, programma difensivo == esponendo i problemi generando eccezioni se i contratti vengono violati. – TimW

4

Lasciatemi dire prima che seguire ciecamente un principio è idealistico e SBAGLIATO. È necessario ottenere ciò che si desidera ottenere (ad esempio, la sicurezza della propria applicazione), che di solito è molto più importante della violazione di DRY. Le violazioni intenzionali dei principi sono spesso necessarie nella programmazione GOOD.

Un esempio: io faccio doppi controlli alle fasi importanti (ad es LoginService - prima convalidare l'input una volta prima di chiamare LoginService.Login, e poi di nuovo dentro), ma a volte tendo a rimuovere quello esterno in un secondo momento dopo che ho fatto in modo che tutto funziona al 100%, solitamente usando i test unitari. Dipende.

Non mi sarei mai svegliato con il doppio controllo. FORGETTARLI interamente d'altra parte è di solito più grandezze peggiori :)

1

Nell'esempio semplificato, sì, il secondo formato è probabilmente preferibile.

Tuttavia, ciò non vale in realtà per i programmi più grandi, più complessi e più realistici.

Perché non si sa mai in anticipo dove o come verrà utilizzato "foo", è necessario proteggere foo convalidando l'input. Se l'input è convalidato dal chiamante (ad esempio "main" nel tuo esempio), allora "main" deve conoscere le regole di convalida e applicarle.

Nella programmazione del mondo reale, le regole di convalida dell'input possono essere abbastanza complesse. Non è opportuno che il chiamante conosca tutte le regole di convalida e le applichi correttamente. Alcuni chiamanti, da qualche parte, dimenticheranno le regole di convalida o eseguiranno quelle sbagliate. Quindi è meglio mettere la convalida in "pippo", anche se verrà ripetutamente chiamata. Questo sposta l'onere dal chiamante al chiamato, che libera il chiamante a pensare meno ai dettagli di "foo", e lo usa più come un'interfaccia astratta e affidabile.

Se avete veramente un modello in cui "foo" verrà chiamato più volte con lo stesso ingresso, mi permetto di suggerire una funzione wrapper che fa la convalida una volta, e una versione non protetta quel lato-passi la validazione:

void RepeatFoo(int bar, int repeatCount) 
{ 
    /* Validate bar */ 
    if (bar != /*condition*/) 
    { 
     //code, assert, return, etc. 
    } 

    for(int i=0; i<repeatCount; ++i) 
    { 
     UnprotectedFoo(bar); 
    } 
} 

void UnprotectedFoo(int bar) 
{ 
    /* Note: no validation */ 

    /* do something with bar */ 
} 

void Foo(int bar) 
{ 
    /* Validate bar */ 
    /* either do the work, or call UnprotectedFoo */ 
} 
+1

Meglio esporre BarIsValid() e consentire al chiamante di scrivere il proprio ciclo. Quindi hai un'API minima: BarIsValid() e Foo(), e non sarà necessario estendere l'API quando un utente desidera un ciclo più complicato. PS: Penso che RepeatBar non dovrebbe convalidare la barra quando repeatCount è 0. –

9

violando il principio DRY sembra che:

int foo(int bar) 
{ 
    if (bar != /*condition*/) 
    { 
     //code, assert, return, etc. 
    } 
} 

int main() 
{ 
    int input = 10; 
    if (input == /*condition*/) 
    { 
     foo(input); 
     foo(input); 
     foo(input); 
    } 
} 

come si può vedere, il problema è che abbiamo lo stesso controllo due volte nel programma, quindi se cambia la condizione, dobbiamo modificarlo in due punti e è probabile che ce ne dimentichiamo uno, causando stranezze comportamento. DRY non significa "non eseguire lo stesso codice due volte", ma "non scrivere lo stesso codice due volte"

+0

Penso che ciò che Hooked voleva dire non lo stava controllando 2 volte ma solo nella funzione main(). –

+0

questo è un esempio OK. sebbene si possa discutere se la funzione 'if' nella funzione' pippo' fosse un'asserzione, che fa parte di una "difesa"; allora saremmo in qualche modo, ripetendo noi stessi, ma meno, ed entrambi sarebbero effettivamente necessari. –

1

Come ha detto Alex, dipende dalla situazione, ad esempio, quasi sempre convalido l'input a ogni fase del processo di accesso.

In altri posti, non è necessario tutto questo.

Tuttavia, nell'esempio che hai dato, sto assumendo, nel secondo esempio, che hai più di un input, perché altrimenti sarà ridondante chiamando la stessa funzione 3 volte per lo stesso input che significa dovrai scrivere la condizione 3 volte. Ora QUELLO è ridondante.

Se l'ingresso SEMPRE deve essere controllato, inserirlo nella funzione.

3

Penso che la programmazione difensiva diventi un brutto colpo, dal momento che fa cose che sono un po 'indesiderabili, che includono il codice wordy e, in modo più significativo, la carta sugli errori.

La maggior parte delle persone sembra concordare sul fatto che un programma debba fallire rapidamente quando incontra un errore, ma che i sistemi mission critical dovrebbero preferibilmente non fallire, e invece fanno di tutto per andare avanti di fronte agli stati di errore.

C'è un problema con questa affermazione, ovviamente, come può un programma, anche mission critical, continuare quando si trova in uno stato incoerente. Certo che non può, davvero.

Quello che vuoi è che il programma compia ogni passo ragionevole per fare la cosa giusta, anche se c'è qualcosa di strano in corso. Allo stesso tempo, il programma dovrebbe protestare, a voce alta, ogni volta che incontra uno stato così strano.E nel caso in cui incontri un errore irrecuperabile, di solito dovrebbe evitare di emettere un'istruzione HLT, piuttosto dovrebbe fallire con garbo, arrestando i suoi sistemi in modo sicuro o attivando qualche sistema di backup se disponibile.

+0

Sono completamente d'accordo. Penso che la logica difensiva debba essere messa al suo posto per essere utile, proprio come qualsiasi altra cosa. Usarlo ovunque (ad esempio i membri privati ​​incapsulati) sembra eccessivo per me e può davvero ostacolare la capacità di scrivere codice succinto ed elegante. –