Styl przepływu kontroli z kontrolami sprawdzania poprawności

27

Piszę dużo takiego kodu:

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

Może stać się dość niechlujny, szczególnie jeśli w grę wchodzi wiele kontroli. W takich przypadkach eksperymentowałem z alternatywnymi stylami, takimi jak ten:

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;
}

Interesują mnie tutaj komentarze dotyczące stylistycznych wyborów. [Nie przejmuj się zbytnio szczegółami poszczególnych oświadczeń; interesuje mnie ogólny przepływ kontroli.]

William Jockusch
źródło
8
Pozwolę sobie zauważyć, że w twoim przykładzie masz dość poważny błąd specyfikacji. Jeśli na przykład aktywa == 42 i zobowiązania == 43, zadeklarujesz, że dana osoba nie istnieje.
John R. Strohm
Czy nie zgłoszenie wyjątku i pozwolenie kodowi klienta na zarządzanie sprawdzaniem poprawności byłoby lepsze?
Tulains Córdova
@ TulainsCórdova Wyjątki mogą nie być dostępne lub być może nieprawidłowe dane nie są na tyle wyjątkowe, że wpływ na wydajność tworzenia śladu stosu itp. Jest akceptowalny.
Hulk,

Odpowiedzi:

27

W przypadku tego rodzaju problemów Martin Fowler zaproponował wzór specyfikacji :

... wzorzec projektowy, w którym reguły biznesowe można łączyć, łącząc reguły biznesowe razem za pomocą logiki logicznej.
 
Wzorzec specyfikacji określa regułę biznesową, którą można łączyć z innymi regułami biznesowymi. W tym wzorcu jednostka logiki biznesowej dziedziczy swoją funkcjonalność po abstrakcyjnej klasie zagregowanej specyfikacji złożonej. Klasa Composite Specification ma jedną funkcję o nazwie IsSatisfiedBy, która zwraca wartość logiczną. Po utworzeniu instancji specyfikacja jest „łączona” z innymi specyfikacjami, dzięki czemu nowe specyfikacje są łatwe w utrzymaniu, a jednocześnie można je w dużym stopniu dostosować do własnych potrzeb. Ponadto po utworzeniu instancji logika biznesowa może, poprzez wywołanie metody lub odwrócenie kontroli, zmienić swój stan, aby stać się delegatem innych klas, takich jak repozytorium trwałości ...

Powyżej brzmi to trochę wysoko (przynajmniej dla mnie), ale kiedy wypróbowałem to w moim kodzie, poszło dość gładko i okazało się łatwe do wdrożenia i odczytu.

Moim zdaniem, główną ideą jest „wyodrębnienie” kodu, który wykonuje kontrole w dedykowanych metodach / obiektach.

W twoim netWorthprzykładzie może to wyglądać następująco:

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;
}

Twoja sprawa wydaje się dość prosta, aby wszystkie kontrole wyglądały OK, aby zmieścić się na prostej liście w ramach jednej metody. Często muszę podzielić się na więcej metod, aby poprawić czytanie.

Zazwyczaj grupuję / wyodrębniam metody związane ze „specyfikacją” w obiekcie dedykowanym, choć bez tego twoja sprawa wygląda dobrze.

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

To pytanie w Stack Overflow zaleca kilka linków oprócz jednego wspomnianego powyżej: Przykład wzorca specyfikacji . W szczególności odpowiedzi sugerują Dimecastsa „Nauka wzorca specyfikacji” jako przewodnik po przykładzie i wspominają o dokumencie „Specyfikacje” autorstwa Erica Evansa i Martina Fowlera .

komar
źródło
8

Uważam, że łatwiej jest przenieść sprawdzanie poprawności do własnej funkcji, pomaga to utrzymać zamiar innych funkcji w czystości, więc twój przykład byłby taki.

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;
}
Ryathal
źródło
2
Dlaczego masz ifin validPerson? person!=NULL && person->isAlive && person->assets !=-1 && person->liabilities != -1Zamiast tego po prostu wróć .
David Hammen
3

Jedną z rzeczy, które widziałem szczególnie dobrze, jest wprowadzenie warstwy sprawdzającej do twojego kodu. Najpierw masz metodę, która wykonuje wszystkie niechlujne sprawdzanie poprawności i zwraca błędy (jak -1w powyższych przykładach), gdy coś pójdzie nie tak. Po sprawdzeniu poprawności funkcja wywołuje inną funkcję w celu wykonania rzeczywistej pracy. Teraz ta funkcja nie musi wykonywać wszystkich tych kroków sprawdzania poprawności, ponieważ należy je już wykonać. Oznacza to, że funkcja pracy zakłada, że dane wejściowe są prawidłowe. Jak radzić sobie z założeniami? Zapewniasz je w kodzie.

Myślę, że dzięki temu kod jest bardzo łatwy do odczytania. Metoda sprawdzania poprawności zawiera cały niechlujny kod do obsługi błędów po stronie użytkownika. Metoda pracy czysto dokumentuje swoje założenia za pomocą stwierdzeń, a następnie nie musi działać z potencjalnie nieprawidłowymi danymi.

Rozważ to refaktoryzacja swojego przykładu:

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
}
Oleksi
źródło