Функциональный C#: Обработка исключений

Мы продолжаем цикл статей о функциональном C#. Сегодняшняя часть заключительная, и мы в ней рассмотрим вопрос обработки исключений и ошибок. Предлагаем прочитать предыдущие статьи:

  1. Функциональный C#: Неизменные объекты
  2. Функциональный C#: Одержимость примитивами
  3. Функциональный C#: Ненулевые ссылочные типы
  4. Функциональный C#: Обработка исключений

Обработка ошибок: основной подход

Концепция проверок и обработок исключений хорошо известна многим из вас, но код, необходимый для этого, может действительно раздражать. По крайней мере в таком языке программирования как C#. Эта статья вдохновлена концепцией Railway Oriented Programming, которую представил Scott Wlaschin в своей презентации. Советуем вам просмотреть его полное выступление, так как это даст вам бесценные знания о том, каким неудобным может быть C# и как это обойти.

Посмотрите на пример ниже:

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

Когда же вы начинаете отлавливать ошибки, то код становится похожим на что-то подобное:

Дела обстоят еще хуже, если при возникновении ошибки, нам потребуется отменить последнюю операцию. Тогда наш код станет еще больше:

Ну наконец-то! Теперь уж точно все. Но есть одна проблема… Наш метод ранее состоял из 5 строк, а сейчас их целых 35! Семикратное увеличение! И это только один простой метод. Более того, теперь нам трудно ориентироваться в написанном. Эти самые 5 строчек значимого кода погребены под толщей обработок исключений.

Обработка исключений и ошибок ввода в функциональном стиле

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

Вы, возможно, заметили, что мы используем технику, описанную в одной из наших прошлых статей. Вместо того, чтобы использовать примитивы, мы используем классы. Например, CustomerName и BillingInfo. Это позволяет ставить всю проверку на корректность в одном месте и придерживаться принципа DRY.

Статический метод Create возвращает объект класса Result, который инкапсулирует всю необходимую информацию, касающуюся результатов операции — сообщение об ошибке в случае неудачи и экземпляр объекта в случае успеха.

Кроме того, обратите внимание, что функции, которые могли бы вызвать ошибки, обернуты в конструкцию try/catch. Такой подход нарушает одно из лучших практик, которая описана в этой статье. Суть заключается в том, что если вы знаете, как бороться с исключением, то обработать его следует на минимально возможном уровне. Давайте перепишем код:

Как вы могли заметить, мы оборачиваем все возможные места ошибок в Result. Работа такого класса очень похожа на работу монады Maybe, о которой мы говорили в одной из прошлых статей. Используя Result, мы можем анализировать код, не глядя в детали реализации. Вот как выглядит сам класс (некоторые детали опущены для краткости):

Теперь мы можем применить тот же принцип, который используется в функциональных языках. Вот, где происходит настоящая магия:

Если вы знакомы с функциональными языками программирования, то вы, возможно, заметили, что метод OnSuccess — это, фактически, метод Bind.

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

Метод OnFailure работает с точностью до наоборот, как вы могли догадаться: выполняет текущий метод только в том случае, если предыдущая операция привела к ошибке.

OnBoth находится в конце цепочки. Используется он для вывода различного рода сообщений и логов.

Итак, мы написали нужный нам метод с обработками исключений и ошибок, но с гораздо меньшим количеством кода. Более того, обратите внимание, что теперь намного легче понимать, что вообще делает метод.

А что насчет принципа CQS?

CQS — Command-Query Separation — принцип императивного программирования. Он гласит, что каждый метод должен быть либо командой, которая выполняет какое-то действие, либо запросом, возвращающим данные. Есть ли у нас конфликт с данным принципом?

Нет. И более того, наш подход увеличивает читаемость кода таким же способом, что и в принципе CQS. Но теперь потенциальный диапазон информации, которую мы получаем от наших методов, расширился вдвое. Вместо 2 (значение null и какой-либо объект, возвращаемый запросом) у нас их 4:

    • Метод-команда, который не может привести к ошибке:

public void Save(Customer customer)

    • Метод-запрос, который не может привести к ошибке:

public Customer GetById(long id)

    • Метод-команда, который может привести к ошибке:

public Result Save(Customer customer)

    • Метод-запрос, который может привести к ошибке:

public Result GetById(long id)

Когда говорится, что метод не может привести к ошибке, мы не имеем в виду то, что ошибки не может быть совсем. Всегда есть какая-то вероятность, что выпадет исключение (особенно там, где его никто не ожидал). Под методами, которое не могут привести к ошибке, подразумеваются такие функции, которые должны всегда работать без исключений. То есть любая произошедшая в них ошибка является неожиданной.

Вывод

Очень важно смотреть на свой код и сразу понимать, что там выполняется. Код должен быть читаем. Но одновременно с этим вы должны уделять большое внимание обработке исключений. Однако классический подход довольно громоздкий и неудобный. Именно поэтому есть смысл применять технику, описанную в этой статье.

В сочетании с другими тремя техниками — неизменными объектами, избавлением от одержимости примитивами и необнуляемыми ссылочными типами — этот подход представляет мощную модель программирования, которая позволяет существенно повысить производительность.

Перевод статьи «Functional C#: Handling failures, input errors»