Мы можем улучшить обработку и композицию ошибок, используя класс Result и несколько других инструментов из мира функционального программирования.
Часть серии Функциональное программирование: чистая архитектура и DDD
Вместо того, чтобы выдавать ошибки, мы оборачиваем наши результаты. Либо Результат является значением ошибки, либо значением успеха в процессе документирования возможных ошибок. Вызывающие должны сначала изучить и развернуть результат, обрабатывая либо успех, либо отказ. Прокладывая путь к более функциональному программированию и композиции.
Для более полного ознакомления с классом Result и железнодорожным программированием:
- https://fsharpforfunandprofit.com/rop/
- https://dev.to/_gdelgado/type-safe-error-handling-in-typescript-1p4n
- https://khalilstemmler.com/articles/enterprise-typescript-nodejs/handling-errors-result-class/
Для реализации проверьте GitHub #railway-Oriented-programming; ROP во многих языках программирования (Python/Go/Java/C#/F# и т. д.)
В этих сериях я поделюсь своими открытиями во время моего (захватывающего) путешествия.
Императивный образец
const r = doSomeAction() // Result<string, SomeVariableIsInvalid | ServiceUnavailableError> if (r.isErr()) { // r: Error<SomeVariableIsInvalid | ServiceUnavailableError> if (r.error instanceof SomeVariableIsInvalid) { ctx.body = r.error.message ctx.statusCode = 400 } else { ctx.statusCode = 500 } return } // r: Ok<string> ctx.body = r.value ctx.statusCode = 200
doSomeAction
может быть реализовано так:
function doSomeAction(): Result<string, SomeVariableIsInvalid | ServiceUnavailableError> { if (!someVariableIsValid) { return err(new SomeVariableIsInvalid("some variable is not valid") } if (!isServiceAvailable()) { return err(new ServiceUnavailableError("The service is currently unavailable") } return ok("success response") }
Функциональный образец
doSomeAction() // Result<string, SomeVariableIsInvalid | ServiceUnavailableError> .map(value => { ctx.body = value ctx.statusCode = 200 }) .mapErr(error => { if (error instanceof SomeVariableIsInvalid) { ctx.body = error.message ctx.statusCode = 400 } else { ctx.statusCode = 500 } })
Все «операторы» должны жить в объекте Result, поэтому расширение сложнее. (Это похоже на то, как, например, запустился RxJS)
Функциональный состав
doSomeAction() // Result<string, SomeVariableIsInvalid | ServiceUnavailableError> .pipe( map(value => { ctx.body = value ctx.statusCode = 200 }), mapErr(error => { if (error instanceof SomeVariableIsInvalid) { ctx.body = error.message ctx.statusCode = 400 } else { ctx.statusCode = 500 } }) )
Теперь операторы — это просто функции, которые легко расширить и свернуть самостоятельно ;-) (пользователи RxJS v5.5 могут увидеть здесь некоторое сходство)
Данные последние
const pipeline = pipe( map(value => { ctx.body = value ctx.statusCode = 200 }), mapErr(error => { if (error instanceof SomeVariableIsInvalid) { ctx.body = error.message ctx.statusCode = 400 } else { ctx.statusCode = 500 } }) ) pipeline(doSomeAction())
Итак, pipeline
теперь можно использовать повторно. Если бы только tc39 offer-pipeline-operator поскорее приземлился, чтобы мы получили синтаксический сахар, который скроет некоторые шаблоны и синтаксический шум :)
Строительство сверху
const writeSuccessResponse = value => { ctx.body = value ctx.statusCode = 200 } const writeErrorResponse = error => { if (error instanceof SomeVariableIsInvalid) { ctx.body = error.message ctx.statusCode = 400 } else { ctx.statusCode = 500 } } const pipeline = pipe( map(writeSuccessResponse), mapErr(writeErrorResponse) )
Дальнейшая декомпозиция на отдельные функции, чтобы они стали повторно используемыми, или для разделения уровней абстракции, чтобы пайплайн стал легче читать.
Дальнейшее разложение:
const writeSuccessResponse = value => { ctx.body = value ctx.statusCode = 200 } const writeDefaultErrorResponse = error => { ctx.statusCode = 500 } const writeSomeVariableIsInvalidErrorResponse = error => { if (error instanceof SomeVariableIsInvalid) { ctx.body = error.message ctx.statusCode = 400 } } const pipeline = pipe( map(writeSuccessResponse), mapErr(writeDefaultErrorResponse), mapErr(writeSomeVariableIsInvalidErrorResponse), )
Возможно другой вариант:
const mapErrIf = (errorHandler: error => void, predicate: error => boolean) => error => { if (!predicate(error)) { return } errorHandler(error) } } // usage mapErrIf(_ => ctx.statusCode = 400, error => error instanceOf SomeVariableIsInvalid)
И, конечно же, есть много других вариантов и форм композиции, пусть это будет упражнением для читателя ;-)
Фреймворк и пример кода
Изучая эти темы, я работаю над структурой приложения, которая широко использует состав конвейера, включая пример приложения!
Исходный код:
- пример приложения
- никогда не бросайте удлинители труб
- Использует форк neverthrow (форк gDelgado14/neverthrow)
Что дальше
Далее в этой серии я планирую представить более продвинутые концепции, такие как flatMap
, toTup
, tee
и другие :)
дальнейшее чтение
Не забудьте также проверить gcanti/fp-ts; очень функциональная библиотека, ориентированная на программирование, особенно v2 выглядит очень многообещающе из-за похожего состава каналов!
Первоначально опубликовано на https://patrickroza.com.