Exception logic?

25 октября 2008 г.

Недавно проводил собеседование. На тестовое задание вместе выбрали сделать консольный калькулятор. Суть работы в том, что вы вводите первый операнд, операцию и второй операнд.

> calculator.exe 1 + 2

После чего должен показаться результат.

Мы сели вместе за компьютер и начали конечно же с тестов. Постепенно вырисовывалась логика приложения. Мы решили, что у нас будет объект AgrumentParser, который принимает массив аргументов командной строки в конструкторе и проверяете их. Потом у этого класса можно узнать значение первого операнда, операцию, которую он уже преобразовал в enum, и второй операнд.

   1:  public enum OperationType
   2:  {
   3:      None = 0,
   4:      Add,
   5:      Min,
   6:      Plus,
   7:      Div
   8:  }
   9:   
  10:  public class ArgumentParser
  11:  {
  12:      private double firstOperand;
  13:      private double secondOperand;
  14:      private OperationType operationType;
  15:   
  16:      public ArgumentParser(string[] args)
  17:      {
  18:      // заполняем переменные firstOperand, secondOperand и operationType из args
  19:      }
  20:   
  21:      public double FirstOperand
  22:      {
  23:          get { return firstOperand; }
  24:      }
  25:   
  26:      public OperationType OperationType
  27:      {
  28:          get { return operationType; }
  29:      }
  30:   
  31:      public double SecondOperand
  32:      {
  33:          get { return secondOperand; }
  34:      }
  35:  }

К тому же мы завели очень интересный спор. Суть его вот в чем - если пользователь ввел аргументы в неверном формате (например, 1 плюс 2), как вести себя классу, который "оборачивает" эти аргументы? Если аргументы неверные, то как можно обратиться к полю FirstOperand? Должны мы выбрасывать исключение или же добавить поле IsArgumentValid в класс ArgumentParser?

Здесь нам виделось 2 основных подхода:

  1. Т.к. состояние объекта после передачи ему неверных аргументов будет не определено, то сам объект должен выбрасывать исключение. Соответственно его клиент, должен ловить это исключение и обрабатывать ошибку.
  2. После того, как создан класс ArgumentParser из параметров командной строки, мы должны узнать у него на сколько эти параметры верны, т.е. вызвать у него свойство IsArgumentValid.

В принципе, оба подхода вполне приемлемы для такого маленького приложения. Но если задумать о разработке модулей в общем. Как нужно строить логику своего приложения? Неужели опираясь на виды исключительных ситуаций?

В данном примере это абсолютно оправдано:

   1:  using (var adapter = new DataAccessAdapter(false))
   2:  {
   3:      try
   4:      {
   5:          adapter.SaveEntity(entity, false, false);
   6:      }
   7:      catch
   8:      {
   9:          adapter.Rollback();
  10:          throw;
  11:      }
  12:      adapter.Commit();
  13:  }

А что если ваш модуль при обращении к нему может выбрасывать свои определнные исключения? Как работать с такой "исключительной" логикой?

Так:

   1:  Customer customer = CustomerFactory.Create();
   2:  if (customer.IsLoaded)
   3:  {
   4:      // обычная логика
   5:  }
   6:  else
   7:  {
   8:      // альтернативная логика
   9:  }

или так:

   1:  try
   2:  {
   3:      Customer customer = CustomerFactory.Create();
   4:      // обычная логика
   5:  }
   6:  catch(NotLoadedException)
   7:  {
   8:      // альтернативная логика
   9:  }

Возвращаясь к примеру с консольным калькулятором.

Первый вариант:

   1:  var parser = new ArgumentParser(args);
   2:  if (parser.IsArgumentValid)
   3:  {
   4:      // работаем с аргументами и производим операцию выбранную пользователем
   5:  }
   6:  else
   7:  {
   8:      // показываем пользователю формат работы с нашим приложением
   9:  }

Второй вариант:

   1:  try
   2:  {
   3:      var parser = new ArgumentParser(args);
   4:      // работаем с аргументами и производим операцию выбранную пользователем
   5:  }
   6:  catch(ArgumentException)
   7:  {
   8:      // показываем пользователю формат работы с нашим приложением
   9:  }

