it-swarm.com.de

Ist die funktionale Programmierung eine praktikable Alternative zu Abhängigkeitsinjektionsmustern?

Ich habe kürzlich ein Buch mit dem Titel Functional Programming in C # gelesen und es fällt mir ein, dass die unveränderliche und zustandslose Natur der funktionalen Programmierung ähnliche Ergebnisse wie Abhängigkeitsinjektionsmuster erzielt und möglicherweise sogar ein besserer Ansatz ist, insbesondere in in Bezug auf Unit-Tests.

Ich wäre dankbar, wenn jeder, der Erfahrung mit beiden Ansätzen hat, seine Gedanken und Erfahrungen teilen könnte, um die Hauptfrage zu beantworten: Ist die funktionale Programmierung eine praktikable Alternative zu Abhängigkeitsinjektionsmustern?

23
Matt Cashatt

Das Abhängigkeitsmanagement ist ein großes Problem in OOP aus den folgenden zwei Gründen:

  • Die enge Kopplung von Daten und Code.
  • Allgegenwärtige Verwendung von Nebenwirkungen.

Die meisten OO Programmierer betrachten die enge Kopplung von Daten und Code als durchaus vorteilhaft, sind jedoch mit Kosten verbunden. Die Verwaltung des Datenflusses durch die Ebenen ist ein unvermeidbarer Bestandteil der Programmierung in jedem Paradigma. Das Koppeln Ihrer Daten und Ihres Codes fügt das zusätzliche Problem hinzu, dass Sie, wenn Sie an einem bestimmten Punkt eine Funktion verwenden möchten, einen Weg finden müssen, um das Objekt an diesen Punkt zu bringen.

Die Verwendung von Nebenwirkungen führt zu ähnlichen Schwierigkeiten. Wenn Sie für einige Funktionen einen Nebeneffekt verwenden, aber dessen Implementierung austauschen möchten, haben Sie so gut wie keine andere Wahl, als diese Abhängigkeit einzufügen.

Stellen Sie sich als Beispiel ein Spammerprogramm vor, das Webseiten nach E-Mail-Adressen durchsucht und diese dann per E-Mail versendet. Wenn Sie eine DI-Denkweise haben, denken Sie gerade an die Dienste, die Sie hinter Schnittstellen einkapseln, und welche Dienste wo injiziert werden. Ich werde dieses Design als Übung für den Leser belassen. Wenn Sie eine FP Denkweise) haben, denken Sie gerade an die Ein- und Ausgänge für die unterste Funktionsebene, wie zum Beispiel:

  • Geben Sie eine Webseitenadresse ein und geben Sie den Text dieser Seite aus.
  • Geben Sie den Text einer Seite ein und geben Sie eine Liste der Links von dieser Seite aus.
  • Geben Sie den Text einer Seite ein und geben Sie eine Liste der E-Mail-Adressen auf dieser Seite aus.
  • Geben Sie eine Liste der E-Mail-Adressen ein und geben Sie eine Liste der E-Mail-Adressen mit entfernten Duplikaten aus.
  • Geben Sie eine E-Mail-Adresse ein und geben Sie eine Spam-E-Mail für diese Adresse aus.
  • Geben Sie eine Spam-E-Mail ein und geben Sie die SMTP-Befehle aus, um diese E-Mail zu senden.

Wenn Sie in Ein- und Ausgängen denken, gibt es keine Funktionsabhängigkeiten, sondern nur Datenabhängigkeiten. Das macht sie so einfach zu testen. Ihre nächste Ebene sorgt dafür, dass die Ausgabe einer Funktion in die Eingabe der nächsten eingespeist wird, und kann die verschiedenen Implementierungen nach Bedarf problemlos austauschen.

In einem sehr realen Sinne veranlasst die funktionale Programmierung Sie natürlich, Ihre Funktionsabhängigkeiten immer umzukehren, und daher müssen Sie normalerweise keine besonderen Maßnahmen ergreifen, um dies nachträglich zu tun. Wenn Sie dies tun, erleichtern Werkzeuge wie Funktionen höherer Ordnung, Verschlüsse und Teilanwendungen die Ausführung mit weniger Boilerplate.

Beachten Sie, dass es nicht die Abhängigkeiten selbst sind, die problematisch sind. Es sind Abhängigkeiten, die in die falsche Richtung weisen. Die nächste Ebene kann eine Funktion haben wie:

processText = spamToSMTP . emailAddressToSpam . removeEmailDups . textToEmailAddresses

Es ist vollkommen in Ordnung, wenn diese Ebene Abhängigkeiten wie diese fest codiert hat, da ihr einziger Zweck darin besteht, die Funktionen der unteren Ebene zusammenzukleben. Das Austauschen einer Implementierung ist so einfach wie das Erstellen einer anderen Komposition:

processTextFancy = spamToSMTP . emailAddressToFancySpam . removeEmailDups . textToEmailAddresses

Diese einfache Neuzusammensetzung wird durch fehlende Nebenwirkungen ermöglicht. Die Funktionen der unteren Schicht sind völlig unabhängig voneinander. Die nächste Ebene kann basierend auf einer Benutzerkonfiguration auswählen, welches processText tatsächlich verwendet wird:

actuallyUsedProcessText = if (config == "Fancy") then processTextFancy else processText

Auch dies ist kein Problem, da alle Abhängigkeiten in eine Richtung weisen. Wir müssen einige Abhängigkeiten nicht invertieren, damit sie alle in die gleiche Richtung weisen, da uns reine Funktionen bereits dazu gezwungen haben.

