Как выглядит функциональный подход к архитектуре веб-приложений?

P.S. Пример выполнен в виде Scala, но язык не имеет особого значения, меня интересует функциональный подход в целом.

Обычно я видел такую ​​картину

outer world -> controller -> serviceA -> serviceB -> DB Accessor

Итак, мы получили слои, в которых функция внешнего слоя вызывает функцию внутреннего слоя. (Для простоты я опускаю упаковку монад IO)

class Controller(service: ServiceA) {
  def call(req): res {
    val req1 = someWorkBefore(req)
    val res1 = service.call(req1)
    someWorkAfter(res1)
  }

  private someWorkBefore(req): req1
  private someWorkAfter(res1): res
}

class ServiceA(service: ServiceB) {
  def call(req1): res1 {
    val req2 = someWorkBefore(req1)
    val res2 = service.call(req2)
    someWorkAfter(res2)
  }

  private someWorkBefore(req1): req2
  private someWorkAfter(res2): res1
}

class ServiceB(db: DBAccessor) {
  def call(req2): res2 {
    val req3 = someWorkBefore(req2)
    val res3 = service.call(req3)
    someWorkAfter(res3)
  }

  private someWorkBefore(req2): req3
  private someWorkAfter(res3): res2
}

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

Другой вариант — как-то забыть о разделении задач и собрать все в одном месте. (Для простоты я опускаю упаковку монад IO)

class Controller(serviceA: ServiceA, serviceB: ServiceB, db: DBAccessor) {
  def call(req): res = {
    val req1 = someWorkBefore(req)
    val req2 = serviceA.someWorkBefore(req1)
    val req3 = serviceB.someWorkBefore(req2)
    val res3 = db.call(req3)
    val res2 = serviceB.someWorkAfter(res3)
    val res1 = serviceA.someWorkAfter(res2)
    someWorkAfter(res1)
  }

  private someWorkBefore(req): req1
  private someWorkAfter(res1): res
}

Это выглядит лучше, потому что каждая функция в сервисах в некотором роде чиста и не зависит от других вещей, которые следует высмеивать, но функция Controller теперь представляет собой беспорядок. Какие еще варианты архитектуры можно рассмотреть?

Проблема здесь не в ФП и IO ее не решит (насколько я это люблю и использую). Проблема просто в том, что ваш код смоделирован неправильно. Если вам нужно смоделировать метод private вашего класса для его тестирования, то это проблема. - Кстати, вы также можете рассмотреть возможность сценарного тестирования как альтернативы модульному тестированию, что может помочь вам избежать некоторого рефакторинга: jducoeur.medium.com/…

Luis Miguel Mejía Suárez 16.08.2024 15:40

Да, я знаю, что мой вопрос связан не с IO (даже из примера убрал), а со структурой кода. Причина, по которой я упомянул FP, заключается в том, чтобы привлечь людей, у которых, по моему мнению, больше шансов дать хороший совет. И потому что я использую Scala.

Егор Лебедев 16.08.2024 16:17

Хорошо, тогда возникает вопрос: зачем вам нужно имитировать частные методы, чтобы протестировать Controller? Все должно быть протестировано Controller — это имитация ServiceA, если вы хотите следовать традиционному модульному тестированию. Или вы можете использовать другую стратегию тестирования.

Luis Miguel Mejía Suárez 16.08.2024 17:12

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

Егор Лебедев 16.08.2024 17:28

Почему вы считаете это «неоптимальным»? Как бы вы это сделали в кодовой базе, отличной от FP? Вот что я имею в виду, когда говорю, что вопрос не связан с ФП. - В любом случае, да, проверьте пост в блоге о тестировании сценариев, которым я поделился, у меня такое ощущение, что это больше соответствует тому, что вы хотите.

Luis Miguel Mejía Suárez 16.08.2024 18:22
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
51
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

Что делать вместо этого, зависит от того, какой именно язык вы используете, и от используемых в нем идиом. В Haskell, например, свободные монады могут быть одним из способов решения таких проблем, но хотя свободные монады технически возможны и в F#, я бы не считал их идиоматическими в этом контексте.

Поскольку веб-приложения являются своего рода интерактивным программным обеспечением , я обычно нахожу их хорошо подходящими для архитектуры Functional Core, Imperative Shell . Другая метафора — рассматривать такую ​​архитектуру как сэндвич. Выполните нечистые действия, вызовите чистую функцию, выполните еще несколько нечистых действий и выйдите.

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

Короче говоря, в функциональном программировании (по крайней мере, во всех примерах, которые я видел) точка входа всегда нечиста (ср. действия Haskell main), поэтому вы помещаете все нечистое как можно ближе к точке входа, а затем вызовите чистые функции со значениями, полученными в результате нечистых действий.

Идея перенести нечистые функции в точку входа звучит круто и просто. Я думаю, это кажется странным, когда кто-то приходит из какого-то опыта ООП, когда нечистая база данных, например, находится на самом низком уровне вложенных вызовов.

Егор Лебедев 16.08.2024 11:12

Взгляните на этот пример с ZIO — blog.pierre-ricadat.com/… — аналогичный подход можно использовать для Cats Effect: функции домена работают на case class/sealed/Scala 3 enum, ошибки обрабатываются с помощью Eithers, некоторые репозитории работают с монадой ввода-вывода, контроллеры затем выполняют преобразование между представлениями API/DTO и моделями предметной области, делегируя операции предметной области чистым функциям и сохраняемость нечистым (монадам ввода-вывода).

Mateusz Kubuszok 16.08.2024 13:16

Идея функционального веб-приложения заключается в простом наличии функции «Запрос -> Ответ».

Ivan Klass 16.08.2024 17:05

Другие вопросы по теме