Если вы открываете какой-то код, обычно есть два состояния: либо обработка ошибок разбросана по всему коду отвлекающим образом, из-за чего трудно увидеть, что делает основной алгоритм, либо нет обработки ошибок, что предполагает маловероятный сценарий, что ничего не происходит. когда-нибудь пойдет не так. В этом посте я представлю шаблон, который принесет золотую середину в эти два состояния.

Если мы хотим улучшить обработку ошибок в нашем коде, первое, от чего мы должны избавиться, — это исключения. Под исключением я подразумеваю ту вещь в вашем языке, которая требует от вас использования специального синтаксиса для работы с ней. Многие языки создают что-то вроде try/catch в качестве специального синтаксиса. Вместо того, чтобы генерировать исключения, мы можем просто возвращать ошибки в обычном порядке. Многие языки позволяют вам возвращать только одно значение, поэтому они добавляют возможность генерировать исключения в качестве обходного пути, и на самом деле все функции могут либо возвращать ошибку (через ключевое слово throw), либо значение (через ключевое слово return).

В этой настройке у нас есть два возможных типа возврата

  • Ожидаемое возвращаемое значение или счастливый путь
  • Ошибка или печальный путь

Мы можем создать оболочку, которая будет либо содержать ошибку, либо ожидаемое возвращаемое значение. Назовем эту оболочку Failable.

Неудачник задает два вопроса: что мне делать в случае успеха? Что делать в случае неудачи? На самом деле это еще не принесло нам многого, потому что у нас уже были эти два вопроса, и то, как мы ответили на них, заставило нас начать задаваться вопросом, есть ли лучший способ. Давайте искать лучший ответ.

Железнодорожный путь

Сила несостоятельности проявляется, когда мы начинаем связывать их вместе. Одна мощная модель состоит в том, чтобы думать об этом как о железнодорожном полотне. У нас есть два параллельных пути: Вершина — это счастливый путь, на котором не возникает ошибок. Внизу — печальный путь, где произошла ошибка. Мы начинаем счастливый путь, потому что мы ничего не сделали.

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

Реализация

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

Есть два способа потерпеть неудачу. Почти у каждого языка/сообщества есть свое мнение о том, как должны вызываться эти методы (метод/функция/процедура/и т. д.), но я собираюсь перегрузить proceed() и использовать его для обоих методов. Proceed() примет функцию и вернет другой Failable. Вторая версия будет разворачивать ошибочное возвращаемое значение после запуска функции, поэтому вы не получите Failable, содержащий другой Failable.

Неудачное продолжение(func: (AnyValue) -> AnyValue)

or

Неудачная операция (func: (AnyValue) -> Failable)

Когда вы создаете новый Failable, у вас будет выбор: ввести ошибку или значение.

новый Неудачный (значение)

or

новый Сбой (ошибка)

Это даст вам два железнодорожных пути. Если вы вызовете proceed() для объекта Failable, который содержит ошибку, proceed() ничего не сделает и просто вернет себя. Если вы вызываете proceed() для объекта Failable со значением, он вызовет переданную функцию, используя значение внутри объекта failable в качестве входных данных для функции, и вернет объект Failable с новым значением (или ошибкой). ) внутри.

Если вы хотите объединить вызовы Failable в цепочку, второй метод proceed() развернет для вас результат первого вызова Failable. Вы можете столкнуться с некоторыми проблемами лексической области, если вам нужен доступ к возвращаемым значениям для предыдущих вызовов Failable. Каждый язык обрабатывает это немного по-своему, так что это выходит за рамки этой статьи.

Дополнительные мысли

Здесь я должен открыть вам маленький грязный секрет. Реализуя этот шаблон, вы только что построили что-то с монадическим поведением. Возможно, теоретики категорий скажут, что это не монада по строгому определению, но с практической точки зрения это в основном монада.

Если вы когда-либо использовали Scala, то могли заметить, что тип Try реализует этот шаблон. Другие языки также могут иметь этот шаблон, но я не знаю ни одного другого объекта, в котором он был бы встроен. Этот шаблон является особой версией того, что многие языки называют типом Both. Существует довольно много языков, которые реализуют этот тип.

Еще одна концепция, которую я видел в дикой природе, заключается в добавлении концепции пустого состояния (например, null). Вы бы добавили дополнительный способ создания типа Failable, который позволил бы вам передать значение null.

новый Неудачный (нулевой)

В Failable с нулевым значением proceed() ничего не сделает, кроме возврата текущего Failable, как если бы вы передали ошибку.

Вывод

Шаблон Failable может предоставить мощный способ унифицировать обработку ошибок (и, возможно, обработку null) и упростить код. Собирая потенциальные ошибки во время выполнения, вы можете запрограммировать счастливый путь без перемежающейся обработки ошибок. В конце счастливого пути вы можете обрабатывать ошибки, тем самым отделяя обработку ошибок от бизнес-логики. Это также помогает избавиться от альтернативных форм управления потоком (т. е. try/catch), упрощая анализ вашего кода.