Стиль для потока управления с проверками проверки

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;
  // ...

Этот вопрос в Stack Overflow рекомендует несколько ссылок в дополнение к одной из упомянутых выше: Пример шаблона спецификации . В частности, ответы предлагают 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;
}
Ryathal
источник
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
}
Oleksi
источник