Beachten Sie, dass Sie dies viel besser koppeln können, indem Sie config an die unterste Ebene weiterleiten, anstatt sie oben zu überprüfen. FP hindert Sie nicht daran, dies zu tun, aber es macht es viel ärgerlicher, wenn Sie es versuchen.

29
Karl Bielefeldt

ist die funktionale Programmierung eine praktikable Alternative zu Abhängigkeitsinjektionsmustern?

Das kommt mir seltsam vor. Funktionale Programmieransätze sind weitgehend tangential zur Abhängigkeitsinjektion.

Sicher, ein unveränderlicher Zustand kann Sie dazu bringen, nicht zu "schummeln", indem Sie Nebenwirkungen haben oder den Klassenstatus als impliziten Vertrag zwischen Funktionen verwenden. Dadurch wird die Weitergabe von Daten expliziter, was meiner Meinung nach die grundlegendste Form der Abhängigkeitsinjektion ist. Und das funktionale Programmierkonzept der Weitergabe von Funktionen erleichtert dies erheblich.

Abhängigkeiten werden jedoch nicht entfernt. Ihre Operationen benötigen immer noch alle Daten/Operationen, die sie benötigt haben, als Ihr Status veränderlich war. Und Sie müssen diese Abhängigkeiten immer noch irgendwie dort bekommen. Ich würde also nicht sagen, dass funktionale Programmieransätze ersetzen DI überhaupt sind, also keine Alternative.

Wenn überhaupt, haben sie Ihnen gerade gezeigt, wie schlecht OO Code implizite Abhängigkeiten erzeugen kann, an die Programmierer selten denken.

8
Telastyn

Die schnelle Antwort auf Ihre Frage lautet: Nein .

Aber wie andere behauptet haben, verbindet die Frage zwei, etwas unabhängige Konzepte.

Lassen Sie uns dies Schritt für Schritt tun.

DI führt zu einem nicht funktionalen Stil

Im Kern der Funktionsprogrammierung stehen reine Funktionen - Funktionen, die die Eingabe der Ausgabe zuordnen, sodass Sie für eine bestimmte Eingabe immer die gleiche Ausgabe erhalten.

DI normalerweise bedeutet, dass Ihr Gerät nicht mehr rein ist, da die Leistung je nach Einspritzung variieren kann. Zum Beispiel in der folgenden Funktion:

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

getBookedSeatCount (eine Funktion) kann variieren und zu unterschiedlichen Ergebnissen für dieselbe gegebene Eingabe führen. Dies macht bookSeats ebenfalls unrein.

Hierfür gibt es Ausnahmen: Sie können einen von zwei Sortieralgorithmen einfügen, die dieselbe Eingabe-Ausgabe-Zuordnung implementieren, obwohl unterschiedliche Algorithmen verwendet werden. Dies sind jedoch Ausnahmen.

Ein System kann nicht rein sein

Die Tatsache, dass ein System nicht rein sein kann, wird ebenso ignoriert, wie es in funktionalen Programmierquellen behauptet wird.

Ein System muss Nebenwirkungen haben. Die offensichtlichen Beispiele sind:

  • Benutzeroberfläche
  • Datenbank
  • API (in Client-Server-Architektur)

Ein Teil Ihres Systems muss also Nebenwirkungen beinhalten, und dieser Teil kann durchaus auch einen imperativen Stil oder OO Stil) beinhalten.

Das Shell-Core-Paradigma

In Anlehnung an Gary Bernhardts hervorragender Vortrag über Grenzen umfasst eine gute System oder Modul-) Architektur diese beiden Ebenen:

Der Schlüssel zum Erfolg besteht darin, das System in seinen reinen Teil (den Kern) und den unreinen Teil (die Hülle) aufzuteilen.

Obwohl --- (dieser Artikel von Mark Seemann eine leicht fehlerhafte Lösung (und Schlussfolgerung) bietet, schlägt er das gleiche Konzept vor. Die Haskell-Implementierung ist besonders aufschlussreich, da sie zeigt, dass alles mit FP durchgeführt werden kann.

DI und FP

Der Einsatz von DI ist durchaus sinnvoll, auch wenn der Großteil Ihrer Bewerbung rein ist. Der Schlüssel besteht darin, den DI innerhalb der unreinen Schale einzuschränken.

Ein Beispiel sind API-Stubs - Sie möchten die echte API in der Produktion, verwenden aber Stubs zum Testen. Die Einhaltung des Shell-Core-Modells wird hier sehr hilfreich sein.

Fazit

Also FP und DI sind nicht gerade Alternativen. Sie haben wahrscheinlich beides in Ihrem System, und der Rat ist, die Trennung zwischen dem reinen und dem unreinen Teil des Systems sicherzustellen, wobei FP und DI befinden sich jeweils.

6
Izhaki

Aus der Sicht von OOP) können Funktionen als Schnittstellen mit einer Methode betrachtet werden.

Schnittstelle ist ein stärkerer Vertrag als eine Funktion.

Wenn Sie einen funktionalen Ansatz verwenden und viel DI ausführen, erhalten Sie im Vergleich zur Verwendung eines OOP -Ansatzes) mehr Kandidaten für jede Abhängigkeit.

void DoStuff(Func<DateTime> getDateTime) {}; //Anything that satisfies the signature can be injected.

vs.

void DoStuff(IDateTimeProvider dateTimeProvider) {}; //Only types implementing the interface can be injected.
1
Den