Якщо модель перевіряє дані, чи не повинна вона викидати винятки на погане введення?


9

Читаючи це запитання ТА, здається, що викидання винятків для перевірки введення користувача нахмурюється.

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

Чи моє переконання, що перевірку слід робити на бізнес-шарі, неправильно?

Що я роблю

Тому мій код зазвичай закінчується так:

<?php
class Person
{
  private $name;
  private $age;

  public function setName($n) {
    $n = trim($n);
    if (mb_strlen($n) == 0) {
      throw new ValidationException("Name cannot be empty");
    }
    $this->name = $n;
  }

  public function setAge($a) {
    if (!is_int($a)) {
      if (!ctype_digit(trim($a))) {
        throw new ValidationException("Age $a is not valid");
      }
      $a = (int)$a;
    }
    if ($a < 0 || $a > 150) {
      throw new ValidationException("Age $a is out of bounds");
    }
    $this->age = $a;
  }

  // other getters, setters and methods
}

У контролері я просто передаю вхідні дані в модель і перехоплюю викинуті винятки, щоб показати користувачеві помилку:

<?php
$person = new Person();
$errors = array();

// global try for all exceptions other than ValidationException
try {

  // validation and process (if everything ok)
  try {
    $person->setAge($_POST['age']);
  } catch (ValidationException $e) {
    $errors['age'] = $e->getMessage();
  }

  try {
    $person->setName($_POST['name']);
  } catch (ValidationException $e) {
    $errors['name'] = $e->getMessage();
  }

  ...
} catch (Exception $e) {
  // log the error, send 500 internal server error to the client
  // and finish the request
}

if (count($errors) == 0) {
  // process
} else {
  showErrorsToUser($errors);
}

Це погана методологія?

Альтернативний метод

Чи, можливо, я повинен створити методи для isValidAge($a)повернення true / false, а потім викликати їх з контролера?

<?php
class Person
{
  private $name;
  private $age;

  public function setName($n) {
    $n = trim($n);
    if ($this->isValidName($n)) {
      $this->name = $n;
    } else {
      throw new Exception("Invalid name");
    }
  }

  public function setAge($a) {
    if ($this->isValidAge($a)) {
      $this->age = $a;
    } else {
      throw new Exception("Invalid age");
    }
  }

  public function isValidName($n) {
    $n = trim($n);
    if (mb_strlen($n) == 0) {
      return false;
    }
    return true;
  }

  public function isValidAge($a) {
    if (!is_int($a)) {
      if (!ctype_digit(trim($a))) {
        return false;
      }
      $a = (int)$a;
    }
    if ($a < 0 || $a > 150) {
      return false;
    }
    return true;
  }

  // other getters, setters and methods
}

І контролер буде в основному тим самим, тільки замість спробувати / catch зараз є, якщо / else:

<?php
$person = new Person();
$errors = array();
if ($person->isValidAge($age)) {
  $person->setAge($age);
} catch (Exception $e) {
  $errors['age'] = "Invalid age";
}

if ($person->isValidName($name)) {
  $person->setName($name);
} catch (Exception $e) {
  $errors['name'] = "Invalid name";
}

...

if (count($errors) == 0) {
  // process
} else {
  showErrorsToUser($errors);
}

Отже, що мені робити?

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


Я трохи змінив "оригінальний" код для обробки ValidationExceptionта інші винятки
Carlos Campderrós

2
Одна з проблем із відображенням повідомлень про виключення кінцевому користувачеві полягає в тому, що модель раптом повинна знати, якою мовою користувач говорить, але це здебільшого викликає занепокоєння для Перегляду.
Барт ван Інген Шенау

@BartvanIngenSchenau хороший улов. Мої програми завжди були одномовними, але добре думати про проблеми локалізації, які можуть виникнути при будь-якій реалізації.
Карлос Кампдеррос

