10 May 2011

Exception-ы и мифы о них

Programming
Уже не первый раз сталкиваюсь с негибким отношением к поднятию исключений. Именно к поднятию, потому что к перехвату у большинства мнение совпадает: перехватывай только тогда, когда на самом деле можешь обработать. Поднятие же воспринимается, как нечто исключительное, из ряда вон. Когда видят throw, начинают рассказывать кучу историй о том как...

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

Для начала давайте выясним, а что же это за зверь такой. Я выделяю следующие свойства:
  • невозможно игнорировать наступление;
  • несколько обработчиков в одном месте;
  • просачивание через любое количество вложенных вызовов;
  • независимая, передаваемая обработчику, структура данных — вспомните hresult, макросы вызова com-фунций и другую белиберду, обязанную своим существованием отсутствию (или нежеланию использовать) механизмов exception-ов

И все. Всевозможные потери в производительности, сложность контроля являются контекстно-зависимыми (а чаще просто надуманными) и требуют доказательства в каждом конкретном случае.

Например: производим поиск по файлу.

int FindSymbol(TextReader reader, char symb)
{
  char cur;
  int pos;
  while (cur = (char)reader.Read())
  {
    if (cur == ‘a’)
      throw new FormatException();
    pos ++;
    if (cur == symb)
      return pos;
  }
  throw new MyException(); }
}



Определены следующие варианты выхода из функции:
  • требуемые данные найдены (return pos)
  • файл имеет не правильный формат (throw)
  • файл закончился (reader поднимает исключение)

Последний вариант работы функции наиболее спорен. Обычно программисты очень изобретательны при обосновании своих решений, и могут найти кучу доводов «за» и «против»: неэффективность, клиентскому коду необходимо знать детали реализации, неправильный формат является ошибкой и т.д. Все это правда, но не главное. Основной довод «за» — достаточность.

Клиенту больше не надо анализировать сложный результат, он попросил найти текст — ему нашли; если нет — он об этом даже не узнает.

Еще пример: обработка нажатия пользователем кнопки «cancel». Опять же можно долго дискутировать на тему — для чего изначально создавался этот механизм?

Но если он есть и подходит для логики «cancel» — глупо его не использовать.
Когда мне нужно было подклеить дома порожек, я, недолго думая, использовал в качестве груза книгу М.Мак-Дональда «Wpf …». Так почему же в иных случаях мне поступать иначе?

Как вы относитесь к маленьким функциям? Я вот считаю, что другие просто не имеют права на существование. При таком отношении периодически возникают ситуации, что без поднятия исключений просто никуда. Например, надо выйти из цикла, расположенного выше по стеку вызовов. Не передавать же флаг завершения, в конце концов. Или передать…
Если же цикл располагается не на предыдущем уровне стека вызовов, а выше? Весь код без throw превращается в анализ результатов работы функций. Доходит до смешного — выделяются методы проверяющие результаты работы других функций. Сидишь и думаешь об откате неожиданно «неправильного» рефакторинга. Но все меняется, когда приходят они: код начинает удаляться по две-три строчки за раз (а если я тормозил
до этого достаточно долго, то удаляются и методы, и классы)

А вот еще один интересный «аромат» — большое количество условных операторов. Когда перед выполнением какого-либо действия начинается анализ, «а можно ли это сделать». Оставим в покое алгоритмы, состоящие из нескольких операций, которые по отдельности делают состояние системы неопределенным. Если начал делать, — разбейся в лепешку, но закончи! Я не про них. Хотя и здесь, при желании, можно найти приемлемые решения. Так вот, запах от большего количества условных операторов: ну например, при анализе набора входных данных перед выполнением операции или проверка валидности структуры данных, или… ну мало ли чего еще. В общем, смотрим на каждый элемент данных и решаем, а подходит ли он для наших великих целей, а затем осуществляем их. Не глупо ли? Можно же сразу попробовать осуществить задуманное. И всего-то надо — предположить, что на данном участке может произойти исключение, связанное с нашим нежеланием загромождать код мусором.

