Является ли функциональное программирование жизнеспособной альтернативой шаблонам внедрения зависимостей?

21

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

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

Мэтт Кашатт
источник
10
Это не имеет большого смысла для меня, неизменность не удаляет зависимости.
Теластин
Я согласен, что это не удаляет зависимости. Возможно, мое понимание неверно, но я сделал такой вывод, потому что, если я не могу изменить исходный объект, он должен потребовать, чтобы я передал его (вставил) любой функции, которая его использует.
Мэтт Кашатт
5
Существует также « Как заставить OO-программистов любить функциональное программирование» , что действительно является детальным анализом DI как с точки зрения OO, так и с точки зрения FP.
Роберт Харви
1
Этот вопрос, статьи, на которые он ссылается, и принятый ответ также могут быть полезны: stackoverflow.com/questions/11276319/… Игнорировать страшное слово монады. Как указывает Рунар в своем ответе, в данном случае это не сложная концепция (просто функция).
Брюс

Ответы:

27

Управление зависимостями является большой проблемой в ООП по следующим двум причинам:

  • Тесная связь данных и кода.
  • Повсеместное использование побочных эффектов.

Большинство OO-программистов считают, что тесная связь данных и кода является полностью выгодной, но это обходится дорого. Управление потоком данных через слои является неизбежной частью программирования в любой парадигме. Сопоставление ваших данных и кода добавляет дополнительную проблему, заключающуюся в том, что если вы хотите использовать функцию в определенный момент, вы должны найти способ доставить ее объект к этой точке.

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

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

  • Введите адрес веб-страницы, выведите текст этой страницы.
  • Введите текст страницы, выведите список ссылок с этой страницы.
  • Введите текст страницы, выведите список адресов электронной почты на этой странице.
  • Введите список адресов электронной почты, выведите список адресов электронной почты с удаленными дубликатами.
  • Введите адрес электронной почты, выведите спам на этот адрес.
  • Введите спам-сообщение, выведите команды SMTP для отправки этого сообщения.

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

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

Обратите внимание, что проблема заключается не в самих зависимостях. Это зависимости, которые указывают неверный путь. Следующий слой может иметь такую ​​функцию:

processText = spamToSMTP . emailAddressToSpam . removeEmailDups . textToEmailAddresses

Для этого слоя вполне нормально иметь жестко запрограммированные зависимости, так как его единственная цель - склеить функции нижнего уровня. Поменять реализацию так же просто, как создать другую композицию:

processTextFancy = spamToSMTP . emailAddressToFancySpam . removeEmailDups . textToEmailAddresses

Это простое перекомпонование стало возможным благодаря отсутствию побочных эффектов. Функции нижнего уровня полностью независимы друг от друга. Следующий уровень может выбрать, какой из processTextних фактически используется, основываясь на некоторой пользовательской конфигурации:

actuallyUsedProcessText = if (config == "Fancy") then processTextFancy else processText

Опять же, не проблема, потому что все зависимости указывают в одну сторону. Нам не нужно инвертировать некоторые зависимости, чтобы все они указывали одинаково, потому что чистые функции уже заставили нас сделать это.

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

