Рекомендации по обработке исключений |
Этот пост является продолжением нашей серии «Чистый код». Вы можете найти предыдущий пост здесь.
Вы, наверное, уже слышали о Чернобыльской катастрофе, верно? Если не, HBO сделал действительно отличный мини-сериал относительно этого. В любом случае, я имею в виду, что это очень хороший пример того, как НЕ справляться с проблемами. Обнаруженные ошибки не были обработаны соответствующим образом, заинтересованные стороны не были уведомлены об этом, процесс не был остановлен в нужное время и т.д. Возможно, ваш код не станет причиной ядерной катастрофы, но всегда важно правильно выполнять свою работу и следовать рекомендациям по обработке исключений.
Решение проблемы
Давайте начнем с некоторого кода. Ранее мы разрабатывали робота высшего уровня, и теперь кажется, что нам был назначен тикет относительно того, что наш робот не загружается должным образом. Нам удалось отследить его до следующего фрагмента кода:
public void StartRobot()
{
var flightModule = GetFlightModule();
if(flightModule == null)
{
try
{
InitializeWheelsSpinningSystem();
}
catch (Exception ex) { LogException(ex); }
}
else
{
try
{
OpenWings();
}
catch (Exception ex) { LogException(ex); }
}
try
{
InitializeDefenseModule();
}
catch (Exception ex) { LogException(ex); }
try
{
InitializeTargetingSystem();
}
catch (Exception ex) { LogException(ex); }
try
{
InitializeAttackModule();
}
catch (Exception ex) { LogException(ex); }
}
Ужасно, правда? Оказывается, здесь мы в значительной степени полагаемся на удачу, если происходит какое-либо исключение, мы просто регистрируем его и надеемся на лучшее. Я имею в виду, если наша система наведения неисправна, как мы можем запустить наш атакующий модуль? Хотели бы вы иметь рядом с собой робота, который не может отличить союзника от врага? Начнем с маленьких шагов:
public void StartRobot()
{
try
{
var flightModule = GetFlightModule();
if (flightModule == null)
{
InitializeWheelsSpinningSystem();
}
else
{
OpenWings();
}
InitializeDefenseModule();
InitializeTargetingSystem();
InitializeAttackModule();
}
catch (Exception ex)
{
LogException(ex);
}
}
Это все еще плохо, но, по крайней мере, если мы столкнемся с какими-либо проблемами, мы остановимся на нашем методе и избежим побочного ущерба. Но все же, как правило, плохая идея обрабатывать все исключения, метод должен обрабатывать только то, что он должен (и что он может). Есть разные способы сделать это, давайте рассмотрим некоторые из них:
catch (Exception ex) when (ex.Message == "Defense module initialization error")
{
LogException(ex);
}
Таким образом, мы будем обрабатывать исключения только с ожидаемым сообщением. Этот подход лучше подходит, когда у вас нет контроля над кодом, который вызывает исключение, например, пакет NuGet. Обычно мы всегда должны генерировать и перехватывать определенные исключения, например:
public class DefenseModuleInitializationException : Exception
{
public DefenseModuleInitializationException(string message) : base(message)
{
}
}
При разработке пользовательского исключения вы также должны помнить о некоторых передовых методах:
- Добавьте суффикс «Исключение»
- Наследовать от класса Exception
- При необходимости добавьте настраиваемые свойства (в данном случае — нет).
Таким образом, наш код должен выглядеть следующим образом:
public void StartRobot()
{
try
{
var flightModule = GetFlightModule();
if (flightModule == null)
{
InitializeWheelsSpinningSystem();
}
else
{
OpenWings();
}
InitializeDefenseModule();
InitializeTargetingSystem();
InitializeAttackModule();
}
catch (DefenseModuleInitializationException ex)
{
LogException(ex);
}
}
Я не показывал реализацию каждого метода, потому что это не актуально, но давайте проанализируем, что делает GetFlightModule:
private IFlightModule GetFlightModule()
{
if (!InternationalInformationsProvider.CountryAllowsRobotsToFly("Britania"))
throw new Exception("Can't fly :(");
try
{
var module = new FlightModule();
return module;
}
catch (Exception ex)
{
return null;
}
}
По возможности следует избегать создания исключений, поскольку они обходятся дорого. Если мы можем справиться со сценарием с проверками, мы должны пойти на это. В этом случае, например, мы могли бы просто вернуть null и позволить вызывающему методу обработать его должным образом.
Но говоря о возврате null, перехват исключения только для возврата null не является правильным способом обработки ошибок. То, что у нас могут возникнуть проблемы с созданием FlightModule, не означает, что у нас их нет. Если вы еще раз проверите метод StartRobot, он решит, что, поскольку FlightModule отсутствует, это наземный робот, что, вероятно, приведет к другой ошибке. Правильный способ действовать здесь — это вообще не справиться. Это может показаться странным, но если вы не можете справиться с этим правильно, не делайте этого.
В итоге наш метод должен быть таким:
private IFlightModule GetFlightModule()
{
if (!InternationalInformationsProvider.CountryAllowsRobotsToFly("Britania"))
return null;
var module = new FlightModule();
return module;
}
Есть еще несколько изменений, которые мы могли бы внести в наш код, например, поскольку наши пользовательские исключения ясно указывают на то, что они, возможно, будут выброшены только внутри метода InitializeDefenseModule, мы могли бы уменьшить наш блок try/catch, чтобы немного уменьшить вложенность, например этот:
public void StartRobot()
{
var flightModule = GetFlightModule();
if (flightModule == null)
{
InitializeWheelsSpinningSystem();
}
else
{
OpenWings();
}
try
{
InitializeDefenseModule();
}
catch (DefenseModuleInitializationException ex)
{
LogException(ex);
}
InitializeTargetingSystem();
InitializeAttackModule();
}
Заключение
После нашего рефакторинга мы можем извлечь следующие рекомендации по обработке исключений:
- Будьте осторожны при перехвате всех исключений
- Поместите как можно больше подробностей в свое исключение
- Избегайте возврата null при перехвате исключения, только если это имеет смысл
- При необходимости создавайте собственные исключения
- Если возможно, обработайте условие вместо создания исключений.
- Не обрабатывать исключение, если это не входит в обязанности метода.
И при создании пользовательских исключений мы должны:
- Наследовать от класса Exception
- Добавьте пользовательские свойства по мере необходимости
- Добавьте суффикс «Exception» к имени класса