W jaki sposób inwersja zależności jest związana z funkcjami wyższego rzędu?

41

Dzisiaj właśnie widziałem ten artykuł, który opisał znaczenie zasady SOLID w rozwoju F #

F # i zasady projektowania - SOLID

Odnosząc się do ostatniego - „zasady inwersji zależności”, autor powiedział:

Z funkcjonalnego punktu widzenia te pojemniki i koncepcje wtrysku można rozwiązać za pomocą prostej funkcji wyższego rzędu lub wzorca typu dziura w środku, które są wbudowane bezpośrednio w język.

Ale nie wyjaśnił tego dalej. Moje pytanie brzmi: w jaki sposób inwersja zależności jest związana z funkcjami wyższego rzędu?

Gulszan
źródło

Odpowiedzi:

38

Inwersja zależności w OOP oznacza, że ​​kodujesz w interfejsie, który jest następnie zapewniany przez implementację w obiekcie.

Języki obsługujące funkcje wyższego języka mogą często rozwiązać proste problemy z odwracaniem zależności, przekazując zachowanie jako funkcję zamiast obiektu, który implementuje interfejs w sensie OO.

W takich językach sygnatura funkcji może stać się interfejsem, a funkcja jest przekazywana zamiast tradycyjnego obiektu w celu zapewnienia pożądanego zachowania. Dobrym tego przykładem jest dziura w środkowym wzorze.

Pozwala osiągnąć ten sam wynik przy mniejszym kodzie i większej ekspresji, ponieważ nie trzeba implementować całej klasy, która jest zgodna z interfejsem (OOP), aby zapewnić pożądane zachowanie osoby wywołującej. Zamiast tego możesz po prostu przekazać prostą definicję funkcji. W skrócie: Kod jest często łatwiejszy w utrzymaniu, bardziej wyrazisty i bardziej elastyczny, gdy używa się funkcji wyższego rzędu.

Przykład w C #

Tradycyjne podejście:

public IEnumerable<Customer> FilterCustomers(IFilter<Customer> filter, IEnumerable<Customers> customers)
{
    foreach(var customer in customers)
    {
        if(filter.Matches(customer))
        {
            yield return customer;
        }
    }
}

//now you've got to implement all these filters
class CustomerNameFilter : IFilter<Customer> /*...*/
class CustomerBirthdayFilter : IFilter<Customer> /*...*/

//the invocation looks like this
var filteredDataByName = FilterCustomers(new CustomerNameFilter("SomeName"), customers);
var filteredDataBybirthDay = FilterCustomers(new CustomerBirthdayFilter(SomeDate), customers);

Z funkcjami wyższego rzędu:

public IEnumerable<Customer> FilterCustomers(Func<Customer, bool> filter, IEnumerable<Customers> customers)
{
    foreach(var customer in customers)
    {
        if(filter(customer))
        {
            yield return customer;
        }
    }
}

Teraz implementacja i wywoływanie stają się mniej kłopotliwe. Nie musimy już dostarczać implementacji IFilter. Nie musimy już implementować klas dla filtrów.

var filteredDataByName = FilterCustomers(x => x.Name.Equals("CustomerName"), customers);
var filteredDataByBirthday = FilterCustomers(x => x.Birthday == SomeDateTime, customers);

Oczywiście może to już zrobić LinQ w C #. Użyłem tego przykładu, aby zilustrować, że łatwiej i bardziej elastycznie jest używać funkcji wyższego rzędu zamiast obiektów, które implementują interfejs.

Sokół
źródło
3
Niezły przykład. Jednak, podobnie jak Gulshan, staram się dowiedzieć więcej na temat programowania funkcjonalnego i zastanawiałem się, czy ten rodzaj „funkcjonalnego DI” nie poświęca pewnej dyscypliny i znaczenia w porównaniu z „obiektowym DI”. Podpis wyższego rzędu stwierdza tylko, że przekazana funkcja musi przyjąć klienta jako parametr i zwrócić bool, podczas gdy wersja OO wymusza fakt, że przekazany obiekt jest filtrem (implementuje IFilter <Klient>). Ujawnia również pojęcie filtru, co może być dobrą rzeczą, jeśli jest to podstawowa koncepcja Domeny (patrz DDD). Co myślisz ?
guillaume31
2
@ ian31: To naprawdę interesujący temat! Wszystko, co jest przekazywane do FilterCustomer, będzie domyślnie zachowywać się jak jakiś filtr. Kiedy koncepcja filtrowania jest istotną częścią domeny i potrzebujesz złożonych reguł filtrowania, które są używane wiele razy w całym systemie, na pewno lepiej je kapsułkować. Jeśli nie lub tylko w bardzo niskim stopniu, wówczas dążyłbym do technicznej prostoty i pragmatyzmu.
Falcon,
5
@ ian31: Całkowicie się nie zgadzam. Wdrażanie IFilter<Customer>nie jest wcale egzekwowaniem. Funkcja wyższego rzędu jest znacznie bardziej elastyczna, co jest dużą zaletą, a możliwość pisania ich w linii to kolejna ogromna korzyść. Lambdy są również znacznie łatwiejsze do przechwytywania zmiennych lokalnych.
DeadMG,
3
@ ian31: Funkcję można również zweryfikować podczas kompilacji. Możesz także napisać funkcję, nazwać ją, a następnie przekazać jako argument, o ile wypełnia oczywistą umowę (bierze klienta, zwraca bool). Niekoniecznie musisz przekazać wyrażenie lambda. Możesz więc w pewnym stopniu zaspokoić ten brak ekspresji. Jednak umowa i jej cel nie są tak jasno wyrażone. Czasami jest to poważna wada. Podsumowując, jest to kwestia ekspresji, języka i enkapsulacji. Myślę, że musisz oceniać każdą sprawę osobno.
Falcon,
2
jeśli czujesz się mocno o wyjaśnienie znaczenia semantycznego wtryskiwanego funkcji można w podpisach nazwisko funkcja C # za pomocą delegatów: public delegate bool CustomerFilter(Customer customer). w czysto funkcjonalnych językach, takich jak haskell, typy aliasingu są banalne:type customerFilter = Customer -> Bool
sara
8