Карл Билефельдт
источник
3
«Использование побочных эффектов создает аналогичные трудности. Если вы используете побочный эффект для какой-либо функциональности, но хотите иметь возможность поменять его реализацию, у вас практически нет другого выбора, кроме как внедрить эту зависимость». Я не думаю, что побочные эффекты имеют какое-либо отношение к этому. Если вы хотите поменять местами реализации в Haskell, вам все равно придется внедрять зависимости . Desugar классы типов, и вы передаете интерфейс в качестве первого аргумента для каждой функции.
Доваль
2
Суть в том, что почти каждый язык вынуждает вас жестко ссылаться на ссылки на другие модули кода, поэтому единственный способ поменять местами реализации - использовать динамическую диспетчеризацию повсюду, и тогда вы застреваете, решая свои зависимости во время выполнения. Система модулей позволит вам выразить граф зависимостей во время проверки типов.
Доваль
@ Доваль - Спасибо за ваши интересные и заставляющие задуматься комментарии. Возможно, я вас неправильно понял, но я правильно понял из ваших комментариев, что если бы я использовал функциональный стиль программирования поверх стиля DI (в традиционном смысле C #), то я бы избежал возможных проблем с отладкой, связанных с временем выполнения разрешение зависимостей?
Мэтт Кашатт
@MatthewPatrickCashatt Дело не в стиле или парадигме, а в особенностях языка. Если язык не поддерживает модули как первоклассные вещи, вам придется выполнить некоторую динамическую диспетчеризацию формы и внедрение зависимостей, чтобы поменять местами реализации, потому что нет способа выразить зависимости статически. Иными словами, если ваша программа на C # использует строки, она имеет жестко-зависимую зависимость System.String. Модульная система позволит вам заменить System.Stringпеременную, чтобы выбор строковой реализации не был жестко запрограммирован, но все же решался во время компиляции.
Доваль
8

Является ли функциональное программирование жизнеспособной альтернативой шаблонам внедрения зависимостей?

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

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

Но это не удаляет зависимости. Для ваших операций по-прежнему нужны все данные / операции, которые были необходимы им, когда ваше состояние было изменчивым. И вам все равно нужно как-то получить эти зависимости. Так что я бы не сказал, что подходы функционального программирования вообще заменяют DI, поэтому не являются альтернативой.

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

Telastyn
источник
Еще раз спасибо за участие в разговоре, Теластин. Как вы указали, мой вопрос не очень хорошо продуман (мои слова), но благодаря полученной здесь обратной связи я начинаю немного лучше понимать, что именно вспыхивает в моем мозгу по поводу всего этого: мы все согласны (Я думаю), что юнит-тестирование может быть кошмаром без DI. К сожалению, использование DI, особенно с контейнерами IoC, может создать новую форму кошмара отладки благодаря тому, что он разрешает зависимости во время выполнения. Подобно DI, FP упрощает модульное тестирование, но без проблем с зависимостями во время выполнения.
Мэтт Кашатт
(продолжение сверху). , Это мое настоящее понимание в любом случае. Пожалуйста, дайте мне знать, если я не попал в цель. Я не против признать, что я просто смертный среди великанов!
Мэтт Кашатт
@MatthewPatrickCashatt - DI не обязательно подразумевает проблемы с зависимостями во время выполнения, которые, как вы заметили, ужасны.
Теластин
7

Быстрый ответ на ваш вопрос: нет .

Но, как утверждают другие, этот вопрос объединяет два, несколько не связанных понятия.

Давайте сделаем это шаг за шагом.

DI приводит к нефункциональному стилю

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

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

const bookSeats = ( seatCount, getBookedSeatCount ) => { ... }

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

Для этого есть исключения: вы можете внедрить один из двух алгоритмов сортировки, которые реализуют одно и то же отображение ввода-вывода, хотя и с использованием разных алгоритмов. Но это исключения.

Система не может быть чистой

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

Система должна иметь побочные эффекты с очевидными примерами:

  • UI
  • База данных
  • API (в архитектуре клиент-сервер)

Таким образом, часть вашей системы должна включать побочные эффекты, и эта часть может также включать императивный стиль или стиль OO.

Парадигма ядро-оболочка

Заимствуя термины из превосходного доклада Гэри Бернхардта о границах , хорошая архитектура системы (или модуля) будет включать в себя следующие два уровня:

  • ядро
    • Чистые функции
    • разветвление
    • Нет зависимостей
  • Ракушка
    • Нечистый (побочные эффекты)
    • Нет ветвления
    • зависимости
    • Может быть обязательным, включать ОО стиль и т. Д.

Главное, что нужно сделать, - это «разделить» систему на ее чистую часть (ядро) и нечистую часть (оболочку).

Хотя в статье Марка Симанна предлагается несколько ошибочное решение (и вывод), в ней предлагается та же концепция. Реализация Haskell особенно проницательна, поскольку показывает, что все это можно сделать с помощью FP.

DI и FP

Использование DI вполне разумно, даже если основная часть вашего приложения чистая. Ключ заключается в том, чтобы ограничить ДИ внутри нечистой оболочки.

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

Вывод

Так что FP и DI не совсем альтернативы. Скорее всего, в вашей системе есть и то и другое, и совет состоит в том, чтобы обеспечить разделение между чистой и нечистой частью системы, где находятся соответственно FP и DI.

Izhaki
источник
Когда вы ссылаетесь на парадигму «ядро-оболочка», как можно добиться отсутствия ветвления в оболочке? Я могу вспомнить много примеров, когда приложение должно было бы делать ту или иную нечистую вещь, основываясь на значении. Это правило без ветвления применимо в таких языках, как Java?
Джрахали
@jrahhali Пожалуйста, см. разговор Гари Бернхардта для деталей (ссылка в ответе).
Изаки
еще один релевантный сериал Seemann blog.ploeh.dk/2017/01/27/…
jk.
1

С точки зрения ООП функции могут рассматриваться как интерфейсы с одним методом.

Интерфейс - это более сильный контракт, чем функция.

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

void DoStuff(Func<DateTime> getDateTime) {}; //Anything that satisfies the signature can be injected.

против

void DoStuff(IDateTimeProvider dateTimeProvider) {}; //Only types implementing the interface can be injected.
логово
источник
3
Любой класс может быть упакован для реализации интерфейса, так что «более сильный контракт» не намного сильнее. Что еще более важно, присвоение каждой функции различного типа делает невозможным создание функции.
Доваль
Функциональное программирование не означает «Программирование с функциями более высокого порядка», оно относится к гораздо более широкой концепции, функции высшего порядка - это всего лишь один метод, полезный в парадигме.
Джимми Хоффа