Häufige Missverständnisse über OOP

Hallo Habr!

Heute erwartet Sie eine übersetzte Veröffentlichung, die zum Teil unsere Suche nach neuen Büchern über OOP und FI widerspiegelt. Bitte nehmen Sie an der Abstimmung teil.



Ist das OOP-Paradigma tot? Kann man sagen, dass funktionale Programmierung die Zukunft ist? Es scheint, dass viele Artikel darüber schreiben. Ich bin geneigt, diesem Standpunkt nicht zuzustimmen. Lass uns reden!

Alle paar Monate stoße ich auf einen Beitrag in einem Blog, in dem die Autorin scheinbar fundierte Behauptungen zur objektorientierten Programmierung aufstellt. Danach erklärt sie OOP zu einem Relikt der Vergangenheit, und wir müssen alle auf funktionale Programmierung umsteigen.

Zuvor habe ich geschrieben, dass OOP und FI sich nicht widersprechen. Außerdem konnte ich sie sehr erfolgreich kombinieren.

Warum haben die Autoren dieser Artikel so viele Probleme mit OOP und warum scheint ihnen AF eine so offensichtliche Alternative zu sein?

Wie man OOP unterrichtet


Wenn uns OOP beigebracht wird, betonen sie normalerweise, dass es auf vier Prinzipien basiert: Kapselung , Vererbung , Abstraktion , Polymorphismus . Es sind diese vier Prinzipien, die normalerweise in Artikeln kritisiert werden, in denen die Autoren über den Niedergang der PLO argumentieren.

OOP ist jedoch wie FI ein Werkzeug. Probleme lösen. Es kann konsumiert, es kann auch missbraucht werden. Wenn Sie beispielsweise die falsche Abstraktion erstellen, missbrauchen Sie OOP.
Daher sollte die Square Klasse niemals die Rectangle Klasse erben. In mathematischer Hinsicht sind sie natürlich miteinander verbunden. Aus programmtechnischer Sicht stehen sie jedoch nicht in einer Vererbungsbeziehung. Tatsache ist, dass die Anforderungen an ein Quadrat strenger sind als an ein Rechteck. Während in einem Rechteck zwei Paare gleicher Seiten vorhanden sind, müssen bei einem Quadrat alle Seiten gleich sein.

Vererbung


Lassen Sie uns die Vererbung genauer diskutieren. Sie erinnern sich wahrscheinlich an Lehrbuchbeispiele mit schönen Hierarchien geerbter Klassen, und all diese Strukturen lösen das Problem. In der Praxis wird die Vererbung jedoch nicht so häufig verwendet wie die Komposition.
Betrachten Sie ein Beispiel. Angenommen, wir haben eine sehr einfache Klasse, einen Controller in einer Webanwendung. Die meisten modernen Frameworks gehen davon aus, dass Sie so damit arbeiten:

 class BlogController extends FrameworkAbstractController { } 

Es wird davon ausgegangen, dass Sie auf diese Weise leichter Aufrufe wie this.renderTemplate(...) , da solche Methoden von der FrameworkAbstractController Klasse erben.

Wie in vielen Artikeln zu diesem Thema angegeben, treten hier eine Reihe konkreter Probleme auf. Jede interne Funktion in der Basisklasse wird tatsächlich zu einer API. Sie kann sich nicht mehr ändern. Alle geschützten Variablen des Basiscontrollers beziehen sich jetzt mehr oder weniger auf die API.

Es gibt nichts zu verwirren. Und wenn wir den Ansatz mit Kompositions- und Abhängigkeitsinjektion gewählt hätten, wäre er folgendermaßen ausgefallen:

 class BlogController { public BlogController ( TemplateRenderer templateRenderer ) { } } 

Sie sehen, Sie sind nicht mehr auf einen nebligen FrameworkAbstractController angewiesen, sondern auf eine sehr gut definierte und enge Sache, TemplateRenderer . Tatsächlich erbt BlogController kein anderes Controller, da es kein Verhalten erbt.

Kapselung


