Приключения DI в мире функционального программирования



Когда-то давным-давно (в 2011 году), Марк Симан написал отличную книгу «Внедрение зависимостей в .NET». Всё, что нужно знать про DI в ООП там есть. Маст рид. А несколько лет спустя Марк ударился в функциональное программирование и написал достаточно резонансную серию небольших статей "From dependency injection to dependency rejection" (а также поездил с докладом).



Сегодня у нас будет TL;DR по этой серии.



Итак, Марк начинает с демонстрации «классического» ООП внедрения зависимостей через конструктор объекта. Классика. Все мы делали это. А дальше Марк задаётся вопросом: а как быть в с ФП? Конструктора у нас нет, а значит все параметры нужно передавать на вход функции. Это неудобно, наш API становится чудовищным.



Первое, что приходит в голову, это частичное применение. Напишем функцию с 4 параметрами, первые 3 из которых это зависимости (код на F#):





let tryAccept capacity readReservations createReservation reservation =

let reservedSeats =

readReservations reservation.Date |> List.sumBy (fun x -> x.Quantity)

if reservedSeats + reservation.Quantity <= capacity

then createReservation { reservation with IsAccepted = true } |> Some

else None







«Приклеим» зависимости





let tryAcceptComposition =

let read = DB.readReservations connectionString

let create = DB.createReservation connectionString

tryAccept 10 read create





На выходе имеем функцию tryAcceptComposition в которую уже внедрены зависимости и остался один входной параметр reservation.



Дальше Марк делает классный трюк, и превращает код из F# в C# средствами .NET (через его промежуточное представление IL). И код на C# выглядит как обычный объект с внедрением в конструкторе!



Решена ли задача? Можно ли сказать, что частичное применение решает задачу внедрения зависимостей в ФП? Марк предлагает решить спор используя Haskell. F# язык хороший, но он позволяет много больше, чем «настоящие» ФП языки. И что же у нас происходит в Хаскеле? Если мы объявим tryAccept чистой функций, то код не скомпилируется. Потому что зависимости несут сайд-эффекты (походы в базу данных).



Т.е. частичное применение решает вопрос внедрения зависимостей, но оно делает код нефункциональным. Чтобы функции остались чистыми, мы должны отказаться от внедрения зависимостей. И тут Марк переходит к последнему шагу. Он переписывает tryAccept так, чтобы на входе были только «чистые» значения:





let tryAccept capacity reservations reservation =

let reservedSeats = reservations |> List.sumBy (fun x -> x.Quantity)

if reservedSeats + reservation.Quantity <= capacity

then { reservation with IsAccepted = true } |> Some

else None





Эта функция не требует внедрения зависимостей, в ней нет сайд-эффетов. Сайд-эффекты поднимаются на уровень функции-композиции, в ней происходит вся «грязь»





let flip f x y = f y x



let tryAcceptComposition reservation =

reservation.Date

|> DB.readReservations connectionString

|> flip (tryAccept 10) reservation

|> Option.map (DB.createReservation connectionString)





В итоге Марк постулирует, что функциональное программирование должно отказаться от понятия внедрения зависимостей. Чистые функции не могут вызывать нечистые функции. Мы должны оставить наше ядро чистым и поднять всё нечистое до уровня границы с внешним миром (и снова привет тебе, архитектура портов и адаптеров).