Stile per flusso di controllo con controlli di validazione


27

Mi ritrovo a scrivere un sacco di codice come questo:

int myFunction(Person* person) {
  int personIsValid = !(person==NULL);
  if (personIsValid) {
     // do some stuff; might be lengthy
     int myresult = whatever;
     return myResult;
  }
  else {
    return -1;
  }
}

Può diventare piuttosto disordinato, soprattutto se sono coinvolti più controlli. In questi casi, ho sperimentato stili alternativi, come questo:

int netWorth(Person* person) {
  if (Person==NULL) {
    return -1;
  }
  if (!(person->isAlive))  {
    return -1;
  }
  int assets = person->assets;
  if (assets==-1)  {
    return -1;
  }
  int liabilities = person->liabilities;
  if (liabilities==-1) {
    return -1;
  }
  return assets - liabilities;
}

Sono interessato a commenti sulle scelte stilistiche qui. [Non preoccuparti troppo dei dettagli delle singole dichiarazioni; è il flusso di controllo complessivo che mi interessa.]


8
Consentitemi di sottolineare che nel vostro esempio avete un errore di specifica abbastanza grave. Se, ad esempio, attività == 42 e passività == 43, dichiarerai la persona inesistente.
John R. Strohm,

Non sarebbe un'eccezione e consentire al codice client di gestire le convalide sarebbe meglio?
Tulains Córdova,

@ TulainsCórdova Le eccezioni potrebbero non essere disponibili o forse i dati non validi non sono abbastanza eccezionali da rendere accettabile l'impatto sulle prestazioni della creazione della traccia dello stack, ecc.
Hulk,

Risposte:


27

Per questo tipo di problemi Martin Fowler ha proposto il modello di specifica :

... modello di progettazione, in base al quale le regole aziendali possono essere ricombinate concatenando le regole aziendali utilizzando la logica booleana.
 
Un modello di specifica delinea una regola aziendale che è combinabile con altre regole aziendali. In questo modello, un'unità di business logic eredita la sua funzionalità dalla classe di aggregazione delle specifiche composite astratte. La classe Specifica composita ha una funzione chiamata IsSatisfiedBy che restituisce un valore booleano. Dopo l'istanza, la specifica viene "concatenata" con altre specifiche, rendendo le nuove specifiche facilmente gestibili, ma con una logica di business altamente personalizzabile. Inoltre, al momento dell'istanza, la logica aziendale può, attraverso l'invocazione del metodo o l'inversione del controllo, alterare il suo stato per diventare un delegato di altre classi come un repository di persistenza ...

Sopra suona un po 'troppo acuto (almeno per me), ma quando l'ho provato nel mio codice è andato abbastanza liscio e si è rivelato facile da implementare e leggere.

Per come la vedo io, l'idea principale è quella di "estrarre" il codice che esegue i controlli in metodi / oggetti dedicati.

Con il tuo netWorthesempio, questo potrebbe apparire come segue:

int netWorth(Person* person) {
  if (isSatisfiedBySpec(person)) {
    return person->assets - person->liabilities;
  }
  log("person doesn't satisfy spec");
  return -1;
}

#define BOOLEAN int // assuming C here
BOOLEAN isSatisfiedBySpec(Person* person) {
  return Person != NULL
      && person->isAlive
      && person->assets != -1
      && person->liabilities != -1;
}

Il tuo caso appare piuttosto semplice in modo che tutti i controlli appaiano OK per rientrare in un semplice elenco all'interno di un singolo metodo. Spesso devo dividere in più metodi per farlo leggere meglio.

Inoltre in genere raggruppo / estraggo i metodi relativi alle "specifiche" in un oggetto dedicato, anche se il tuo caso sembra OK senza quello.

  // ...
  Specification s, *spec = initialize(s, person);
  if (spec->isSatisfied()) {
    return person->assets - person->liabilities;
  }
  log("person doesn't satisfy spec");
  return -1;
  // ...

Questa domanda su Stack Overflow consiglia alcuni collegamenti oltre a uno sopra menzionato: Esempio di modello di specifica . In particolare, le risposte suggeriscono che Dimecasts "Impara il modello delle specifiche" per una panoramica di un esempio e menziona il documento "Specifiche" scritto da Eric Evans e Martin Fowler .


8

Trovo più facile spostare la convalida sulla propria funzione, aiuta a mantenere più pulito l'intento di altre funzioni, quindi il tuo esempio sarebbe così.

int netWorth(Person* person) { 
    if(validPerson(person)) {
        int assets = person->assets;
        int liabilities = person->liabilities;
        return assets - liabilities;
    }
    else {
        return -1;
    }
}

bool validPerson(Person* person) { 
    if(person!=NULL && person->isAlive
      && person->assets !=-1 && person->liabilities != -1)
        return true;
    else
        return false;
}

2
Perché hai il ifin validPerson? Basta person!=NULL && person->isAlive && person->assets !=-1 && person->liabilities != -1invece tornare .
David Hammen,

3

Una cosa che ho visto funzionare particolarmente bene è l'introduzione di un livello di validazione nel tuo codice. Innanzitutto hai un metodo che esegue tutta la convalida disordinata e restituisce errori (come -1nei tuoi esempi sopra) quando qualcosa va storto. Al termine della convalida, la funzione chiama un'altra funzione per eseguire il lavoro effettivo. Ora questa funzione non deve eseguire tutte quelle fasi di convalida perché dovrebbero già essere eseguite. Vale a dire, la funzione di lavoro presuppone che l'input sia valido. Come dovresti affrontare le ipotesi? Li affermi nel codice.

Penso che questo renda il codice molto facile da leggere. Il metodo di convalida contiene tutto il codice disordinato per gestire gli errori sul lato utente. Il metodo di lavoro documenta in modo chiaro le sue assunzioni con asserzioni e quindi non deve lavorare con dati potenzialmente non validi.

Considera questo refactoring del tuo esempio:

int myFunction(Person* person) {
  int personIsValid = !(person==NULL);
  if (personIsValid) {
     return myFunctionWork(person)
  }
  else {
    return -1;
  }
}

int myFunction(Person *person) {
  assert( person != NULL);  
  // Do work and return
}
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.