Винятки для перевірок - це просто фантазійний спосіб введення типів у процес. Ви можете досягти тих же результатів, повернувши об’єкт, який реалізує інтерфейс перевірки, як IValidateResults.
Реагуючий

Відповіді:


7

Підхід, який я використовував у минулому, полягає у складанні всіх логік перевірки, присвячених класам перевірки.

Потім ви можете ввести ці класи перевірки у ваш Presentation Layer для ранньої перевірки введення. І ніщо не заважає вашим класам Model використовувати ті самі класи для забезпечення цілісності даних.

Дотримуючись такого підходу, ви можете по-різному ставитися до помилок перевірки залежно від того, у якому шарі вони трапляються:

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

Отже, у вас є клас PersonValidatorз усією логікою для перевірки різних атрибутів a Person, і Personклас, який від цього залежить PersonValidator, правда? Яку перевагу пропонує ваша пропозиція перед альтернативним методом, який я запропонував у питанні? Я бачу лише здатність вводити різні класи валідації для Person, але я не можу придумати жодного випадку, де це було б потрібно.
Карлос Кампдеррос

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

Що ж, для програми, яку ви плануєте продати декільком людям / компаніям, можливо, це має сенс, оскільки кожна компанія може мати різні правила перевірки того, що є дійсним діапазоном для віку особи. Так що це можливо корисно, але насправді надмірно для моїх потреб. У всякому разі, +1 і для вас
Carlos Campderrós

1
Відокремлення валідації від моделі має сенс і з точки зору зв'язку та згуртованості. У цьому простому сценарії це може бути надмірним, але знадобиться лише одне правило перевірки "перехресного поля", щоб зробити окремий клас Validator набагато привабливішим.
Сет М.

8

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

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

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


Зараз є багато людей, які говорять про такі речі, як "винятки слід використовувати лише для виняткових речей, а XYZ - не винятковий". (Наприклад, відповідь @ dann1111 ... де він позначає помилки користувачів як "абсолютно нормальні".)

Моя відповідь на це полягає в тому, що немає жодного об’єктивного критерію для вирішення того, чи є щось ("XY Z") винятковим чи ні. Це суб'єктивний захід. (Те, що будь-якій програмі потрібно перевіряти на помилки у введенні користувача, не робить помилки появи "нормальними". Насправді "нормальне" багато в чому не має сенсу з об'єктивної точки зору.)

У цій мантрі є зерно правди. У деяких мовах (або, точніше, в деяких мовних реалізаціях ) створення, викид та / або виловлювання виключень значно дорожче, ніж прості умови. Але якщо ви дивитесь на це з тієї точки зору, вам потрібно порівняти вартість створення / кидання / лову з витратами на додаткові тести, які, можливо, знадобиться виконати, якщо ви уникали використання винятків. І "рівняння" має враховувати ймовірність того, що виняток потрібно кинути.

Інший аргумент проти винятків полягає в тому, що вони можуть ускладнити розуміння коду. Але зворотний бік полягає в тому, що при правильному використанні вони можуть полегшити розуміння коду .


Коротше кажучи - рішення використовувати або не використовувати винятки слід приймати після зважування достоїнств ... І НЕ на основі якоїсь спрощеної догми.


Хороший момент про загальне Exceptionкидання / спіймане. Я дійсно кидаю якийсь власний підклас Exception, і код сетерів зазвичай нічого не робить, що могло б викинути ще один виняток.
Карлос Кампдеррос

Я трохи змінив "оригінальний" код, щоб обробити ValidationException та інші винятки / cc @ dan1111
Carlos Campderrós

1
+1, я б швидше мав описовий ValidationException, ніж повертався до темних віків, коли потрібно перевірити значення повернення кожного виклику методу. Простіший код = потенційно менше помилок.
Хайнзі

