Уже не первый раз сталкиваюсь с негибким отношением к поднятию исключений. Именно к поднятию, потому что к перехвату у большинства мнение совпадает: перехватывай только тогда, когда на самом деле можешь обработать. Поднятие же воспринимается, как нечто исключительное, из ряда вон. Когда видят throw, начинают рассказывать кучу историй о том как...
Помните, как надо бороться со страхом? Засекаешь одну минуту, в течение которой даешь волю всем своим эмоциям. Затем говоришь себе «хватит» и с головой погружаешься в проблемы. Минута прошла.
Для начала давайте выясним, а что же это за зверь такой. Я выделяю следующие свойства:
И все. Всевозможные потери в производительности, сложность контроля являются контекстно-зависимыми (а чаще просто надуманными) и требуют доказательства в каждом конкретном случае.
Например: производим поиск по файлу.
Определены следующие варианты выхода из функции:
Последний вариант работы функции наиболее спорен. Обычно программисты очень изобретательны при обосновании своих решений, и могут найти кучу доводов «за» и «против»: неэффективность, клиентскому коду необходимо знать детали реализации, неправильный формат является ошибкой и т.д. Все это правда, но не главное. Основной довод «за» — достаточность.
Клиенту больше не надо анализировать сложный результат, он попросил найти текст — ему нашли; если нет — он об этом даже не узнает.
Еще пример: обработка нажатия пользователем кнопки «cancel». Опять же можно долго дискутировать на тему — для чего изначально создавался этот механизм?
Но если он есть и подходит для логики «cancel» — глупо его не использовать.
Когда мне нужно было подклеить дома порожек, я, недолго думая, использовал в качестве груза книгу М.Мак-Дональда «Wpf …». Так почему же в иных случаях мне поступать иначе?
Как вы относитесь к маленьким функциям? Я вот считаю, что другие просто не имею�� права на существование. При таком отношении периодически возникают ситуации, что без поднятия исключений просто никуда. Например, надо выйти из цикла, расположенного выше по стеку вызовов. Не передавать же флаг завершения, в конце концов. Или передать…
Если же цикл располагается не на предыдущем уровне стека вызовов, а выше? Весь код без 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 превращается в анализ результатов работы функций. Доходит до смешного — выделяются методы проверяющие результаты работы других функций. Сидишь и думаешь об откате неожиданно «неправильного» рефакторинга. Но все меняется, когда приходят они: код начинает удаляться по две-три строчки за раз (а если я тормозил
до этого достаточно долго, то удаляются и методы, и классы)
А вот еще один интересный «аромат» — большое количество условных операторов. Когда перед выполнением какого-либо действия начинается анализ, «а можно ли это сделать». Оставим в покое алгоритмы, состоящие из нескольких операций, которые по отдельности делают состояние системы неопределенным. Если начал делать, — разбейся в лепешку, но закончи! Я не про них. Хотя и здесь, при желании, можно найти приемлемые решения. Так вот, запах от большего количества условных операторов: ну например, при анализе набора входных данных перед выполнением операции или проверка валидности структуры данных, или… ну мало ли чего еще. В общем, смотрим на каждый элемент данных и решаем, а подходит ли он для наших великих целей, а затем осуществляем их. Не глупо ли? Можно же сразу попробовать осуществить задуманное. И всего-то надо — предположить, что на данном участке может произойти исключение, связанное с нашим нежеланием загромождать код мусором.
Основным аргументом у противников такого отношения к подъему исключений является просадка производительности. Глупо с этим спорить — условный оператор в несколько раз более эффективен. Но есть одно «но»: проблему надо доказать на реальных данных. Предположим, участок кода действительно критический. О ужас! — теперь придется потратить пару часов на рефакторинг. Не дней, а именно часов. Помните как рассказывал Фаулер о своем отношении к проектированию: «…я пытаюсь определить, насколько трудно окажется рефакторинг от одного дизайна в другой. Если трудностей не видно, то я, не слишком задумываясь о выборе, останавливаюсь на самом простом».
Если предположить, что мы принимаем эту смелую мысль, то осталось определиться с порядком рефакторинга:
- Выделяем метод из тела метода поднимающего исключение.
- Определяем критерий в сигнатуре выделенного метода, который будет показывать, что в иной ситуации мы бы подняли исключение.
- Изменяем исходный метод таким образом, чтобы он, в зависимости от критерия, поднимал исключение. Будем называть исходный метод «методом с исключением», а выделенный — «метод без исключения».
- Создаем метод расширение (копированием) из метода без исключения.
- Заменяем тело метода расширения на вызов метода с исключением.
- Блокируем распространение исключения через метод расширения и, в зависимости от подъема, взводим или сбрасываем критерий.
- Добавляем к интерфейсу метод с сигнатурой метода б��з исключения. В реализациях этих методов производим вызов метода расширения. Если метод не входит в состав внешнего интерфейса (по отношению к классу) — тем лучше. Этот пункт можно пропустить.
- Заменяем вызовы метода с исключениями методами без исключения. В реализациях интерфейса это обязательно (иначе не следовало и начинать), в остальных случаях — по желанию. Замещающий код должен выглядеть так: вызов метода без исключения, анализ критерия.
- Создаем метод расширения из метода с исключением. Структура его должна уже быть верной. Осталось только сделать его рабочим.
- Заменяем оставшиеся вызововы с исключениями на расширения без исключения.
- Удаляем из интерфейса методы с исключениями и расширения без исключения. Расширения с исключениями по возможности.
Вот как это будет выглядеть в коде.
- Исходный код:
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();
}
}
В принципе все. Надеюсь, я сумел развеять хотя бы часть страхов о подъеме исключений, и теперь при возникновении такой необходимости вы более лояльно будете выбирать решение. И скорее всего, выберите более простое. А значит, ваш приемник будет ругать вас с меньшим остервенением (не исключено что им буду я).