Ist funktionale Programmierung eine praktikable Alternative zu Abhängigkeitsinjektionsmustern?

21

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

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

Matt Cashatt
quelle
10
Das macht für mich wenig Sinn, Unveränderlichkeit entfernt keine Abhängigkeiten.
Telastyn,
Ich bin damit einverstanden, dass Abhängigkeiten nicht entfernt werden. Es ist wahrscheinlich mein Verständnis, das falsch ist, aber ich habe diesen Schluss gezogen, denn wenn ich das ursprüngliche Objekt nicht ändern kann, muss ich es an eine Funktion weitergeben (injizieren), die es verwendet.
Matt Cashatt
5
Es gibt auch, wie man OO-Programmierer dazu bringt, funktionale Programmierung zu lieben. Dies ist eine detaillierte Analyse von DI sowohl aus der OO- als auch aus der FP-Perspektive.
Robert Harvey
1
Diese Frage, die Artikel, auf die verwiesen wird , und die akzeptierte Antwort können auch nützlich sein: stackoverflow.com/questions/11276319/… Ignorieren Sie das beängstigende Monadenwort . Wie Runar in seiner Antwort betont, handelt es sich in diesem Fall nicht um ein komplexes Konzept (nur um eine Funktion).
Itsbruce

Antworten:

27

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

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

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

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

Betrachten Sie als Beispiel ein Spammerprogramm, das Webseiten nach E-Mail-Adressen durchsucht und diese dann per E-Mail versendet. Wenn Sie eine DI-Denkweise haben, denken Sie gerade darüber nach, welche Dienste Sie hinter Schnittstellen einkapseln und welche Dienste wo eingebunden werden. Ich lasse dieses Design als Übung für den Leser. Wenn Sie eine FP-Denkweise haben, denken Sie gerade an die Ein- und Ausgänge für die unterste Funktionsebene, wie z.

  • 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 von E-Mail-Adressen ein, und geben Sie eine Liste von 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 zum Unit-Test. Ihre nächste Schicht sorgt dafür, dass die Ausgabe einer Funktion in die Eingabe der nächsten eingespeist wird, und kann die verschiedenen Implementierungen bei Bedarf problemlos austauschen.

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

Beachten Sie, dass nicht die Abhängigkeiten selbst 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, dass diese Ebene Abhängigkeiten hat, die wie folgt fest codiert sind, 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

Möglich wird diese leichte Rekomposition durch fehlende Nebenwirkungen. Die Funktionen der unteren Schicht sind völlig unabhängig voneinander. Die nächsthöhere Ebene kann processTextanhand einiger Benutzerkonfigurationen auswählen, welche 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 gekoppelter gestalten können, indem configSie zur untersten Ebene durchgehen , anstatt sie oben zu markieren. FP hindert Sie nicht daran, aber es macht Sie viel ärgerlicher, wenn Sie es versuchen.

