FP gegen OOP

Vor nicht allzu langer Zeit erschienen auf dem Hub mehrere Posts, die dem Funktions- und Objektansatz gegenüberstanden, was in den Kommentaren eine hitzige Diskussion darüber erzeugte, was es wirklich ist - objektorientierte Programmierung und wie sie sich von funktional unterscheidet. Ich möchte, wenn auch etwas spät, mit anderen teilen, was Robert Martin, auch bekannt als Onkel Bob, darüber denkt.



In den letzten Jahren war ich wiederholt in der Lage, zusammen mit Leuten zu programmieren, die Functional Programming studierten und die voreingenommen gegenüber OOP waren. Dies wurde normalerweise in Form von Aussagen ausgedrückt wie: "Nun, das ist zu viel wie ein Objekt."


Ich denke, das kommt von der Überzeugung, dass sich FP und OOP gegenseitig ausschließen. Viele scheinen zu denken, dass das Programm, wenn es funktionsfähig ist, nicht objektorientiert ist. Ich glaube, dass die Bildung einer solchen Meinung eine logische Folge des Studiums von etwas Neuem ist.


Wenn wir uns einer neuen Technik annehmen, beginnen wir oft, die alten Techniken, die wir zuvor verwendet haben, zu meiden. Das ist natürlich, weil wir glauben, dass die neue Technik "besser" ist und daher die alte Technik wahrscheinlich "schlechter" ist.


In diesem Beitrag bin ich zu Recht der Ansicht, dass OOP und FP zwar orthogonal sind, sich jedoch nicht gegenseitig ausschließen. Dass ein gutes Funktionsprogramm objektorientiert sein kann (und sollte). Und dass ein gutes objektorientiertes Programm funktionieren kann (und sollte). Dazu müssen wir jedoch die Bedingungen festlegen.


Was ist OOP?


Ich werde das Thema aus einer reduktionistischen Perspektive betrachten. Es gibt viele korrekte Definitionen von OOP, die viele Konzepte, Prinzipien, Techniken, Muster und Philosophien abdecken. Ich habe vor, sie zu ignorieren und mich auf das Salz selbst zu konzentrieren. Hier ist Reduktionismus erforderlich, weil all diese Fülle von Möglichkeiten rund um OOP nicht wirklich etwas Spezifisches für OOP ist. Dies ist nur ein Teil der Fülle von Möglichkeiten, die sich bei der Softwareentwicklung im Allgemeinen bieten. Hier werde ich mich auf den Teil von OOP konzentrieren, der definiert und nicht entfernbar ist.


Schauen Sie sich zwei Ausdrücke an:


1: f (o); 2: von ();


Was ist der Unterschied?


Es gibt eindeutig keinen semantischen Unterschied. Der ganze Unterschied liegt ganz in der Syntax. Aber einer sieht prozedural aus und der andere ist objektorientiert. Dies liegt daran, dass wir daran gewöhnt sind, dass Ausdruck 2 implizit eine spezielle Verhaltenssemantik impliziert, die Ausdruck 1 nicht hat. Diese spezielle Verhaltenssemantik ist Polymorphismus.


Wenn wir Ausdruck 1 sehen, sehen wir die Funktion f , die aufgerufen wird, in die das Objekt o übertragen wird. Dies impliziert, dass es nur eine Funktion mit dem Namen f gibt und nicht die Tatsache, dass sie ein Mitglied der Standardfunktionskohorte ist, die o umgibt.


Wenn wir dagegen Ausdruck 2 sehen, sehen wir ein Objekt mit dem Namen o, an das eine Nachricht mit dem Namen f gesendet wird. Wir erwarten, dass es möglicherweise andere Arten von Objekten gibt, die die Nachricht f empfangen, und daher wissen wir nicht, welches spezifische Verhalten von f nach dem Aufruf zu erwarten ist. Verhalten ist abhängig vom Typ o. das heißt, f ist polymorph.


Diese Tatsache, die wir von polymorphen Verhaltensmethoden erwarten, ist das Wesen der objektorientierten Programmierung. Dies ist eine reduktionistische Definition, und diese Eigenschaft kann nicht aus OOP entfernt werden. OOP ohne Polymorphismus ist kein OOP. Alle anderen OOP-Eigenschaften, wie die Datenkapselung und die mit diesen Daten verknüpften Methoden und sogar die Vererbung, beziehen sich eher auf Ausdruck 1. als auf Ausdruck 2.


