Приключения DI в мире функционального программирования
Когда-то давным-давно (в 2011 году), Марк Симан написал отличную книгу «Внедрение зависимостей в .NET». Всё, что нужно знать про DI в ООП там есть. Маст рид. А несколько лет спустя Марк ударился в функциональное программирование и написал достаточно резонансную серию небольших статей "From dependency injection to dependency rejection" (а также поездил с докладом).
Сегодня у нас будет TL;DR по этой серии.
Итак, Марк начинает с демонстрации «классического» ООП внедрения зависимостей через конструктор объекта. Классика. Все мы делали это. А дальше Марк задаётся вопросом: а как быть в с ФП? Конструктора у нас нет, а значит все параметры нужно передавать на вход функции. Это неудобно, наш API становится чудовищным.
Первое, что приходит в голову, это частичное применение. Напишем функцию с 4 параметрами, первые 3 из которых это зависимости (код на F#):
«Приклеим» зависимости
На выходе имеем функцию
Дальше Марк делает классный трюк, и превращает код из F# в C# средствами .NET (через его промежуточное представление IL). И код на C# выглядит как обычный объект с внедрением в конструкторе!
Решена ли задача? Можно ли сказать, что частичное применение решает задачу внедрения зависимостей в ФП? Марк предлагает решить спор используя Haskell. F# язык хороший, но он позволяет много больше, чем «настоящие» ФП языки. И что же у нас происходит в Хаскеле? Если мы объявим
Т.е. частичное применение решает вопрос внедрения зависимостей, но оно делает код нефункциональным. Чтобы функции остались чистыми, мы должны отказаться от внедрения зависимостей. И тут Марк переходит к последнему шагу. Он переписывает
Эта функция не требует внедрения зависимостей, в ней нет сайд-эффетов. Сайд-эффекты поднимаются на уровень функции-композиции, в ней происходит вся «грязь»
В итоге Марк постулирует, что функциональное программирование должно отказаться от понятия внедрения зависимостей. Чистые функции не могут вызывать нечистые функции. Мы должны оставить наше ядро чистым и поднять всё нечистое до уровня границы с внешним миром (и снова привет тебе, архитектура портов и адаптеров).
Когда-то давным-давно (в 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)
В итоге Марк постулирует, что функциональное программирование должно отказаться от понятия внедрения зависимостей. Чистые функции не могут вызывать нечистые функции. Мы должны оставить наше ядро чистым и поднять всё нечистое до уровня границы с внешним миром (и снова привет тебе, архитектура портов и адаптеров).