F # аналог внедрения зависимостей для реального проекта - PullRequest
0 голосов
/ 04 сентября 2018

Вопрос основан на большом посте, связанном с F # / DI: https://fsharpforfunandprofit.com/posts/dependency-injection-1/

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

Интересно, как сценарий, описанный в этом посте, сработает / переведет в более реальный пример. Числа ниже немного с неба, поэтому, пожалуйста, отрегулируйте их так, как считаете нужным.

Рассмотрим некоторый достаточно небольшой код на основе C # DI / TDD / EF. Первый проект:

Корень композиции: 20 интерфейсов с 10 методами (в среднем) на каждый интерфейс. Хорошо, это, вероятно, слишком много методов для интерфейса, но, к сожалению, они часто имеют тенденцию к увеличению по мере развития кода. Я видел намного больше. Из них 10 являются внутренними службами без какого-либо ввода-вывода (нет базы данных / «чистых» функций в мире функций), 5 являются внутренними службами ввода-вывода (локальные базы данных и т. П.), А последние 5 являются внешними службами (например, внешняя база данных ( s) или что-либо еще, что вызывает некоторые удаленные сторонние службы).

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

Существует несколько уровней тестов: модульные тесты, интеграционные тесты (два уровня), приемочные тесты.

Модульные тесты: все вызовы моделируются с помощью соответствующей настройки макета (с использованием некоторого стандартного инструмента, такого как Moq, например). Итак, есть как минимум 20 * 10 = 200 юнит-тестов. Обычно их больше, потому что тестируются несколько различных сценариев.

Интеграционные тесты (уровень 1): все внутренние сервисы без ввода-вывода являются реальными, все внутренние сервисы, связанные с вводом-выводом, являются фальшивками (обычно в БД в памяти), а все внешние сервисы проксируются на некоторые фальшивые / фиктивные. По сути, это означает, что все внутренние службы ввода-вывода, например SomeInternalIOService: ISomeInternalIOService, заменяются на FakeSomeInternalIOService: ISomeInternalIOService, а все внешние службы ввода-вывода, например SomeExternalIOService: ISomeExternalIOService, заменяются на FakeSomeExternalIOService: ISomeExternalIOService. Таким образом, существует 5 поддельных внутренних операций ввода-вывода и 5 поддельных внешних операций ввода-вывода и примерно такое же количество тестов, как указано выше.

Интеграционные тесты (уровень 2): Все внешние сервисы (включая теперь связанные с локальной базой данных) являются реальными, и все внешние сервисы проксируются к некоторым другим фальшивым / ложным сообщениям, которые позволяют тестировать сбои внешних сервисов. По сути это означает, что все внешние службы ввода-вывода, такие как SomeExternalIOService: ISomeExternalIOService, заменяются на BreakableFakeSomeExternalIOService: ISomeExternalIOService. Существует 5 различных (хрупких) внешних поддельных услуг ввода-вывода. Допустим, у нас около 100 таких тестов.

Приемочный тест: все реально, но файлы конфигурации указывают на некоторые «тестовые» версии внешних сервисов. Допустим, таких тестов около 50.

Интересно, как это перешло бы в мир F #. Очевидно, что многие вещи будут очень разными и некоторые вещи могут даже не существовать в мире F # !

Большое спасибо!

PS Я не ищу точного ответа. Было бы достаточно «направления» с некоторыми идеями.

Ответы [ 2 ]

0 голосов
/ 05 сентября 2018

Просто, чтобы добавить к отличному ответу Томаса, вот еще несколько предложений.

Использовать конвейеры для каждого рабочего процесса

Как упоминал Томас, в проектировании FP мы склонны использовать проекты, ориентированные на конвейер, с одним конвейером для каждого сценария использования / рабочего процесса / сценария.

Что хорошо в этом подходе, так это то, что каждый из этих конвейеров может быть настроен независимо , со своим собственным корнем композиции.

Вы говорите, что у вас есть 20 интерфейсов с 10 методами в каждом. Нужен ли каждому рабочему процессу все эти интерфейсы и методы? По моему опыту, отдельному рабочему процессу могут понадобиться только некоторые из них, и в этом случае логика в корне композиции становится намного проще.

Если рабочий процесс действительно требует более 5 параметров, скажем, возможно, стоит создать структуру данных для хранения этих зависимостей и передать ее в:

