Стиль контрольного потоку з перевірки перевірки


27

Я вважаю, що пишу так багато коду:

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

Він може стати досить безладним, особливо якщо задіяно кілька перевірок. У таких випадках я експериментував із альтернативними стилями, такими як цей:

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

Мене цікавлять коментарі щодо стилістичного вибору тут. [Не переживайте надто багато про деталі окремих тверджень; мене цікавить загальний потік управління.]


8
Дозвольте мені зазначити, що у вашому прикладі є досить серйозна помилка специфікації. Якщо, наприклад, активи == 42 та зобов'язання == 43, ви визнаєте особу такою, що не існує.
Джон Р. Стром

Чи не було б викиданням винятків і дозволяти кращому коду клієнта керувати перевірки?
Тулен Кордова

@ TulainsCórdova Винятки можуть бути недоступними, або, можливо, недостовірні дані не є винятковою, щоб вплив на ефективність побудови сліду стека тощо був прийнятним.
Халк

Відповіді:


27

Для таких питань Мартін Фаулер запропонував модель специфікації :

... модель дизайну, згідно з якою бізнес-правила можна рекомбінувати, з'єднавши бізнес-правила разом, використовуючи булеву логіку.
 
Шаблон специфікації окреслює ділове правило, сумісне з іншими правилами ведення бізнесу. У цій схемі одиниця бізнес-логіки успадковує свою функціональність від абстрактного сукупного класу Композитні специфікації. Клас складених специфікацій має одну функцію під назвою IsSatisfiedBy, яка повертає булеве значення. Після ідентифікації специфікація "прикута" до інших специфікацій, що робить нові технічні характеристики легко рентабельними, але при цьому легко налаштовує бізнес-логіку. Крім того, при створенні примірника бізнес-логіка може через виклик методу або інверсію контролю змінити свій стан, щоб стати делегатом інших класів, таких як постійне сховище ...

Вище звучить трохи високо-пришвидшено (принаймні, для мене), але коли я спробував це у своєму коді, він пройшов досить плавно і виявився простим у виконанні та читанні.

Як я це бачу, основна ідея полягає в тому, щоб "витягнути" код, який робить перевірки у виділених методах / об'єктах.

У вашому netWorthприкладі це може виглядати так:

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

Ваш випадок видається досить простим, тому всі чеки виглядають нормально, щоб увійти в звичайний список в рамках одного методу. Мені часто доводиться розбиватися на більше методів, щоб зробити його краще для читання.

Я також зазвичай групую / витягую "специфічні" методи, пов'язані з об'єктом, хоча ваш випадок виглядає нормально без цього.

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

Це питання на переповнення стека рекомендує кілька посилань на додаток до одного згаданих вище: Приклад шаблону специфікацій . Зокрема, відповіді пропонують Dimecasts "Вивчення шаблону специфікацій" для ознайомлення з прикладом і згадують документ "Технічні характеристики", автор автор Ерік Еванс та Мартін Фаулер .


8

Мені легше перемістити валідацію на власну функцію, це допомагає зберегти наміри інших функцій більш чистими, тому ваш приклад був би таким.

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
Чому у вас є ifін validPerson? Просто поверніться person!=NULL && person->isAlive && person->assets !=-1 && person->liabilities != -1замість цього.
Девід Хаммен

3

Одне, що я особливо добре бачив, - це введення шару перевірки у ваш код. Спочатку у вас є метод, який виконує всю безладну перевірку і повертає помилки (як -1у ваших прикладах вище), коли щось піде не так. Коли перевірка завершена, функція викликає іншу функцію для виконання фактичної роботи. Тепер ця функція не повинна виконувати всі ці кроки перевірки, оскільки вони вже повинні бути виконані. Тобто, робоча функція передбачає, що введення є дійсним. Як слід поводитися з припущеннями? Ви стверджуєте їх у коді.

Я думаю, що це робить код дуже легким для читання. Метод перевірки містить весь безладний код для вирішення помилок на стороні користувача. Метод роботи чітко документує свої припущення з твердженнями, а потім не повинен працювати з потенційно недійсними даними.

Розглянемо цей рефакторинг вашого прикладу:

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
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.