Wie hängt die Abhängigkeitsinversion mit Funktionen höherer Ordnung zusammen?

41

Heute habe ich gerade diesen Artikel gesehen, der die Relevanz des SOLID-Prinzips in F # -Entwicklungs-

F # und Designprinzipien - SOLID

Und während er sich mit dem letzten - "Abhängigkeitsinversionsprinzip" - befasste, sagte der Autor:

Aus funktionaler Sicht können diese Behälter- und Injektionskonzepte mit einer einfachen Funktion höherer Ordnung oder einem Muster vom Typ Hole-in-the-Middle gelöst werden, die direkt in die Sprache integriert sind.

Aber er erklärte es nicht weiter. Meine Frage ist also, in welcher Beziehung die Abhängigkeitsinversion zu Funktionen höherer Ordnung steht.

Gulshan
quelle

Antworten:

38

Abhängigkeitsinversion in OOP bedeutet, dass Sie gegen eine Schnittstelle codieren, die dann von einer Implementierung in einem Objekt bereitgestellt wird.

Sprachen, die Funktionen für höhere Sprachen unterstützen, können oft einfache Abhängigkeitsinversionsprobleme lösen, indem sie das Verhalten als Funktion anstelle eines Objekts übergeben, das eine Schnittstelle im OO-Sinne implementiert.

In solchen Sprachen kann die Signatur der Funktion zur Schnittstelle werden, und eine Funktion wird anstelle eines herkömmlichen Objekts übergeben, um das gewünschte Verhalten zu erzielen. Das Loch im mittleren Muster ist dafür ein gutes Beispiel.

Auf diese Weise erzielen Sie dasselbe Ergebnis mit weniger Code und mehr Ausdruckskraft, da Sie keine ganze Klasse implementieren müssen, die einer OOP-Schnittstelle entspricht, um das gewünschte Verhalten für den Aufrufer bereitzustellen. Stattdessen können Sie einfach eine einfache Funktionsdefinition übergeben. Kurz gesagt: Code ist oft einfacher zu pflegen, aussagekräftiger und flexibler, wenn Funktionen höherer Ordnung verwendet werden.

Ein Beispiel in C #

Traditioneller Ansatz:

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);

Mit Funktionen höherer Ordnung:

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

Jetzt werden die Implementierung und der Aufruf weniger umständlich. Wir müssen keine IFilter-Implementierung mehr bereitstellen. Wir müssen keine Klassen mehr für die Filter implementieren.

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

Dies kann natürlich bereits von LinQ in C # durchgeführt werden. Ich habe dieses Beispiel nur verwendet, um zu veranschaulichen, dass es einfacher und flexibler ist, Funktionen höherer Ordnung anstelle von Objekten zu verwenden, die eine Schnittstelle implementieren.

Falke
quelle
3
Nettes Beispiel. Wie Gulshan versuche ich jedoch, mehr über funktionale Programmierung herauszufinden, und ich habe mich gefragt, ob diese Art von "funktionaler DI" im Vergleich zu "objektorientierter DI" nicht etwas an Strenge und Bedeutung einbüßt. Die höhere Ordnung Unterschrift besagt nur , dass die Funktion einen Kunden als Parameter übergeben muß und ein Bool , während der OO - Version gibt die Tatsache erzwingt , dass das Objekt übergeben ist ein Filter (implementiert IFilter <Customer>). Es macht auch den Begriff des Filters explizit, was eine gute Sache sein könnte, wenn es ein Kernkonzept der Domäne ist (siehe DDD). Was denkst du ?
guillaume31
2
@ ian31: Das ist in der Tat ein interessantes Thema! Alles, was an FilterCustomer übergeben wird, verhält sich implizit wie eine Art Filter. Wenn das Filterkonzept ein wesentlicher Bestandteil der Domäne ist und Sie komplexe Filterregeln benötigen, die systemweit mehrfach verwendet werden, ist es sicher besser, sie zu kapseln. Wenn nicht oder nur in sehr geringem Maße, dann würde ich technische Einfachheit und Pragmatismus anstreben.
Falcon
5
@ ian31: da stimme ich überhaupt nicht zu. Implementierung IFilter<Customer>ist überhaupt keine Durchsetzung. Die Funktion höherer Ordnung ist wesentlich flexibler, was ein großer Vorteil ist, und die Möglichkeit, sie inline zu schreiben, ist ein weiterer großer Vorteil. Lambdas sind auch viel einfacher in der Lage, lokale Variablen zu erfassen.
DeadMG
3
@ ian31: Die Funktion kann auch beim Kompilieren überprüft werden. Sie können auch eine Funktion schreiben, benennen und dann als Argument übergeben, solange sie den offensichtlichen Vertrag erfüllt (nimmt den Kunden, gibt bool zurück). Sie müssen nicht unbedingt einen Lambda-Ausdruck übergeben. So können Sie diesen Mangel an Ausdruckskraft bis zu einem gewissen Grad abdecken. Der Vertrag und seine Absicht sind jedoch nicht so klar ausgedrückt. Das ist manchmal ein großer Nachteil. Alles in allem geht es um Ausdruckskraft, Sprache und Verkapselung. Ich denke, Sie müssen jeden Fall für sich beurteilen.
Falcon
2
wenn Sie stark zu fühlen die semantische Bedeutung eines injizierten Funktion Klärung, können Sie in C # Name Funktion Signaturen Delegierten: public delegate bool CustomerFilter(Customer customer). in reinen funktionalen sprachen wie haskell ist das aliasing von typen trivial:type customerFilter = Customer -> Bool
sara
8

