Управление зависимостями является большой проблемой в ООП по следующим двум причинам:
- Тесная связь данных и кода.
- Повсеместное использование побочных эффектов.
Большинство 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 не мешает вам делать это, но, как правило, делает его намного более раздражающим, если вы пытаетесь.