Programmierer, die C und Pascal (und in gewissem Maße sogar Fortran und Cobol) verwenden, haben immer Systeme mit gekapselten Funktionen und Strukturen erstellt. Um solche Strukturen zu erstellen, benötigen Sie nicht einmal eine objektorientierte Programmiersprache. Die Verkapselung und sogar die einfache Vererbung in solchen Sprachen ist offensichtlich und natürlich. (In C und Pascal natürlicher als in anderen)


Was OOP-Programme wirklich von Nicht-OOP-Programmen unterscheidet, ist daher Polymorphismus.


Vielleicht möchten Sie argumentieren, dass Polyphorismus einfach durch die Verwendung von inside f switch oder langen if / else-Ketten erreicht werden kann. Das ist wahr, also muss ich eine andere Einschränkung für OOP festlegen.


Die Verwendung von Polymorphismus sollte nicht die Abhängigkeit des Anrufers vom Angerufenen erzeugen.


Um dies zu erklären, schauen wir uns noch einmal die Ausdrücke an. Der Ausdruck 1: f (o) scheint von der Funktion f auf Quellcodeebene abzuhängen. Wir ziehen diese Schlussfolgerung, weil wir auch annehmen, dass f nur eins ist und der Anrufer daher über den Angerufenen Bescheid wissen muss.


Wenn wir uns jedoch Ausdruck 2 von () ansehen, nehmen wir etwas anderes an. Wir wissen, dass es viele Realisierungen von f geben kann und wir wissen nicht, welche dieser Funktionen f tatsächlich aufgerufen wird. Daher ist der Quellcode, der Ausdruck 2 enthält, unabhängig von der Funktion, die auf Quellcodeebene aufgerufen wird.


Dies bedeutet insbesondere, dass Module (Dateien mit Quellcode), die polymorphe Funktionsaufrufe enthalten, nicht auf Module (Dateien mit Quellcode) verweisen sollten, die die Implementierung dieser Funktionen enthalten. Es darf kein Include oder Use oder Require oder andere Schlüsselwörter geben, die einige Quellcodedateien von anderen abhängig machen.


Unsere reduktionistische Definition von OOP lautet also:


Eine Technik, die dynamischen Polymorphismus zum Aufrufen von Funktionen verwendet und keine Abhängigkeiten des Aufrufers von dem auf der Quellcodeebene aufgerufenen Programm erstellt.

Was ist AF?


Und wieder werde ich den reduktionistischen Ansatz verwenden. FP hat eine reiche Tradition und Geschichte, deren Wurzeln tiefer liegen als die Programmierung selbst. Es gibt Prinzipien, Techniken, Theoreme, Philosophien und Konzepte, die dieses Paradigma durchdringen. Ich werde das alles ignorieren und direkt zur Essenz gehen, zu der inhärenten Eigenschaft, die FP von anderen Stilen unterscheidet. Hier ist es:


f (a) == f (b) wenn a == b.


In einem Funktionsprogramm führt der Aufruf einer Funktion mit demselben Argument zu demselben Ergebnis, unabhängig davon, wie lange das Programm ausgeführt wurde. Dies wird manchmal als referentielle Transparenz bezeichnet.


Daraus folgt, dass f die Teile des globalen Zustands, die das Verhalten von f beeinflussen, nicht ändern sollte. Wenn wir außerdem sagen, dass f alle Funktionen im System darstellt - das heißt, alle Funktionen im System müssen referenziell transparent sein -, kann keine Funktion im System den globalen Zustand ändern. Keine Funktion kann etwas tun, das dazu führen kann, dass eine andere Funktion vom System einen anderen Wert mit denselben Argumenten zurückgibt.


Dies hat eine tiefere Konsequenz - kein benannter Wert kann geändert werden. Das heißt, es gibt keinen Zuweisungsoperator.