Jeśli chcesz zmienić zachowanie funkcji

doThis(Foo)

możesz przekazać inną funkcję

doThisWith(Foo, anotherFunction)

która implementuje zachowanie, które chcesz być inne.

„doThisWith” jest funkcją wyższego rzędu, ponieważ przyjmuje inną funkcję jako argument.

Na przykład możesz mieć

storeValues(Foo, writeToDatabase)
storeValues(Foo, imitateDatabase)
LennyProgrammers
źródło
5

Krótka odpowiedź:

Wstrzykiwanie / odwracanie kontroli klasycznej wykorzystuje interfejsy klas jako symbole zastępcze dla funkcji zależnych. Ten interfejs jest implementowany przez klasę.

Zamiast interfejsu / implementacji klasy wiele zależności można łatwiej zaimplementować dzięki funkcji delegowania.

Przykład dla obu znajdziesz w c # w ioc-factory-pros-and-contras-for-interface-vers-delegates .

k3b
źródło
0

Porównaj to:

String[] names = {"Fred", "Susan"};
List<String> namesBeginningWithS = new LinkedList<String>();
for (String name : names) {
    if (name.startsWith("S")) {
        namesBeginningWithS.add(name);
    }
}

z:

String[] names = {"Fred", "Susan"};
List<String> namesBeginningWithS = names.stream().filter(n <- n.startsWith("S")).collect();

Druga wersja to sposób, w jaki Java 8 redukuje kod bojlera (zapętlanie itp.) Poprzez udostępnienie funkcji wyższego rzędu, filterktóre pozwalają przekazać absolutne minimum (tj. Zależność do wstrzyknięcia - wyrażenie lambda).

Sridhar Sarnobat
źródło
0

Piggy-backing off of LennyProgrammers example ...

Jedną z rzeczy, których pominięto w innych przykładach, jest to, że można użyć funkcji wyższego rzędu wraz z aplikacją funkcji częściowej (PFA) w celu powiązania (lub „wstrzyknięcia”) zależności do funkcji (poprzez listę argumentów) w celu utworzenia nowej funkcji.

Jeśli zamiast:

doThisWith(Foo, anotherFunction)

my (aby być konwencjonalnym w sposobie, w jaki zazwyczaj wykonuje się PFA), mamy niskopoziomową funkcję roboczą jako (zamiana porządku arg):

doThisWith( anotherFunction, Foo )

Następnie możemy częściowo zastosować doThisW ten sposób:

doThis = doThisWith( anotherFunction )  // note that "Foo" is still missing, argument list is partial

Co pozwala nam później korzystać z nowej funkcji w następujący sposób:

doThis(Foo)

Lub nawet:

doThat = doThisWith( yetAnotherDependencyFunction )
...
doThat( Bar )

Zobacz także: https://ramdajs.com/docs/#partial

... a tak, sumatory / mnożniki są niewyobrażalnymi przykładami. Lepszym przykładem może być funkcja, która pobiera wiadomości i rejestruje je lub wysyła e-mailem w zależności od tego, co przekazała funkcja „konsumenta” jako zależność.

Rozszerzając ten pomysł, jeszcze dłuższe listy argumentów można stopniowo zawęzić do coraz bardziej wyspecjalizowanych funkcji z coraz krótszymi listami argumentów, i oczywiście każdą z tych funkcji można przekazać do innych funkcji jako zależności, które należy częściowo zastosować.

OOP jest fajny, jeśli potrzebujesz pakietu rzeczy z wieloma ściśle powiązanymi operacjami, ale zamienia się on w make-work, aby stworzyć grupę klas, z których każda ma jedną publiczną metodę „zrób to”, a la „Królestwo rzeczowników”.

Roboprog
źródło