Найти в Дзене

Паттерн Result и управление потоком выполнения программы

В этой статье я не буду открывать Америку — материалов про паттерн Result на любом языке очень много. Но он мне очень понравился, так что я решил поделиться им и с вами, уважаемые подписчики, и конечно же, как и всегда, в контексте Revit API. Это классический вариант управления потоком в C# и в Revit API тоже. Давайте рассмотрим его на примерах:
Пример 1. У нас есть 2 линии, нам надо найти точку их пересечения. Написать метод, который принимает на вход 2 Line, возвращает XYZ. Метод будет выглядеть, допустим, так: Давайте посмотрим на поток выполнения: 1. Мы проверяем, не параллельны ли линии, и если да, кидаем исключение. 2. Если линии не параллельны, то мы возвращаем точку пересечения (как именно, нам сейчас всё равно). Теперь давайте представим, что это метод из сторонней библиотеки. Что мы видим: сигнатура метода обещает нам, что вернёт XYZ. Реальность такова, что метод может кинуть исключение и ничего не вернуть. Сторонний API нас обманывает. Пример 2: создадим пол через Revit API
Оглавление

В этой статье я не буду открывать Америку — материалов про паттерн Result на любом языке очень много. Но он мне очень понравился, так что я решил поделиться им и с вами, уважаемые подписчики, и конечно же, как и всегда, в контексте Revit API.

Управление потоком выполнения через исключения

Это классический вариант управления потоком в C# и в Revit API тоже. Давайте рассмотрим его на примерах:

Пример 1. У нас есть 2 линии, нам надо найти точку их пересечения. Написать метод, который принимает на вход 2 Line, возвращает XYZ. Метод будет выглядеть, допустим, так:

Давайте посмотрим на поток выполнения:

1. Мы проверяем, не параллельны ли линии, и если да, кидаем исключение.

2. Если линии не параллельны, то мы возвращаем точку пересечения (как именно, нам сейчас всё равно).

Теперь давайте представим, что это метод из сторонней библиотеки. Что мы видим: сигнатура метода обещает нам, что вернёт XYZ. Реальность такова, что метод может кинуть исключение и ничего не вернуть. Сторонний API нас обманывает.

Пример 2: создадим пол через Revit API. Вот у нас и метод даже есть Create. И даже статья у меня про это есть. Присмотримся к методу повнимательнее:

-2

5 видов исключений. Сигнатура обещает нам экземпляр класса Floor, реальность говорит, что мы вызов этого метода должны обернуть в 5 блоков catch на каждое исключение (их же по-разному надо обрабатывать в идеальном мире, да), а потом добивает сверху исключением контекста Ревита и фатальной ошибкой (сам метод о нём молчит). Удобно? Неудобно, но привычно.

Пример 3: Метод get_Parameter. Обещает вернуть Parameter, на деле может вернуть null. Сигнатура метода об этом не говорит. Так же и метод AsString() может вернуть null вместо пустой строки, если значение параметра ни разу не было заполнено. И каждый раз нам надо это держать в голове и проверять на null. Когда это забываешь, получаешь исключения, и хорошо если на стадии самотестирования кода. А если через полгодика? И вспоминай потом, где у тебя null.

Управление потоком через паттерн Result

Этот паттерн пришёл из функционального программирования. C# — не функциональный язык, но некоторые фишки этого стиля можно использовать и в C#.

В чём суть: мы создаём 3 обобщённых класса: Result<T>, и его наследники: Success<T> и Error<T>. Если у вас будет больше вариантов ветвления, можно создать больше наследников, никто вас не ограничивает. Каждый метод, который мы пишем, возвращает не объект SomeClass, а Result<SomeClass>.

Далее мы просто проверяем, является ли этот объект Success<SomeClass>, и, если да, то используем его поле Value как результат, возвращая в нём объект. Если же вернулся Error<T>, то мы ничего сделать не можем — поля Value у него просто нет.

Вот так выглядят наши вспомогательные классы (это
record, я написал их в 1 строку, по-хорошему всё равно для каждого такого класса в 1 строку отдельный файл).

-3

Тогда управление потоком в нашем первом примере становится таким:

-4

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

-5

И даже можно обработать ошибку, но без генерации исключения:

-6

Но, что важно: если мы получили Error, мы даже случайно не можем пойти дальше, ведь поля Value у него просто нет.

Преимущества


1. Более очевидные сигнатуры методов, не нужно держать кучу подводных камней в голове

2. Более быстрая обработка ошибок: генерация исключения и его поимка через catch занимают много времени, тут такого нет, мы остаёмся в том же потоке выполнения, ничего не выбрасываем, можем просто пропустить ошибку, если знаем, что она не важная.

3. Теперь конструкция try-catch служит для обработки серьёзных ошибок, вроде StakOverflow или OutOfMemory, а простые исключения из-за неправильных аргументов мы не используем

4. Это просто красиво. Вместе с pattern-matching от C# это прям песня:
if (result is Success<XYZ> intersectionPoint). Мне очень нравится.

5. Как приятно было бы работать с Revit API, когда не надо каждый чих проверять на null. Может быть, и таких бы статей не писал.

Заключение

Сегодня без репозитория на гитхабе, код очень простой. Но вы можете на меня подписаться там, вам несложно, мне приятно. Если нет аккаунта на гитхабе — заводите, не архиовм же вы будете свои плагины другим людям присылать, несолидно.

И как всегда, не забывайте заходить в
самый лучший телеграм-канал о Revit API на русском языке и подписываться на него. До новых встреч!

-7

Наука
7 млн интересуются