Das zweite häufig kritisierte Merkmal von OOP ist die Kapselung. In der literarischen Sprache wird die Bedeutung der Kapselung wie folgt formuliert: Daten und Funktionalität werden zusammen geliefert, und der interne Zustand der Klasse ist der Außenwelt verborgen.

Diese Gelegenheit ermöglicht wiederum die Nutzung und den Missbrauch. Das Hauptbeispiel für Missbrauch in diesem Fall ist ein undichter Zustand.

Angenommen, die Klasse List<> enthält eine Liste von Elementen, und diese Liste kann geändert werden. Erstellen Sie eine Klasse, um einen Bestellkorb wie folgt zu verarbeiten:

 class ShoppingCart { private List<ShoppingCartItem> items; public List<ShoppingCartItem> getItems() { return this.items; } } 

In den meisten OOP-orientierten Sprachen geschieht Folgendes: Die Variable items wird als Referenz zurückgegeben. Daher können wir dies weiter tun:

 shoppingCart.getItems().clear(); 

Auf diese Weise löschen wir tatsächlich die Liste der Artikel im Warenkorb, und ShoppingCart weiß nicht einmal davon. Wenn Sie sich dieses Beispiel jedoch genau ansehen, wird deutlich, dass das Problem nicht im Prinzip der Kapselung liegt. Dieses Prinzip wird hier nur verletzt, weil der interne Status aus der ShoppingCart Klasse ShoppingCart .

In diesem speziellen Beispiel könnte der Autor der ShoppingCart Klasse die Unveränderlichkeit verwenden , um das Problem zu umgehen und sicherzustellen, dass das Kapselungsprinzip nicht verletzt wird.

Unerfahrene Programmierer verstoßen häufig auf andere Weise gegen das Prinzip der Kapselung: Sie führen einen Zustand ein, in dem er nicht benötigt wird. Solche unerfahrenen Programmierer verwenden häufig private Klassenvariablen, um Daten von einer Funktion zu einer anderen innerhalb derselben Klasse zu übertragen, während es angemessener wäre, Datenübertragungsobjekte zu verwenden, um eine komplexe Struktur zu einer anderen Funktion zu übertragen. Aufgrund solcher Fehler ist der Code unnötig kompliziert, was zu Fehlern führen kann.

Im Allgemeinen wäre es schön, auf den Zustand insgesamt zu verzichten - veränderbare Daten nach Möglichkeit in Klassen zu speichern. Auf diese Weise müssen Sie eine zuverlässige Kapselung sicherstellen und sicherstellen, dass nirgendwo Lecks auftreten.

Abstraktion


Abstraktion wird wiederum in vielerlei Hinsicht falsch verstanden. In keinem Fall sollten Sie den Code mit abstrakten Klassen füllen und darin tiefe Hierarchien erstellen.

Wenn Sie dies ohne guten Grund tun, suchen Sie nur nach Problemen auf Ihrem eigenen Kopf. Es spielt keine Rolle, wie die Abstraktion durchgeführt wird - als abstrakte Klasse oder als Schnittstelle; In jedem Fall erscheint zusätzliche Komplexität im Code. Diese Komplexität muss begründet werden.
Einfach ausgedrückt, eine Schnittstelle kann nur erstellt werden, wenn Sie bereit sind, Zeit zu verbringen und das Verhalten zu dokumentieren, das von der Klasse erwartet wird, die es implementiert. Ja, du hast mich richtig gelesen. Es reicht nicht aus, nur eine Liste der Funktionen zu erstellen, die Sie implementieren müssen, sondern auch zu beschreiben, wie sie (im Idealfall) funktionieren sollten.

Polymorphismus


Lassen Sie uns abschließend über Polymorphismus sprechen. Er schlägt vor, dass eine Klasse viele Verhaltensweisen implementieren kann. Ein schlechtes Lehrbuchbeispiel ist das Schreiben, dass ein Square mit Polymorphismus entweder ein Rectangle oder ein Parallelogram . Wie ich bereits oben ausgeführt habe, ist dies in der OOP entschieden unmöglich, da das Verhalten dieser Entitäten unterschiedlich ist.

