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

Часть серии Функциональное программирование: чистая архитектура и DDD

Вместо того, чтобы выдавать ошибки, мы оборачиваем наши результаты. Либо Результат является значением ошибки, либо значением успеха в процессе документирования возможных ошибок. Вызывающие должны сначала изучить и развернуть результат, обрабатывая либо успех, либо отказ. Прокладывая путь к более функциональному программированию и композиции.

Для более полного ознакомления с классом Result и железнодорожным программированием:

Для реализации проверьте 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)

И, конечно же, есть много других вариантов и форм композиции, пусть это будет упражнением для читателя ;-)

Фреймворк и пример кода

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

Исходный код:

фреймворк fp-приложения

Что дальше

Далее в этой серии я планирую представить более продвинутые концепции, такие как flatMap, toTup, tee и другие :)

дальнейшее чтение

Не забудьте также проверить gcanti/fp-ts; очень функциональная библиотека, ориентированная на программирование, особенно v2 выглядит очень многообещающе из-за похожего состава каналов!

Первоначально опубликовано на https://patrickroza.com.