Niedawno czytałem książkę zatytułowaną Programowanie funkcjonalne w języku C # i przychodzi mi do głowy, że niezmienna i bezstanowa natura programowania funkcjonalnego osiąga wyniki podobne do wzorców wstrzykiwania zależności i jest prawdopodobnie jeszcze lepszym podejściem, szczególnie w odniesieniu do testów jednostkowych.
Byłbym wdzięczny, gdyby ktokolwiek, kto ma doświadczenie w obu podejściach, mógł podzielić się swoimi przemyśleniami i doświadczeniami, aby odpowiedzieć na podstawowe pytanie: czy programowanie funkcjonalne jest realną alternatywą dla wzorców wstrzykiwania zależności?
Odpowiedzi:
Zarządzanie zależnościami jest dużym problemem w OOP z dwóch następujących powodów:
Większość programistów OO uważa ścisłe połączenie danych i kodu za całkowicie korzystne, ale wiąże się to z pewnymi kosztami. Zarządzanie przepływem danych przez warstwy jest nieuniknioną częścią programowania w dowolnym paradygmacie. Połączenie danych i kodu dodaje dodatkowy problem, że jeśli chcesz użyć funkcji w pewnym momencie, musisz znaleźć sposób, aby dostać się do tego obiektu.
Stosowanie efektów ubocznych stwarza podobne trudności. Jeśli używasz efektu ubocznego do niektórych funkcji, ale chcesz mieć możliwość zamiany jego implementacji, właściwie nie masz innego wyjścia, jak wstrzyknąć tę zależność.
Rozważmy jako przykład program spamujący, który usuwa strony internetowe w poszukiwaniu adresów e-mail, a następnie wysyła je pocztą elektroniczną. Jeśli masz sposób myślenia DI, teraz myślisz o usługach, które będziesz enkapsulować za interfejsami i które usługi zostaną tam wprowadzone. Zostawię ten projekt jako ćwiczenie dla czytelnika. Jeśli masz nastawienie FP, teraz myślisz o wejściach i wyjściach dla najniższej warstwy funkcji, takich jak:
Gdy myślisz o wejściach i wyjściach, nie ma zależności funkcji, tylko zależności danych. Dzięki temu są tak łatwe do przetestowania w jednostce. Następna warstwa organizuje przekazywanie danych wyjściowych jednej funkcji do danych wejściowych kolejnej i może w razie potrzeby łatwo wymieniać różne implementacje.
W bardzo realnym sensie programowanie funkcjonalne w naturalny sposób prowadzi do odwrócenia zależności funkcji i dlatego zwykle nie trzeba podejmować żadnych specjalnych działań po tym fakcie. Gdy to zrobisz, narzędzia takie jak funkcje wyższego rzędu, zamknięcia i częściowe zastosowanie ułatwiają to przy mniejszej liczbie płyt kotłowych.
Zauważ, że same zależności nie są problematyczne. Zależności wskazują niewłaściwie. Następna warstwa może mieć funkcję:
Zupełnie dobrze jest, aby ta warstwa miała tak zakodowane zależności, ponieważ jej jedynym celem jest sklejenie funkcji dolnej warstwy razem. Zamiana implementacji jest tak prosta, jak utworzenie innej kompozycji:
Ta łatwa rekompozycja jest możliwa dzięki brakowi efektów ubocznych. Funkcje niższej warstwy są całkowicie od siebie niezależne. Następna warstwa może wybrać, która
processText
jest faktycznie używana na podstawie konfiguracji użytkownika:Znowu nie jest to problem, ponieważ wszystkie zależności wskazują w jedną stronę. Nie musimy odwracać niektórych zależności, aby wszystkie wskazywały w ten sam sposób, ponieważ czyste funkcje już nas do tego zmusiły.
Zauważ, że możesz to uczynić o wiele bardziej sprzężonym, przechodząc
config
do najniższej warstwy zamiast sprawdzać ją u góry. FP nie przeszkadza ci to robić, ale sprawia, że staje się o wiele bardziej irytujący, jeśli spróbujesz.źródło
System.String
. System modułowy pozwoliłby zastąpićSystem.String
zmienną, tak aby wybór implementacji łańcucha nie był zakodowany na stałe, ale nadal był rozstrzygany w czasie kompilacji.To uderza mnie jako dziwne pytanie. Metody programowania funkcjonalnego są w dużej mierze styczne do wstrzykiwania zależności.
Oczywiście, posiadanie niezmiennego stanu może zmusić cię do tego, aby nie „oszukiwać”, wywołując skutki uboczne lub wykorzystując stan klasy jako domyślną umowę między funkcjami. To sprawia, że przekazywanie danych jest bardziej wyraźne, co, jak sądzę, jest najbardziej podstawową formą wstrzykiwania zależności. A koncepcja programowania funkcjonalnego polegająca na przekazywaniu funkcji znacznie ułatwia.
Ale to nie usuwa zależności. Twoje operacje nadal potrzebują wszystkich danych / operacji, których potrzebowali, gdy stan był zmienny. I nadal musisz jakoś zdobyć te zależności. Nie powiedziałbym więc, że funkcjonalne metody programowania w ogóle zastępują DI, więc nie są żadną alternatywą.
Jeśli już, to właśnie pokazali ci, jak zły kod OO może tworzyć ukryte zależności, niż programiści rzadko myślą.
źródło
Szybka odpowiedź na twoje pytanie brzmi: nie .
Ale jak twierdzą inni, pytanie łączy dwie, nieco niepowiązane koncepcje.
Zróbmy to krok po kroku.
DI skutkuje niefunkcjonalnym stylem
Podstawą programowania funkcji są funkcje czyste - funkcje, które odwzorowują dane wejściowe na dane wyjściowe, dzięki czemu zawsze uzyskuje się takie same dane wyjściowe dla danego wejścia.
DI zwykle oznacza, że urządzenie nie jest już czyste, ponieważ moc wyjściowa może się różnić w zależności od iniekcji. Na przykład w następującej funkcji:
getBookedSeatCount
(funkcja) może się różnić, dając różne wyniki dla tego samego danych wejściowych. To również czynibookSeats
nieczystym.Istnieją wyjątki od tego - możesz wstrzyknąć jeden z dwóch algorytmów sortujących, które implementują to samo odwzorowanie wejścia-wyjścia, aczkolwiek używając różnych algorytmów. Ale to są wyjątki.
System nie może być czysty
Fakt, że system nie może być czysty, jest równie ignorowany, jak to jest stwierdzone w źródłach programowania funkcjonalnego.
System musi mieć skutki uboczne, a oczywistymi przykładami są:
Więc część twojego systemu musi wiązać się z efektami ubocznymi, a ta część może również obejmować imperatywny styl lub styl OO.
Paradygmat shell-core
Pożyczając terminy z doskonałej rozmowy Gary'ego Bernhardta na temat granic , dobra architektura systemowa (lub modułowa) będzie obejmować te dwie warstwy:
Kluczową kwestią jest podzielenie systemu na jego czystą część (rdzeń) i nieczystą (powłokę).
Mimo że oferuje nieco wadliwe rozwiązanie (i wniosek), artykuł Marka Seemanna proponuje tę samą koncepcję. Wdrożenie Haskell jest szczególnie wnikliwe, ponieważ pokazuje, że można to wszystko zrobić przy użyciu FP.
DI i FP
Stosowanie DI jest całkowicie uzasadnione, nawet jeśli większość aplikacji jest czysta. Kluczem jest ograniczenie DI w nieczystej powłoce.
Przykładem mogą być kody pośredniczące API - chcesz mieć prawdziwy interfejs API w produkcji, ale używaj kodów pośredniczących w testowaniu. Przestrzeganie modelu z rdzeniem skorupowym bardzo pomoże tutaj.
Wniosek
Zatem FP i DI nie są dokładnie alternatywami. Prawdopodobnie będziesz mieć oba w swoim systemie, a rada polega na zapewnieniu separacji między czystą i nieczystą częścią systemu, w której rezydują odpowiednio FP i DI.
źródło
Z punktu widzenia OOP funkcje można uznać za interfejsy jednej metody.
Interfejs to silniejszy kontrakt niż funkcja.
Jeśli korzystasz z podejścia funkcjonalnego i wykonujesz dużo DI, to w porównaniu z podejściem OOP otrzymasz więcej kandydatów na każdą zależność.
vs
źródło