2
@ Dan1111 - У той час як я поважаю ваше право мати свою думку, нічого в коментарі ніщо - небудь інше , ніж думка. Не існує логічного зв’язку між "нормальністю" перевірки та механізмом обробки помилок перевірки. Все, що ви робите, - це декламація догми.
Стівен С

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

6

На мою думку, корисно розрізняти помилки програми та помилки користувачів , а використовувати лише винятки для перших.

  • Винятки призначені для покриття речей, які перешкоджають належному виконанню вашої програми .

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

  • Помилки користувачів, як недійсне введення, є цілком нормальними (з точки зору вашої програми), і ваша програма не повинна сприйматись як несподівані .

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

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

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

Ваш оригінальний код є проблематичним:

  • Абонент setAge()методу повинен занадто багато знати про внутрішню обробку помилок методу: абонент повинен знати, що виняток видається, коли вік недійсний, і що жодні інші винятки не можуть бути викинуті в межах методу . Це припущення може бути порушено пізніше, якщо ви додали додаткові функціональні можливості всередині setAge().
  • Якщо абонент не може вилучити винятки, пізніше винятковий вік буде оброблений іншим, швидше за все, непрозорим способом. Або навіть призвести до аварійного виходу винятків. Недобре поведінка щодо введення недійсних даних.

Альтернативний код також має проблеми:

  • Введено додатковий, можливо, непотрібний метод isValidAge().
  • Тепер setAge()метод повинен припустити, що абонент вже перевірив isValidAge()(жахливе припущення) або ще раз підтвердив вік. Якщо він ще раз підтверджує вік, setAge() все-таки потрібно надати певну помилку, і ви знову повернетесь до одного.

Запропонований дизайн

  • Зробіть setAge()повернення справжнім на успіх і хибним на невдачу.

  • Перевірте значення повернення setAge()та, якщо воно не вдалося, повідомте користувача, що вік був недійсним, не за винятком, але за допомогою звичайної функції, яка відображає помилку для користувача.


Тоді як мені це зробити? Я запропонував альтернативний метод чи щось зовсім інше, про що я не думав? Також, чи моє передумови, що "перевірку слід робити на бізнес-шарі" помилковою?
Карлос Кампдеррос

@ CarlosCampderrós, див. Оновлення; Я додав цю інформацію, як ви коментували. Ваш оригінальний дизайн мав перевірку в правильному місці, але помилка використання винятків для проведення цієї перевірки.

Альтернативний метод дійсно змушує setAgeперевірити його ще раз, але оскільки логіка в основному "якщо вона дійсна, то встановлений вік ще викине виняток", це не повертає мене до прямого.
Карлос Кампдеррос

2
Одна з проблем, які я бачу як із альтернативним методом, так і з запропонованою конструкцією, полягає в тому, що вони втрачають здатність розрізняти ЧОМУ вік був недійсним. Це може бути зроблено для повернення true або помилки (так, php брудно), але це може призвести до безлічі проблем, тому що "The entered age is out of bounds" == trueлюди завжди повинні використовувати ===, тому такий підхід був би більш проблематичним, ніж проблема, яку він намагається вирішити
Карлос Кампдеррос

2
Але тоді кодування програми дійсно стомлює, оскільки для кожного, setAge()що ви робите де завгодно, ви повинні перевірити, чи справді він працював. Викидання винятків означає, що ви не повинні турбуватися про запам'ятовування, щоб перевірити, чи все пройшло нормально. Як я бачу, намагання встановити недійсне значення в атрибут / властивість - це щось виняткове і тоді варто кинути це Exception. Модель не має байдуже, чи отримує вона свої дані з бази даних або від користувача. Він ніколи не повинен отримувати поганий внесок, тому я вважаю, що законно кидати там виняток.
Карлос Кампдеррос

4

З моєї точки зору (я хлопець на Java), цілком справедливо, як ти реалізував це в першу чергу.