Лично я склоняюсь к первому варианту. Какие будут мысли?



Примечание

Crypto привел отличный аргумент против логики построенной на исключениях.

8 комментариев:

  1. А я вот больше склоняюсь ко второму варианту. Если посмотреть на эволюцию, то от первого подхода (ErrCode и др.) пришли ко второму.

    Все-таки ситуации, когда исключения заметно снижают производительность, редки. Для этих случаев рядом с Parse можно реализовать TryParse.

    Интересно узнать твое мнение на этот счет 2 года спустя.

    ОтветитьУдалить
  2. @Idsa
    Мне тоже было интересно перечитать этот пост.

    Я думаю, что исключения должны кидаться, когда программа сделала критическую ошибку и дальше работать не может (повреждены данные, нарушена целостность в БД и т.п.). Как было написано в книжке Программист-пракматик: "Мёртвые программы не врут".

    Т.е. в случае парсера метод TryParse или возврат объекта в качестве результата выглядит куда более привлекательней.

    Я за первый :)

    ОтветитьУдалить
  3. Я за второй :)

    Несколько аргументов:
    1. Исключения нельзя проигнорировать. Ты можешь забыть обработать ErrorCode, и приложение продолжит работать, делая вид, что ошибки нет (отладка в таком случае получится адской). Исключение в этом случае просто убьет приложение (и правильно сделает). К тому же вероятность того, что за время разработки исключение останется без соответствующего обработчика гораздо ниже чем аналогичная ситуация с ErrorCode
    2. Исключения позволяют обрабатывать ошибки в конструкторах и в перегруженных операторах
    3. Исключения более гибки и функциональны. Мы можем узнать, где и при каких обстоятельствах произошло исключение.
    4. Если исключения генерируются редко, думаю, их использование может быть даже эффективнее этих бесконечных проверок ErrorCode
    5. Мне кажется, в рамках большого проекта код с исключениями поддерживать проще: они стандартизированы (не так часто нужно создавать свои исключения; а если и нужно, то они наследуются от стандартных), блок try-catch более декларативен (если так можно выразиться).

    Если этих аргументов недостаточно, я еще вернусь :)

    В целом, считаю, что основным должен быть второй подход. Первый подход в основном применим в коде, в котором может генерироваться действительно много исключений.

    ОтветитьУдалить
  4. Этот комментарий был удален автором.

    ОтветитьУдалить
  5. Этот комментарий был удален автором.

    ОтветитьУдалить
  6. День добрый. Александр, прошел год, мнение не изменилось?

    Как по мне, то я предпочитаю второй вариант с исключеними, т.о. избавиться от множества проверок + практически сводится к нулю вероятность работы с нарушенной целостностью.

    ОтветитьУдалить
  7. Действительно прошло много времени, перечитал свою статью и понял, что сейчас я уже не 100% на стороне первого варианта.
    Тем не менее я бы не стал часто использовать вариант с исключениями. В данный момент я склоняюсь к способу, который используется в функциях TryParse, когда результат сразу понятен. Еще возможен вариант с созданием NullObject или как вариация InvalidObject через билдер, когда аргументы неверные, зависит от ситуации.

    ОтветитьУдалить
  8. Александр привет. Допустим есть класс треугольник. В конструкторе задаются 3 числа. 
    Напрашивается валидация: все значения больше нуля, сумма двух не может быть меньше третьей стороны.
     В данном случае как бы построил эту валидацию? Возможны варианты с билдером, котороый бы принимал параметры эти и выдавал инстанс треугольника, а конструктор сделать приватным к примеру, но в случае не валидных данных InvalidObject/Null Object не всегда можно вернуть, иначе выходит они должны быть наследниками некоего прародителя. 

    Все же пока меня вариант с exception'ами больше привлекает в связи с простотой дизайна. Хотя не все тут гладко тоже.

    ОтветитьУдалить

Моя книга «Антихрупкость в IT»

Как достигать результатов в IT-проектах в условиях неопределённости. Подробнее...