Karl Bielefeldt
quelle
3
"Die Verwendung von Nebenwirkungen führt zu ähnlichen Schwierigkeiten. Wenn Sie einen Nebeneffekt für bestimmte Funktionen verwenden, aber dessen Implementierung austauschen möchten, haben Sie so gut wie keine andere Wahl, als diese Abhängigkeit einzuschleusen." Ich glaube nicht, dass Nebenwirkungen damit zu tun haben. Wenn Sie Implementierungen in Haskell austauschen möchten, müssen Sie noch eine Abhängigkeitsinjektion durchführen . Wenn Sie die Typklassen desugarieren, übergeben Sie eine Schnittstelle als erstes Argument für jede Funktion.
Doval
2
Der springende Punkt ist, dass Sie in fast jeder Sprache gezwungen sind, Verweise auf andere Codemodule fest zu codieren. Der einzige Weg, um Implementierungen auszutauschen, besteht darin, den dynamischen Versand überall zu verwenden, und dann müssen Sie Ihre Abhängigkeiten zur Laufzeit auflösen. In einem Modulsystem können Sie das Abhängigkeitsdiagramm zur Zeit der Typprüfung ausdrücken.
Doval
@Doval - Vielen Dank für Ihre interessanten und zum Nachdenken anregenden Kommentare. Ich habe Sie vielleicht missverstanden, aber bin ich richtig, wenn ich aus Ihren Kommentaren schlussfolgere, dass ich, wenn ich einen funktionalen Programmierstil über einen DI-Stil (im traditionellen C # -Sinn) verwende, mögliche Debugging-Frustrationen vermeiden würde, die mit der Laufzeit verbunden sind Auflösung von Abhängigkeiten?
Matt Cashatt
@MatthewPatrickCashatt Es geht nicht um Stil oder Paradigma, sondern um Sprachfunktionen. Wenn die Sprache Module nicht als erstklassige Dinge unterstützt, müssen Sie eine dynamische Verteilung und Abhängigkeitsinjektion durchführen, um Implementierungen auszutauschen, da es keine Möglichkeit gibt, die Abhängigkeiten statisch auszudrücken. Anders ausgedrückt: Wenn Ihr C # -Programm Zeichenfolgen verwendet, besteht eine fest programmierte Abhängigkeit von System.String. In einem Modulsystem können Sie durch System.Stringeine Variable ersetzen , sodass die Auswahl der Zeichenfolgenimplementierung nicht fest codiert ist, sondern beim Kompilieren noch aufgelöst wird.
Doval
8

Ist funktionale Programmierung eine praktikable Alternative zu Abhängigkeitsinjektionsmustern?

Das scheint mir eine merkwürdige Frage zu sein. Ansätze der funktionalen Programmierung sind weitgehend tangential zur Abhängigkeitsinjektion.

Sicher, ein unveränderlicher Zustand kann dazu führen, dass Sie nicht "betrügen", indem Sie Nebenwirkungen haben oder den Klassenzustand als impliziten Vertrag zwischen Funktionen verwenden. Es macht die Weitergabe von Daten expliziter, was meiner Meinung nach die grundlegendste Form der Abhängigkeitsinjektion ist. Und das funktionale Programmierkonzept des Weitergebens von Funktionen erleichtert dies erheblich.

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

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

Telastyn
quelle
Nochmals vielen Dank für Ihren Beitrag, Telastyn. Wie Sie bereits betont haben, ist meine Frage nicht sehr gut aufgebaut (meine Worte), aber dank des Feedbacks hier verstehe ich allmählich ein bisschen besser, was in meinem Gehirn an all dem Funken macht: Wir sind uns alle einig (Ich denke), dass Unit-Tests ein Albtraum ohne DI sein können. Leider kann die Verwendung von DI, insbesondere bei IoC-Containern, eine neue Form des Debuggens von Alptraum verursachen, da Abhängigkeiten zur Laufzeit aufgelöst werden. Ähnlich wie DI erleichtert FP das Testen von Einheiten, jedoch ohne Probleme mit der Laufzeitabhängigkeit.
Matt Cashatt
(Fortsetzung von oben). . Das ist sowieso mein gegenwärtiges Verständnis. Bitte lassen Sie mich wissen, wenn ich die Marke verpasse. Es macht mir nichts aus, zuzugeben, dass ich hier nur ein Sterblicher unter Riesen bin!
Matt Cashatt
@MatthewPatrickCashatt - DI impliziert nicht unbedingt Laufzeitabhängigkeitsprobleme, die, wie Sie bemerken, schrecklich sind.
Telastyn
7

Die schnelle Antwort auf Ihre Frage lautet: Nein .

Aber wie andere behauptet haben, verbindet die Frage zwei Begriffe, die in keiner Beziehung zueinander stehen.

Lassen Sie uns dies Schritt für Schritt tun.

DI führt zu einem nicht funktionalen Stil

Der Kern der Funktionsprogrammierung besteht aus reinen Funktionen - Funktionen, die Eingaben auf Ausgaben abbilden, sodass Sie für eine bestimmte Eingabe immer die gleiche Ausgabe erhalten.

DI bedeutet normalerweise, 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 die gleiche Eingabe führen. Dies macht auch bookSeatsunrein.

Es gibt Ausnahmen dafür - Sie können einen von zwei Sortieralgorithmen einfügen, die dasselbe Eingabe-Ausgabe-Mapping implementieren, obwohl sie unterschiedliche Algorithmen verwenden. Aber das sind 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 einen OO-Stil beinhalten.

Das Shell-Core-Paradigma

Eine gute System- (oder Modul-) Architektur, die die Begriffe aus Gary Bernhardts hervorragendem Vortrag über Grenzen übernimmt, umfasst die folgenden zwei Ebenen:

  • Ader
    • Reine Funktionen
    • Verzweigung
    • Keine Abhängigkeiten
  • Schale
    • Unrein (Nebenwirkungen)
    • Keine Verzweigung
    • Abhängigkeiten
    • Kann zwingend sein, OO-Stil beinhalten, etc.

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

Obwohl dieser Artikel eine etwas 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

Die Verwendung von DI ist durchaus sinnvoll, auch wenn der Großteil Ihrer Anwendung rein ist. Der Schlüssel besteht darin, den DI in der unreinen Hülle zu begrenzen.

Ein Beispiel sind API-Stubs - Sie möchten die echte API in der Produktion, verwenden jedoch Stubs beim Testen. Das Festhalten am Shell-Core-Modell wird hier sehr hilfreich sein.

Fazit

FP und DI sind also keine Alternativen. Sie haben wahrscheinlich beides in Ihrem System, und es wird empfohlen, die Trennung zwischen dem reinen und dem unreinen Teil des Systems sicherzustellen, in dem sich FP und DI befinden.

Izhaki
quelle
Wenn Sie sich auf das Shell-Core-Paradigma beziehen, wie würde man keine Verzweigung in der Shell erreichen? Ich kann mir viele Beispiele vorstellen, bei denen eine Anwendung die eine oder andere unreine Sache basierend auf einem Wert ausführen müsste. Ist diese Verzweigungsfreiheit in Sprachen wie Java anwendbar?
Jrahhali
@ jrahhali Weitere Informationen finden Sie in Gary Bernhardts Vortrag (in der Antwort verlinkt).
Izhaki
eine andere relavente Seemann-Serie blog.ploeh.dk/2017/01/27/…
jk.
1

Aus der Sicht von OOP können Funktionen als Einzelmethodenschnittstellen 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 zu einem OOP-Ansatz 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.
Den
quelle
3
Es kann jede Klasse eingeschlossen werden, um die Schnittstelle zu implementieren, sodass der "stärkere Vertrag" nicht viel stärker ist. Noch wichtiger ist, dass es unmöglich ist, jeder Funktion einen anderen Typ zuzuweisen, wenn eine Grenzlinie festgelegt wird.
Doval
Funktionales Programmieren bedeutet nicht "Programmieren mit Funktionen höherer Ordnung", es bezieht sich auf ein weitaus breiteres Konzept, Funktionen höherer Ordnung sind nur eine Technik, die im Paradigma nützlich ist.
Jimmy Hoffa