Дійсно, що об'єкт видає виняток, коли деякі умови не виконані (наприклад, порожній рядок). У Java поняття перевірених винятків введене до такої мети - винятки, які повинні бути оголошені в підписі, щоб їх можна було кинути, і абонент явно повинен їх вловлювати. Навпаки, неперевірені винятки (також RuntimeExceptions) можуть трапитися будь-коли без необхідності визначення коду-застереження в коді. Хоча перші використовуються для відновлюваних випадків (наприклад, неправильний ввід користувача, ім'я файлу не існує), останні використовуються для випадків, коли користувач / програміст нічого не може зробити (наприклад, Out-Of-Memory).

Вам слід, однак, як уже згадував @Stephen C, визначити власні винятки і конкретно вибирати ті, щоб не зловити інших ненавмисно.

Інший спосіб, однак, буде використовувати об'єкти передачі даних, які є просто контейнерами даних без будь-якої логіки. Потім ви передаєте такий DTO валідатору або самому Модельному об'єкту для перевірки, і лише за умови успішного внесення оновлень у Model-Object. Цей підхід часто застосовується, коли логіка презентації та логіка додатків відокремлені ярусами (презентація - це веб-сторінка, додаток - веб-сервіс). Таким чином вони фізично розділені, але якщо у вас є обидва на одному рівні (як у вашому прикладі), ви повинні переконатися, що не буде вирішення питання встановлення значення без перевірки.


4

З моєю шапкою Haskell обидва підходи помилкові.

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

У Особи є певні інваріанти, такі як присутність імені та вік.

Вміти представляти Особу, яка має лише ім’я, але вік - це те, чого хочеться уникнути будь-якою ціною, адже саме це створює повноту. Суворі інваріанти означають, що вам не потрібно перевіряти наявність віку, наприклад, пізніше.

Тож у моєму світі Особистість створюється атомно за допомогою одного конструктора чи функції. Цей конструктор або функція може знову перевірити правильність параметрів, але жодна половина Person не повинна будуватися.

На жаль, Java, PHP та інші мови OO роблять правильний варіант досить багатослівним. У належних API Java часто використовуються об'єкти-конструктори. У такому API створення людини виглядатиме приблизно так:

Person p = new Person.Builder().setName(name).setAge(age).build();

або більш багатослівне:

Person.Builder builder = new Person.Builder();
builder.setName(name);
builder.setAge(age);
Person p = builder.build();
// Person object must have name and age here

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


Все, що ви тут зробили, - це перенести проблему на клас Builder, на який ви дійсно не відповіли.
Cypher

2
Я локалізував проблему на функції builder.build (), яка виконується атомно. Ця функція - це список усіх етапів перевірки. Існує величезна різниця між цим підходом та спеціальним підходом. Клас Builder не має інваріантів поза простими типами, тоді як клас Person має сильні інваріанти. Створення правильних програм - це все, що стосується застосування сильних інваріантів у ваших даних.
user239558

Це все ще не відповідає на питання (принаймні, не повністю). Чи можете ви детальніше розповісти про те, як передаються окремі повідомлення про помилки від класу Builder до стека викликів до Перегляду?
Cypher

Три можливості: build () може кинути конкретні винятки, як у першому прикладі ОП. Може бути загальнодоступний Set <String> validate (), який повертає набір помилок, зрозумілих для людини. Існує загальнодоступний набір <Error> validate () для помилок, готових до i18n. Справа в тому, що це відбувається під час перетворення на об’єкт Особи.
user239558

2

Словами мирянина:

Перший підхід - правильний.

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

Бізнес-класи повинні кидати виняток щоразу, коли ділове правило порушується.

Контролер або шар представлення повинен вирішити, кидає він їх, чи робить власні перевірки, щоб запобігти виникненню винятків.

Пам'ятайте: ваші заняття потенційно будуть використовуватися в різних контекстах і різними інтеграторами. Тому вони повинні бути досить розумними, щоб кидати винятки з поганих даних.

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