Чи є функціональне програмування життєздатною альтернативою моделям введення залежності?


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, вам все одно доведеться робити введення залежності . Обезлюдьте типи класів, і ви передаєте інтерфейс як перший аргумент кожної функції.
Доваль

2
Суть у тому, що майже кожна мова змушує вас до жорсткого кодування посилань на інші модулі коду, тому єдиний спосіб змінити реалізацію - використовувати динамічну диспетчеру скрізь, і тоді ви зациклюєтесь на вирішенні ваших залежностей під час виконання. Модульна система дозволить вам виразити графік залежності під час перевірки типу.
Доваль

@ Doval - Дякуємо за ваші цікаві та вражаючі зауваження коментарі. Можливо, я вас зрозумів неправильно, але чи правильно я виводжу з ваших коментарів, що якби я використовував функціональний стиль програмування над стилем DI (у традиційному сенсі C #), то я б уникну можливих відладок розладів, пов’язаних із часом виконання вирішення залежностей?
Метт Кашатт

@MatthewPatrickCashatt Це не питання стилю чи парадигми, а мовних особливостей. Якщо мова не підтримує модулі як першокласні речі, вам доведеться виконати певну форму динамічної диспетчеризації та ін'єкції залежностей, щоб здійснити обмін реалізаціями, оскільки немає можливості статично виразити залежності. Якщо говорити трохи інакше, якщо у вашій програмі C # використовуються рядки, вона має жорстко кодовану залежність System.String. Модульна система дозволить вам замінити System.Stringзмінну, щоб вибір реалізації рядка не був жорстко закодований, але все-таки вирішений під час компіляції.
Доваль

8

чи функціональне програмування є життєздатною альтернативою моделям введення залежності?

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

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

Але це не знімає залежності. Вашим операціям все ще потрібні всі ті дані / операції, які вони були потрібні, коли ваш стан змінювався. І вам все-таки потрібно якось дістати ці залежності там. Тому я б не сказав, що функціональні підходи до програмування замінюють DI взагалі, тому альтернативи не є.

Якщо що-небудь, вони щойно показали вам, як поганий код OO може створити неявні залежності, ніж рідко думають програмісти.


Ще раз дякую за те, що ви долучилися до розмови, Теластин. Як ви вже зазначали, моє запитання не дуже добре побудоване (мої слова), але завдяки відгукам тут я починаю трохи краще розуміти, що це таке, що іскриться в моєму мозку про все це: ми всі згодні (Я думаю), що тестування на одиницях може бути кошмаром, не маючи DI. На жаль, використання DI, особливо з контейнерами IoC, може створити нову форму налагодження кошмару завдяки тому, що він вирішує залежності під час виконання. Як і в DI, FP полегшує тестування приладів, але без проблем із залежністю від часу виконання.
Метт Кашатт

(продовжується зверху). . . Це все-таки моє теперішнє розуміння. Будь ласка, дайте мені знати, якщо я пропускаю позначку. Я не проти визнати, що я тут просто смертний серед гігантів!
Метт Кашатт

@MatthewPatrickCashatt - DI не обов'язково передбачає проблеми залежності від часу виконання, які, як ви зазначаєте, жахливі.
Теластин

7

Швидка відповідь на ваше запитання: Ні .

Але, як стверджують інші, питання стосується двох, дещо не пов'язаних між собою понять.

Давайте зробимо це покроково.

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

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

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

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

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

Для цього є винятки - ви можете вводити один з двох алгоритмів сортування, які реалізують одне і те ж відображення вводу-виводу, хоча і використовуючи різні алгоритми. Але це винятки.

Система не може бути чистою

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

Система повинна мати побічні ефекти, очевидними прикладами є:

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

Тому частина вашої системи повинна включати побічні ефекти, і ця частина також може включати в себе імперативний стиль або стиль ОО.

Парадигма оболонки

Запозичивши умови з чудової розмови про Гері Бернхардта про межі , хороша архітектура системи (або модуля) включатиме ці два шари:

  • Основні
    • Чисті функції
    • Відгалуження
    • Ніяких залежностей
  • Оболонка
    • Нечисті (побічні ефекти)
    • Відсутні розгалуження
    • Залежності
    • Може бути імперативним, включати стиль ОО тощо.

Ключовим способом є "розщеплення" системи на чисту частину (серцевина) і нечисту частину (оболонку).

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

DI та FP

Використовувати DI абсолютно розумно, навіть якщо основна частина вашої заявки є чистою. Ключовим моментом є обмеження DI в нечистій оболонці.

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

Висновок

Тож FP і DI не є абсолютно альтернативою. Ви, ймовірно, маєте і те, і інше у своїй системі, і порада - забезпечити розділення між чистою та нечистою частиною системи, де FP та DI проживають відповідно.


Якщо ви посилаєтесь на парадигму основної оболонки, як би не досягти розгалуження в оболонці? Я можу привести багато прикладів, коли програмі потрібно буде робити ту чи іншу чисту річ на основі значення. Чи застосовується це правило без розгалуження в таких мовах, як Java?
jrahhali

@jrahhali Будь ласка, перегляньте розмову Гарі Бернхардта для детальної інформації (пов'язана у відповіді).
Іжакі

черговий релавент із
jk.

1

З точки зору OOP функції можна вважати інтерфейсами однометодними.

Інтерфейс - сильніший контракт, ніж функція.

Якщо ви використовуєте функціональний підхід і робите багато ІР, то порівняно з підходом ООП ви отримаєте більше кандидатів на кожну залежність.

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
Будь-який клас може бути завернутий для реалізації інтерфейсу, тому "сильніший контракт" не набагато сильніший. Більш важливо надати кожній функції різного типу, що робить кордон неможливим для складання функції.
Доваль

Функціональне програмування не означає "Програмування з функціями вищого порядку", воно стосується набагато ширшого поняття, функції вищого порядку - лише одна методика, корисна в парадигмі.
Джиммі Хоффа
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.