contratti / asserzioni di codice: cosa succede con i controlli doppi?


10

Sono un grande fan della scrittura di asserzioni, contratti o qualsiasi tipo di controllo disponibile nella lingua che sto usando. Una cosa che mi preoccupa un po 'è che non sono sicuro di quale sia la pratica comune per gestire i controlli doppi.

Esempio di situazione: scrivo innanzitutto la seguente funzione

void DoSomething( object obj )
{
  Contract.Requires<ArgumentNullException>( obj != null );
  //code using obj
}

poi poche ore dopo scrivo un'altra funzione che chiama la prima. Poiché tutto è ancora fresco in memoria, decido di non duplicare il contratto, poiché so che DoSomethingverificherò già un oggetto null:

void DoSomethingElse( object obj )
{
  //no Requires here: DoSomething will do that already
  DoSomething( obj );
  //code using obj
}

Il problema ovvio: DoSomethingElseora dipende dalla DoSomethingverifica che obj non sia nullo. Quindi dovrei DoSomethingmai decidere di non controllare più, o se decido di usare un'altra funzione, obj potrebbe non essere più controllato. Il che mi porta a scrivere questa implementazione dopo tutto:

void DoSomethingElse( object obj )
{
  Contract.Requires<ArgumentNullException>( obj != null );
  DoSomething( obj );
  //code using obj
}

Sempre sicuro, nessuna preoccupazione, tranne che se la situazione cresce lo stesso oggetto potrebbe essere verificato più volte ed è una forma di duplicazione e sappiamo tutti che non è così buono.

Qual è la pratica più comune per situazioni come queste?


3
ArgumentBullException? Questo è nuovo :)
un CVn

lol @ le mie abilità di battitura ... lo modificherò.
stijn

Risposte:


13

Personalmente verificherei la presenza di null in qualsiasi funzione che fallirà se ottiene un null e non in nessuna funzione che non lo farà.

Quindi nel tuo esempio sopra, se doSomethingElse () non ha bisogno di dereference obj, allora non controllerei obj per null lì.

Se DoSomething () non fa obiezioni, allora dovrebbe controllare null.

Se entrambe le funzioni lo differenziano, dovrebbero controllare entrambe. Quindi, se DoSomethingElse dereferences obj, allora dovrebbe verificare la presenza di null, ma DoSomething dovrebbe comunque verificare la presenza di null poiché potrebbe essere chiamato da un altro percorso.

In questo modo puoi lasciare il codice abbastanza pulito e garantire comunque che i controlli siano nella posizione corretta.


1
Sono completamente d'accordo. I presupposti di ciascun metodo dovrebbero essere autonomi. Immagina di riscrivere in modo DoSomething()tale che la condizione preliminare non sia più necessaria (improbabile in questo caso particolare, ma potrebbe verificarsi in una situazione diversa) e rimuovere il controllo delle condizioni preliminari. Ora un metodo apparentemente totalmente non correlato è rotto a causa del presupposto mancante. Prenderò un po 'di duplicazione del codice per chiarezza su strani errori del genere dal desiderio di salvare alcune righe di codice, ogni giorno.
un CVn

2

Grande! Vedo che hai scoperto i contratti di codice per .NET. I contratti di codice vanno molto oltre le asserzioni medie, di cui il controllo statico è l'esempio migliore. Questo potrebbe non essere disponibile se non hai installato Visual Studio Premium o superiore, ma è importante capire l'intenzione che sta dietro se intendi utilizzare Contratti di codice.

Quando si applica un contratto a una funzione, si tratta letteralmente di un contratto . Tale funzione garantisce di comportarsi secondo il contratto ed è garantita per essere utilizzata solo come definito dal contratto.

Nel tuo esempio dato, la DoSomethingElse()funzione non è all'altezza del contratto come specificato da DoSomething(), poiché può essere passato null e il controllo statico indicherà questo problema. Il modo per risolverlo è aggiungendo lo stesso contratto a DoSomethingElse().

Ora, questo significa che ci sarà la duplicazione, ma questa duplicazione è necessaria quando si sceglie di esporre la funzionalità tra due funzioni. Queste funzioni, sebbene private, possono essere chiamate anche da diversi luoghi della tua classe, quindi l'unico modo per garantire che da una determinata chiamata l'argomento non sarà mai nullo è duplicare i contratti.

Questo dovrebbe farti riconsiderare perché hai diviso il comportamento in due funzioni in primo luogo. È sempre stata la mia opinione ( contrariamente alla credenza popolare ) che non dovresti dividere le funzioni che sono chiamate solo da un posto . Esponendo l'incapsulamento applicando i contratti questo diventa ancora più evidente. Sembra che abbia trovato un'ulteriore argomentazione per la mia causa! Grazie! :)


per quanto riguarda il tuo ultimo paragrafo: nel codice attuale entrambe le funzioni erano membri di due classi diverse, motivo per cui sono divise. A parte questo, ero nella seguente situazione tante volte: scrivere una funzione lunga, decidere di non dividerla. Più tardi scoprirai che parte della logica è duplicata da qualche altra parte, quindi dividila comunque. O un anno dopo, rileggilo e trovalo illeggibile, quindi dividilo comunque. O durante il debug: funzioni split = meno colpi sul tasto F10. Ci sono più ragioni, quindi personalmente preferisco dividere, anche se a volte potrebbe essere troppo estremo.
stijn

(1) "In seguito scoprirai che parte della logica è duplicata da qualche altra parte" . Ecco perché trovo più importante "sviluppare sempre verso un'API" piuttosto che semplicemente dividere le funzioni. Pensa costantemente al riutilizzo, non solo all'interno della classe attuale. (2) "O un anno dopo leggerlo di nuovo e trovarlo illeggibile" Perché le funzioni hanno nomi questo è meglio? Hai ancora più leggibilità se usi quello che un commentatore sul mio blog ha descritto come "paragrafi di codice". (3) "funzioni split = meno colpi sul tasto F10" ... non vedo perché.
Steven Jeuris,

(1) concordato (2) la leggibilità è una preferenza personale, quindi non è proprio qualcosa in discussione per me .. (3) passare attraverso una funzione di 20 righe richiede di colpire F10 20 volte. Passare attraverso una funzione che ha 10 di quelle linee in una funzione divisa mi dà la scelta di dover colpire F10 solo 11 volte. Sì, nel primo caso posso mettere dei punti di interruzione o selezionare "jumpt to pointer", ma è ancora più uno sforzo del secondo caso.
stijn

@stijn: (2) d'accordo; p (3) grazie per il chiarimento!
Steven Jeuris,
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.