Stile per il controllo del flusso con controlli di validazione

25

Mi trovo 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 complicato, specialmente 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 affermazioni; è il flusso di controllo generale che mi interessa.]

    
posta William Jockusch 16.05.2012 - 16:25
fonte

3 risposte

24

Per questo tipo di problemi Martin Fowler ha proposto Schema delle specifiche :

...design pattern, whereby business rules can be recombined by chaining the business rules together using boolean logic.
 
A specification pattern outlines a business rule that is combinable with other business rules. In this pattern, a unit of business logic inherits its functionality from the abstract aggregate Composite Specification class. The Composite Specification class has one function called IsSatisfiedBy that returns a boolean value. After instantiation, the specification is "chained" with other specifications, making new specifications easily maintainable, yet highly customizable business logic. Furthermore upon instantiation the business logic may, through method invocation or inversion of control, have its state altered in order to become a delegate of other classes such as a persistence repository...

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

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

Con il tuo esempio di netWorth , 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 sembra piuttosto semplice, così tutti i controlli sembrano OK per rientrare in un elenco semplice in un unico metodo. Spesso devo dividere a più metodi per farlo leggere meglio.

Generalmente, inoltre, raggruppo / estrai i metodi correlati "spec" 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 link oltre a uno sopra menzionato: Esempio di modello di specifica . In particolare, le risposte suggeriscono Dimecasts "Apprendimento dello schema delle specifiche" per una procedura dettagliata di un esempio e menzioni "Specifiche tecniche" scritto da Eric Evans e Martin Fowler .

    
risposta data 16.05.2012 - 16:52
fonte
8

Trovo più semplice spostare la convalida nella propria funzione, aiuta a mantenere più pulito l'intento di altre funzioni, quindi il tuo esempio sarebbe come questo.

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;
}
    
risposta data 16.05.2012 - 16:34
fonte
3

Una cosa che ho visto funzionare particolarmente bene è l'introduzione di un livello di validazione nel codice. Innanzitutto hai un metodo che fa tutti gli errori di convalida e restituisce (come -1 nei 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 ha bisogno di fare tutti quei passaggi di convalida perché dovrebbero già essere fatti. Vale a dire, la funzione di lavoro assume che l'input sia valido. Come dovresti affrontare le ipotesi? Li asserisci nel codice.

Penso che questo faciliti la lettura del codice. Il metodo di convalida contiene tutto il codice disordinato per gestire gli errori sul lato utente. Il metodo di lavoro documenta in modo pulito le assunzioni con gli asser 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
}
    
risposta data 16.05.2012 - 16:34
fonte

Leggi altre domande sui tag