Основным аргументом у противников такого отношения к подъему исключений является просадка производительности. Глупо с этим спорить — условный оператор в несколько раз более эффективен. Но есть одно «но»: проблему надо доказать на реальных данных. Предположим, участок кода действительно критический. О ужас! — теперь придется потратить пару часов на рефакторинг. Не дней, а именно часов. Помните как рассказывал Фаулер о своем отношении к проектированию: «…я пытаюсь определить, насколько трудно окажется рефакторинг от одного дизайна в другой. Если трудностей не видно, то я, не слишком задумываясь о выборе, останавливаюсь на самом простом».

Если предположить, что мы принимаем эту смелую мысль, то осталось определиться с порядком рефакторинга:
  1. Выделяем метод из тела метода поднимающего исключение.
  2. Определяем критерий в сигнатуре выделенного метода, который будет показывать, что в иной ситуации мы бы подняли исключение.
  3. Изменяем исходный метод таким образом, чтобы он, в зависимости от критерия, поднимал исключение. Будем называть исходный метод «методом с исключением», а выделенный — «метод без исключения».
  4. Создаем метод расширение (копированием) из метода без исключения.
  5. Заменяем тело метода расширения на вызов метода с исключением.
  6. Блокируем распространение исключения через метод расширения и, в зависимости от подъема, взводим или сбрасываем критерий.
  7. Добавляем к интерфейсу метод с сигнатурой метода без исключения. В реализациях этих методов производим вызов метода расширения. Если метод не входит в состав внешнего интерфейса (по отношению к классу) — тем лучше. Этот пункт можно пропустить.
  8. Заменяем вызовы метода с исключениями методами без исключения. В реализациях интерфейса это обязательно (иначе не следовало и начинать), в остальных случаях — по желанию. Замещающий код должен выглядеть так: вызов метода без исключения, анализ критерия.
  9. Создаем метод расширения из метода с исключением. Структура его должна уже быть верной. Осталось только сделать его рабочим.
  10. Заменяем оставшиеся вызововы с исключениями на расширения без исключения.
  11. Удаляем из интерфейса методы с исключениями и расширения без исключения. Расширения с исключениями по возможности.


Вот как это будет выглядеть в коде.
  • Исходный код:
    class SrcClass
    {
    public void Exec()
    {
    throw new MyException();
    }
    }
  • После третьего шага:
    class SrcClass
    {
    public void Exec()
    {
    if (!TryExec())
    throw new MyException();
    }
    public bool TryExec()
    {
    return false;
    }
    }
  • После девятого шага:
    class SrcClass
    {
    public void Exec()
    {
    if (!TryExec())
    throw new MyException();
    }
    public bool TryExec()
    {
    return false;
    }
    }

    static class SrcClassHelper
    {
    public static bool TryExecHelp(this SrcClass src)
    {
    try
    {
    src.Exec();
    return true;
    }
    catch (MyException)
    {
    return false;
    }
    }
    public static void Exec(this SrcClass src)
    {
    if (!src.TryExec())
    throw new MyException();
    }
    }
  • По завершению:
    class SrcClass
    {
    public bool TryExec()
    {
    return false;
    }
    }

    static class SrcClassHelper
    {
    public static void Exec(this SrcClass src)
    {
    if (!src.TryExec())
    throw new MyException();
    }
    }


В принципе все. Надеюсь, я сумел развеять хотя бы часть страхов о подъеме исключений, и теперь при возникновении такой необходимости вы более лояльно будете выбирать решение. И скорее всего, выберите более простое. А значит, ваш приемник будет ругать вас с меньшим остервенением (не исключено что им буду я).
Tags:exceptionthrowrefactoring
Hubs: Programming
+25
13k 65
Comments 68
Top of the last 24 hours