module BuyWorkflow =

    type Dependencies = {
       SaveSomething : Something -> AsyncResult<unit,DbError>
       LoadSomething : Key -> AsyncResult<Something,DbError>
       SendEmail : EmailMessage -> AsyncResult<unit,EmailError>
       ...
       }

    // define the workflow 
    let buySomething (deps:Dependencies) = 
        asyncResult {
           ...
           do! deps.SaveSomething ...
           let! something = deps.LoadSomething ...
        }

Обратите внимание, что зависимости - это, как правило, отдельные функции, а не целые интерфейсы. Вы должны только попросить вас нужно!

Рассмотрим наличие более одного «корня композиции»

Вы можете рассмотреть возможность использования нескольких «составных корней» - один для внутренних служб и один для внешних.

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

Например, в сборке «Ядро» может быть модуль, который запекает внутренние чистые сервисы. Вот некоторый псевдокод:

module Workflows =

    // set up pure services
    let internalServiceA = ...
    let internalServiceB = ...
    let internalServiceC = ...

    // set up workflows
    let homeWorkflow = homeWorkflow internalServiceA.method1 internalServiceA.method2 
    let buyWorkflow = buyWorkflow internalServiceB.method2 internalServiceC.method1 
    let sellWorkflow = ...

Затем вы используете этот модуль для «Интеграционных тестов (уровень 1)». На данный момент рабочие процессы по-прежнему не имеют своих внешних зависимостей, поэтому вам потребуется предоставить макеты для тестирования.

Аналогично, в сборке «API» вы можете иметь составной корень, в котором предоставляются внешние службы.

module Api =

    // load from configuration
    let dbConnectionA = ...
    let dbConnectionB = ...

    // set up impure services
    let externalServiceA = externalServiceA(dbConnectionA)
    let externalServiceB = externalServiceB(dbConnectionB)
    let externalServiceC = ...

    // set up workflows
    let homeWorkflow = Workflows.homeWorkflow externalServiceA.method1 externalServiceA.method2 
    let buyWorkflow = Workflows.buyWorkflow externalServiceB.method2 externalServiceC.method1 
    let sellWorkflow = ...

Затем в ваших «Интеграционных тестах (уровень 2)» и другом коде верхнего уровня вы используете рабочие процессы Api:

// setup routes (using Suave/Giraffe style)
let routes : WebPart =
  choose [
    GET >=> choose [
      path "/" >=> Api.homeWorkflow 
      path "/buy" >=> Api.buyWorkflow 
      path "/sell" >=> Api.sellWorkflow 
      ]
  ]   

Приемочные испытания (с разными файлами конфигурации) могут использовать один и тот же код.

0 голосов
/ 04 сентября 2018

Я думаю, что один ключевой вопрос, от которого зависит ответ, заключается в том, какова схема связи с внешним вводом-выводом, которой следует приложение, и насколько сложна логика, управляющая взаимодействиями.

В простом сценарии у вас есть что-то вроде этого:

+-----------+      +---------------+      +---------------+      +------------+
| Read data | ---> | Processing #1 | ---> | Processing #2 | ---> | Write data |
+-----------+      +---------------+      +---------------+      +------------+

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

В более сложном сценарии у вас есть что-то вроде этого:

+----------+      +----------------+      +----------+      +------------+      +----------+
| Some I/O | ---> | A bit of logic | ---> | More I/O | ---> | More logic | ---> | More I/O |
+----------+      +----------------+      +----------+      +------------+      +----------+

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

  • Передача функций (и частичное применение) - это простой функциональный подход, который будет работать, если вам не нужно передавать слишком много параметров.

  • Используйте более объектно-ориентированную архитектуру с интерфейсами - F # - это смешанный язык FP и OO, поэтому он также имеет хорошую поддержку для этого. Особенно использование анонимных реализаций интерфейса означает, что вам часто не нужны библиотеки-насмешки.

  • Используйте шаблон «интерпретатор», где вычисления написаны на (встроенном) доменно-ориентированном языке, который описывает, какие вычисления и какие операции ввода / вывода необходимо выполнить (фактически не делая этого). Затем вы можете по-разному интерпретировать DSL в реальном и тестовом режимах.

  • В некоторых функциональных языках (в основном Scala и Haskell) людям нравится делать вышеупомянутое, используя технику, называемую «свободными монадами», но, на мой взгляд, типичное описание этого имеет тенденцию быть слишком сложным. (т.е. если вы знаете, что такое свободная монада, это может быть полезным указателем, но в противном случае вам, вероятно, лучше не попадать в эту кроличью нору).

...