Wenn Sie diese Aussage sorgfältig überdenken, können Sie zu dem Schluss kommen, dass ein Programm, das nur aus transparent transparenten Funktionen besteht, nichts kann - da jedes nützliche Verhalten des Systems den Zustand von etwas ändert; auch wenn es nur der Zustand des Druckers oder der Anzeige ist. Wenn wir jedoch Eisen von den Anforderungen für referentielle Transparenz und alle Elemente der Welt um uns herum ausschließen, stellt sich heraus, dass wir sehr nützliche Systeme schaffen können.


Der Fokus liegt natürlich auf der Rekursion. Stellen Sie sich eine Funktion vor, die eine Struktur mit state als Argument verwendet. Dieses Argument besteht aus allen Statusinformationen, die eine Funktion benötigt, um zu funktionieren. Wenn die Arbeit beendet ist, erstellt die Funktion eine neue Struktur mit einem Status, dessen Inhalt sich vom vorherigen unterscheidet. Und mit der letzten Aktion ruft sich die Funktion mit einer neuen Struktur als Argument auf.


Dies ist nur einer der einfachen Tricks, mit denen ein Funktionsprogramm Statusänderungen speichern kann, ohne den Status ändern zu müssen [1].


Also die reduktionistische Definition der funktionalen Programmierung:


Referentielle Transparenz - Sie können keine Werte neu zuweisen.

FP gegen OOP


Zu diesem Zeitpunkt schauen mich sowohl Befürworter von OOP als auch Befürworter von FI bereits durch optische Visiere an. Reduktionismus ist nicht der beste Weg, um Freunde zu finden. Aber manchmal ist es nützlich. In diesem Fall halte ich es für nützlich, das unverblasste Anti-OOP-Holivar zu beleuchten.


Es ist klar, dass die beiden von mir gewählten reduktionistischen Definitionen völlig orthogonal sind. Polymorphismus und referentielle Transparenz haben nichts miteinander zu tun. Sie kreuzen sich in keiner Weise.


Orthogonalität bedeutet jedoch keinen gegenseitigen Ausschluss (fragen Sie James Clerk Maxwell). Es ist durchaus möglich, ein System zu erstellen, das sowohl dynamischen Polymorphismus als auch referenzielle Transparenz verwendet. Es ist nicht nur möglich, es ist richtig und gut!


Warum ist diese Kombination gut? Aus genau den gleichen Gründen wie beide Komponenten! Systeme, die auf dynamischem Polymorphismus basieren, sind gut, weil sie eine geringe Konnektivität aufweisen. Abhängigkeiten können invertiert und auf verschiedenen Seiten der Architekturgrenzen platziert werden. Diese Systeme können mit Moki und Fake und anderen Arten von Testdoppeln getestet werden. Module können geändert werden, ohne Änderungen an anderen Modulen vorzunehmen. Daher sind solche Systeme leichter zu modifizieren und zu verbessern.


Systeme, die auf referenzieller Transparenz basieren, sind ebenfalls gut, weil sie vorhersehbar sind. Durch die Unveränderlichkeit des Zustands können solche Systeme leichter verstanden, geändert und verbessert werden. Dies verringert die Wahrscheinlichkeit von Rennen und anderen Multithreading-Problemen erheblich.


Die Hauptidee hier ist folgende:


Es gibt kein Holivar FP gegen OOP

FP und OOP arbeiten gut zusammen. Beide sind gut und in modernen Systemen geeignet. Das System, das auf einer Kombination der Prinzipien von OOP und FP basiert, maximiert Flexibilität, Wartbarkeit, Testbarkeit, Einfachheit und Festigkeit. Wenn Sie eine entfernen, um eine weitere hinzuzufügen, wird die Struktur des Systems nur verschlechtert.


[1] Da wir Maschinen mit Von-Neumann-Architektur verwenden, gehen wir davon aus, dass sie Speicherzellen haben, deren Zustand sich tatsächlich ändert. In dem von mir beschriebenen Rekursionsmechanismus ermöglicht die Optimierung der Schwanzschwanzrekursion nicht die Erstellung neuer Glasrahmen, und der ursprüngliche Glasrahmen wird verwendet. Diese Verletzung der referentiellen Transparenz ist (normalerweise) dem Programmierer verborgen und hat keinerlei Auswirkungen.

Source: https://habr.com/ru/post/de474518/


All Articles