Wenn Sie das Verhalten einer Funktion ändern möchten

doThis(Foo)

Sie könnten eine andere Funktion übergeben

doThisWith(Foo, anotherFunction)

welche implementiert das Verhalten, das Sie anders sein möchten.

"doThisWith" ist eine Funktion höherer Ordnung, da eine andere Funktion als Argument verwendet wird.

Zum Beispiel könntest du haben

storeValues(Foo, writeToDatabase)
storeValues(Foo, imitateDatabase)
LennyProgrammierer
quelle
5

Kurze Antwort:

Classical Dependency Injection / Inversion of Control verwendet eine Klassenschnittstelle als Platzhalter für abhängige Funktionen. Diese Schnittstelle wird von einer Klasse implementiert.

Anstelle von Interface / ClassImplementation können viele Abhängigkeiten mit einer Delegate-Funktion einfacher implementiert werden.

Ein Beispiel für beides finden Sie in c # unter ioc-factory-pros-and-contras-for-interface-versus-delegates .

k3b
quelle
0

Vergleichen Sie dies:

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

mit:

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

Die zweite Version ist die Methode von Java 8 zur Reduzierung von Boilerplate-Code (Schleifen usw.), indem Funktionen höherer Ordnung bereitgestellt werden, mit filterdenen Sie das absolute Minimum (dh die zu injizierende Abhängigkeit - der Lambda-Ausdruck) überschreiten können.

Sridhar Sarnobat
quelle
0

Huckepack von LennyProgrammers Beispiel ...

Eines der Dinge, die in den anderen Beispielen übersehen wurden, ist, dass Sie Funktionen höherer Ordnung zusammen mit Partial Function Application (PFA) verwenden können, um Abhängigkeiten in eine Funktion (über ihre Argumentliste) zu binden (oder "einzufügen"), um eine neue Funktion zu erstellen.

Wenn anstelle von:

doThisWith(Foo, anotherFunction)

Wir (um konventionell zu sein, wie PFA normalerweise gemacht wird) haben die Low-Level-Worker-Funktion als (Austausch der Argumentreihenfolge):

doThisWith( anotherFunction, Foo )

Dann können wir doThisWith teilweise so anwenden :

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

Damit können wir die neue Funktion später wie folgt nutzen:

doThis(Foo)

Oder auch:

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

Siehe auch: https://ramdajs.com/docs/#partial

... und Addierer / Multiplikatoren sind einfallslose Beispiele. Ein besseres Beispiel wäre eine Funktion, die Nachrichten entgegennimmt und sie protokolliert oder per E-Mail versendet, je nachdem, was die "Consumer" -Funktion als Abhängigkeit übergeben hat.

Um diese Idee zu erweitern, können noch längere Argumentlisten schrittweise auf immer spezialisiertere Funktionen mit immer kürzeren Argumentlisten eingegrenzt werden, und natürlich kann jede dieser Funktionen als teilweise anzuwendende Abhängigkeiten an andere Funktionen übergeben werden.

OOP ist nett, wenn Sie ein Bündel von Dingen mit mehreren eng verwandten Operationen benötigen, aber es wird zu einer Arbeit, um eine Reihe von Klassen mit jeweils einer öffentlichen "do it" -Methode zu erstellen, a la "The Kingdom of Nouns".

Roboprog
quelle