Wenn man von Polymorphismus spricht, sollte man Verhaltensweisen im Auge behalten, nicht Code . Ein gutes Beispiel ist die Soldier Klasse in einem Computerspiel. Es kann sowohl Movable Verhalten (Situation: es kann sich bewegen) als auch Enemy Verhalten (Situation: erschießt dich) implementieren. Im Gegensatz dazu kann die GunEmplacement Klasse nur das Verhalten von GunEmplacement implementieren.

Wenn Sie also Square implements Rectangle, Parallelogram , wird diese Aussage nicht wahr. Ihre Abstraktionen sollten gemäß der Geschäftslogik funktionieren. Sie sollten mehr über Verhalten als über Code nachdenken.

Warum FP keine Silberkugel ist


Wenn wir also die vier Grundprinzipien von OOP wiederholen, überlegen wir uns, was die funktionale Programmierung ausmacht und warum sie nicht verwendet wird, um alle Probleme in Ihrem Code zu lösen.

Aus der Sicht vieler Anhänger von FP sind Klassen Sakrilegien , und der Code sollte in Form von Funktionen dargestellt werden . Je nach Sprache können Daten mithilfe primitiver Typen oder in Form des einen oder anderen strukturierten Datensatzes (Arrays, Wörterbücher usw.) von Funktion zu Funktion übertragen werden.

Darüber hinaus sollten die meisten Funktionen keine Nebenwirkungen haben. Mit anderen Worten, sie sollten Daten nicht an einer unerwarteten Stelle im Hintergrund ändern, sondern nur mit Eingabeparametern arbeiten und eine Ausgabe erzeugen.

Dieser Ansatz trennt die Daten von der Funktion - auf den ersten Blick unterscheidet sich diese FP radikal von OOP. FP betont, dass der Code auf diese Weise einfach bleibt. Wenn Sie etwas tun möchten, schreiben Sie eine Funktion für diesen Zweck - das ist alles.

Probleme beginnen, wenn einige Funktionen auf anderen beruhen müssen. Wenn Funktion A Funktion B aufruft und Funktion B weitere fünf bis sechs Funktionen aufruft und ganz am Ende eine Nullfüllfunktion gefunden wird, die brechen kann - hier werden Sie nicht beneidet.

Die meisten Programmierer, die sich als Befürworter von FP betrachten, lieben FP wegen seiner Einfachheit und betrachten solche Probleme nicht als ernst. Dies ist ziemlich ehrlich, wenn Ihre Aufgabe darin besteht, den Code einfach weiterzugeben und nie wieder darüber nachzudenken. Wenn Sie eine Codebasis erstellen möchten, die für die Unterstützung geeignet ist, ist es besser, die Prinzipien des reinen Codes einzuhalten , insbesondere die Abhängigkeitsinversion anzuwenden, bei der das FI in der Praxis auch viel komplizierter wird.

OOP oder FP?


OOP und FI sind Werkzeuge . Letztendlich spielt es keine Rolle, welches Programmierparadigma Sie verwenden. Die in den meisten Artikeln zu diesem Thema beschriebenen Probleme beziehen sich auf die Codeorganisation.

Meiner Meinung nach ist die Makrostruktur der Anwendung viel wichtiger. Was sind die Module darin? Wie tauschen sie Informationen miteinander aus? Welche Datenstrukturen sind bei Ihnen am häufigsten? Wie sind sie dokumentiert? Welche Objekte sind für die Geschäftslogik am wichtigsten?

All diese Probleme hängen in keiner Weise mit dem verwendeten Programmierparadigma zusammen, auf der Ebene eines solchen Paradigmas können sie nicht einmal gelöst werden. Ein guter Programmierer studiert das Paradigma, um die angebotenen Werkzeuge zu beherrschen, und wählt dann aus, welche zur Lösung der Aufgabe am besten geeignet sind.

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


All Articles