Warum ist die „enge Kopplung zwischen Funktionen und Daten“ schlecht?

38

Ich fand dieses Zitat in " Die Freude an Clojure " auf S. 32, aber jemand hat mir letzte Woche beim Abendessen dasselbe gesagt und ich habe es auch an anderen Orten gehört:

Ein Nachteil der objektorientierten Programmierung ist die enge Kopplung zwischen Funktion und Daten.

Ich verstehe, warum unnötige Kopplung in einer Anwendung schlecht ist. Ich sage auch gerne, dass veränderlicher Zustand und Vererbung vermieden werden sollten, auch in der objektorientierten Programmierung. Aber ich verstehe nicht, warum das Festhalten an Klassenfunktionen von Natur aus schlecht ist.

Ich meine, das Hinzufügen einer Funktion zu einer Klasse scheint so, als würde man eine Mail in Google Mail markieren oder eine Datei in einen Ordner stecken. Es ist eine organisatorische Technik, die Ihnen hilft, es wiederzufinden. Sie wählen einige Kriterien aus und fügen sie dann zusammen. Vor OOP waren unsere Programme ziemlich viele Methoden in Dateien. Ich meine, du musst Funktionen irgendwo platzieren. Warum nicht organisieren?

Wenn dies ein verschleierter Angriff auf Typen ist, warum sagen sie dann nicht einfach, dass es falsch ist, die Art der Eingabe und Ausgabe auf eine Funktion zu beschränken? Ich bin mir nicht sicher, ob ich dem zustimmen könnte, aber zumindest kenne ich die Argumente für und gegen die Sicherheit. Das klingt für mich nach einem größtenteils separaten Problem.

Klar, manchmal verstehen die Leute es falsch und ordnen Funktionalität der falschen Klasse zu. Im Vergleich zu anderen Fehlern scheint dies jedoch eine sehr geringfügige Unannehmlichkeit zu sein.

Clojure hat also Namespaces. Wie unterscheidet sich das Festhalten einer Funktion an einer Klasse in OOP vom Festhalten einer Funktion in einem Namespace in Clojure und warum ist es so schlecht? Denken Sie daran, dass Funktionen in einer Klasse nicht unbedingt nur für Mitglieder dieser Klasse ausgeführt werden. Schauen Sie sich java.lang.StringBuilder an - es funktioniert auf jedem Referenztyp oder durch Auto-Boxing auf jedem Typ.

PS Dieses Zitat bezieht sich auf ein Buch, das ich nicht gelesen habe: Multiparadigmenprogrammierung in Leda: Timothy Budd, 1995 .

GlenPeterson
quelle
20
Ich glaube, der Autor hat OOP einfach nicht richtig verstanden und brauchte nur einen weiteren Grund, um zu sagen, Java ist schlecht und Clojure ist gut. / rant
Euphoric
6
Instanzmethoden (im Gegensatz zu freien Funktionen oder Erweiterungsmethoden) können nicht aus anderen Modulen hinzugefügt werden. Dies stellt eine größere Einschränkung dar, wenn Sie Schnittstellen berücksichtigen, die nur von den Instanzmethoden implementiert werden können. Sie können kein Interface und keine Klasse in verschiedenen Modulen definieren und dann den Code eines dritten Moduls verwenden, um sie miteinander zu verbinden. Ein flexiblerer Ansatz wie die Typklassen von haskell sollte dies ermöglichen.
CodesInChaos
4
@Euphoric Ich glaube, der Autor hat es verstanden, aber die Clojure-Community scheint es zu mögen, einen Strohmann aus OOP zu machen und es als Abbild für alle Übel der Programmierung zu verbrennen, bevor wir eine gute Speicherbereinigung, viel Speicher, schnelle Prozessoren und viel Speicherplatz. Ich wünschte, sie würden aufhören, sich mit OOP zu befassen, und die wahren Ursachen ins Visier nehmen: Zum Beispiel die Architektur von Neuman.
GlenPeterson
4
Mein Eindruck ist, dass die meiste Kritik an OOP tatsächlich Kritik an OOP ist, wie es in Java implementiert ist. Nicht weil das ein bewusster Strohmann ist, sondern weil es das ist, was sie mit OOP assoziieren. Es gibt ziemlich ähnliche Probleme mit Leuten, die sich über statisches Tippen beschweren. Die meisten Probleme sind nicht mit dem Konzept verbunden, sondern weisen nur Mängel bei der Umsetzung dieses Konzepts auf.
CodesInChaos
3
Ihr Titel entspricht nicht dem Text Ihrer Frage. Es ist leicht zu erklären, warum eine enge Kopplung von Funktionen und Daten schlecht ist, aber in Ihrem Text werden die Fragen "Tut OOP dies?", "Wenn ja, warum?" Gestellt. und "Ist das eine schlechte Sache?". Bisher hatten Sie das Glück, Antworten zu erhalten, die sich mit einer oder mehreren dieser drei Fragen befassen, und keine, die die einfachere Frage im Titel voraussetzt.
Itsbruce

Antworten:

34

Theoretisch erleichtert die lose Funktions-Daten-Kopplung das Hinzufügen weiterer Funktionen, um mit denselben Daten zu arbeiten. Der Nachteil ist, dass es schwieriger ist, die Datenstruktur selbst zu ändern, weshalb in der Praxis gut gestalteter Funktionscode und gut gestalteter OOP-Code sehr ähnliche Kopplungsgrade aufweisen.

Nehmen Sie einen gerichteten azyklischen Graphen (DAG) als Beispiel für eine Datenstruktur. Bei der funktionalen Programmierung müssen Sie immer noch abstrahieren, um Wiederholungen zu vermeiden. Daher erstellen Sie ein Modul mit Funktionen zum Hinzufügen und Löschen von Knoten und Kanten, zum Auffinden von Knoten, die von einem bestimmten Knoten aus erreichbar sind, zum Erstellen einer topologischen Sortierung usw. Diese Funktionen sind effektiv eng mit den Daten verbunden, auch wenn der Compiler sie nicht erzwingt. Sie können einen Knoten auf die harte Tour hinzufügen, aber warum möchten Sie das? Kohäsivität innerhalb eines Moduls verhindert eine enge Kopplung im gesamten System.

Umgekehrt werden auf der OOP-Seite alle anderen Funktionen als die grundlegenden DAG-Operationen in separaten "Ansichts" -Klassen ausgeführt, wobei das DAG-Objekt als Parameter übergeben wird. Es ist genauso einfach, so viele Ansichten hinzuzufügen, wie Sie möchten, um die DAG-Daten zu verarbeiten. Dadurch wird die gleiche Ebene der Entkopplung von Funktionsdaten wie im Funktionsprogramm erreicht. Der Compiler wird Sie nicht davon abhalten, alles in eine Klasse zu packen, aber Ihre Kollegen werden es tun.

Das Ändern von Programmierparadigmen ändert nicht die bewährten Methoden für Abstraktion, Zusammenhalt und Kopplung, sondern nur die Methoden, mit deren Hilfe Sie den Compiler durchsetzen können. Wenn Sie in der funktionalen Programmierung eine Funktions-Daten-Kopplung wünschen, wird dies eher durch die Zustimmung von Gentlemen's als durch den Compiler erzwungen. In OOP wird die Trennung zwischen Modell und Ansicht eher durch die Zustimmung von Gentlemen's als durch den Compiler erzwungen.

Karl Bielefeldt
quelle
13

Falls Sie es noch nicht wussten, nehmen Sie diese Einsicht: Die Konzepte von objektorientiert und Verschlüssen sind zwei Seiten derselben Medaille. Das heißt, was ist eine Schließung? Es nimmt Variablen oder Daten aus dem umgebenden Bereich und bindet sie innerhalb der Funktion an, oder aus einer OO-Perspektive tun Sie genau dasselbe, wenn Sie beispielsweise etwas an einen Konstruktor übergeben, damit Sie dies später verwenden können Daten in einer Mitgliedsfunktion dieser Instanz. Es ist jedoch nicht ratsam, Dinge aus dem Umfeld zu entnehmen - je größer das Umfeld, desto schlimmer ist es, dies zu tun (obwohl pragmatisch gesehen, ist oft etwas Schlimmes erforderlich, um die Arbeit zu erledigen). Die Verwendung globaler Variablen bringt dies auf das Äußerste, wo Funktionen in einem Programm Variablen im Programmumfang verwenden - wirklich, wirklich böse. Es gibtGute Beschreibungen darüber, warum globale Variablen böse sind.

Wenn Sie OO-Techniken befolgen, akzeptieren Sie grundsätzlich bereits, dass jedes Modul in Ihrem Programm ein bestimmtes Mindestmaß an Übel aufweist. Wenn Sie einen funktionalen Ansatz für die Programmierung wählen, streben Sie ein Ideal an, bei dem kein Modul in Ihrem Programm das Übel des Abschlusses enthält, obwohl Sie vielleicht noch einige haben, aber es wird viel weniger als OO sein.

Das ist der Nachteil von OO - es ermutigt diese Art von Übel, die Kopplung von Daten an Funktionen durch die Standardisierung von Closures (eine Art Theorie der Programmierung mit zerbrochenen Fenstern ).

Die einzige positive Seite ist, dass, wenn Sie wüssten, dass Sie am Anfang viele Closures verwenden würden, OO Ihnen zumindest einen idealen logischen Rahmen bietet, um diesen Ansatz so zu organisieren, dass der durchschnittliche Programmierer ihn verstehen kann. Insbesondere werden die Variablen, über die geschlossen wird, im Konstruktor explizit und nicht nur implizit in einem Funktionsschluss verwendet. Funktionsprogramme, die viele Verschlüsse verwenden, sind oft kryptischer als das entsprechende OO-Programm, obwohl sie nicht unbedingt weniger elegant sind :)

Benedikt
quelle
8
Zitat des Tages: "
Etwas
5
Sie haben nicht wirklich erklärt, warum die Dinge, die Sie als böse bezeichnen, böse sind. Du nennst sie nur böse. Erklären Sie, warum sie böse sind, und vielleicht haben Sie eine Antwort auf die Frage des Herrn.
Robert Harvey
2
Ihr letzter Absatz speichert die Antwort jedoch. Es mag Ihrer Meinung nach die einzige positive Seite sein, aber das ist keine Kleinigkeit. Wir sogenannten "durchschnittlichen Programmierer" begrüßen tatsächlich eine bestimmte Anzahl von Zeremonien, die ausreichen, um uns mitzuteilen, was zum Teufel los ist.
Robert Harvey
Wenn OO und Closures synonym sind, warum haben so viele OO-Sprachen keine explizite Unterstützung für sie bereitgestellt? Die von Ihnen angegebene C2-Wiki-Seite hat noch mehr Disputation (und weniger Konsens) als für diese Site üblich.
Itsbruce
1
@itsbruce Sie werden weitgehend unnötig gemacht. Die Variablen, die "geschlossen" werden, werden stattdessen zu Klassenvariablen, die an das Objekt übergeben werden.
Izkata
7

Es geht um Typ - Kopplung:

Eine in ein Objekt eingebaute Funktion zur Bearbeitung dieses Objekts kann nicht für andere Objekttypen verwendet werden.

In Haskell schreiben Sie Funktionen zur Arbeit gegen Typ - Klassen - so gibt es viele verschiedene Arten von Objekten jede gegebene Funktion gegen funktionieren kann, solange es sich um eine Art der gegebenen ist Klasse diese Funktion funktioniert auf.

Freistehende Funktionen ermöglichen eine solche Entkopplung, die Sie nicht erhalten, wenn Sie sich darauf konzentrieren, Ihre Funktionen so zu schreiben, dass sie innerhalb des Typs A funktionieren, da Sie sie dann nicht verwenden können, wenn Sie keine Instanz des Typs A haben, auch wenn die Funktion dies könnte Andernfalls sollten Sie allgemein genug sein, um für eine Instanz vom Typ B oder Typ C verwendet zu werden.

Jimmy Hoffa
quelle
3
Ist das nicht der springende Punkt bei Schnittstellen? Damit die Elemente, mit denen Typ B und Typ C für Ihre Funktion gleich aussehen, für mehrere Typen verwendet werden können?
Random832
2
@ Random832 absolut, aber warum sollte eine Funktion in einen Datentyp eingebettet werden, wenn nicht mit diesem Datentyp gearbeitet werden soll? Die Antwort: Es ist der einzige Grund , eine Funktion in einen Datentyp einzubetten. Sie könnten nichts anderes als statische Klassen schreiben und dafür sorgen, dass sich alle Funktionen nicht um den Datentyp kümmern, in den sie eingekapselt sind, um sie vollständig von ihrem eigenen Typ zu entkoppeln. Warum sollten Sie sich dann überhaupt darum kümmern, sie in einen Typ zu setzen? Der funktionale Ansatz lautet: Machen Sie sich keine Gedanken, schreiben Sie Ihre Funktionen auf Schnittstellen, und dann gibt es keinen Grund, sie mit Ihren Daten zu kapseln.
Jimmy Hoffa
Sie müssen die Schnittstellen noch implementieren.
Random832
2
@ Random832 Die Schnittstellen sind Datentypen. Sie benötigen keine darin gekapselten Funktionen. Bei freien Funktionen müssen alle Schnittstellen die Daten preisen, die sie zur Verfügung stellen, damit Funktionen verwendet werden können.
Jimmy Hoffa
2
@ Random832, um sich auf reale Objekte zu beziehen, wie es in OO so üblich ist, denken Sie an die Schnittstelle eines Buches: Es präsentiert Informationen (Daten), das ist alles. Sie haben die freie Funktion, Seiten umzublättern, die gegen die Klasse der Typen mit Seiten arbeitet. Diese Funktion funktioniert gegen alle Arten von Büchern, Zeitungen, die Posterspindeln bei K-Mart, Grußkarten, Post, alles, was in der Liste zusammengeheftet ist Ecke. Wenn Sie die Turn-Page als Mitglied des Buches implementiert haben, verpassen Sie alle Dinge, die Sie für Turn-Page verwenden könnten, da es sich um eine kostenlose Funktion handelt, die nicht gebunden ist. es wirft nur eine PartyFoulException auf Bier.
Jimmy Hoffa
4

In Java und ähnlichen Inkarnationen von OOP können Instanzmethoden (im Gegensatz zu freien Funktionen oder Erweiterungsmethoden) nicht aus anderen Modulen hinzugefügt werden.

Dies stellt eine größere Einschränkung dar, wenn Sie Schnittstellen berücksichtigen, die nur von den Instanzmethoden implementiert werden können. Sie können eine Schnittstelle und eine Klasse nicht in verschiedenen Modulen definieren und dann den Code eines dritten Moduls verwenden, um sie miteinander zu verbinden. Ein flexiblerer Ansatz, wie Haskells Typklassen, sollte das können.

CodesInChaos
quelle
Das geht in Scala ganz einfach. Ich kenne Go nicht, aber AFAIK kann man dort auch machen. In Ruby ist es ebenfalls üblich, Objekte nachträglich mit Methoden zu versehen, um sie an eine bestimmte Schnittstelle anzupassen. Was Sie beschreiben, scheint eher ein schlecht entworfenes Typensystem zu sein als alles, was auch nur aus der Ferne mit OO zusammenhängt. Nur als Gedankenexperiment: Wie würde Ihre Antwort anders ausfallen, wenn Sie über abstrakte Datentypen anstelle von Objekten sprechen? Ich glaube nicht, dass es einen Unterschied machen würde, was beweisen würde, dass Ihr Argument nicht mit OO zusammenhängt.
Jörg W Mittag
1
@ JörgWMittag Ich denke du meintest algebraische Datentypen. Und CodesInChaos, Haskell, rät ausdrücklich von Ihren Vorschlägen ab. Es wird als verwaiste Instanz bezeichnet und gibt Warnungen auf GHC aus.
Jozefg
3
@ JörgWMittag Mein Eindruck ist, dass viele, die OOP kritisieren, die in Java und ähnlichen Sprachen verwendete Form von OOP mit ihrer starren Klassenstruktur und dem Fokus auf Instanzmethoden kritisieren. Mein Eindruck von diesem Zitat ist, dass es den Fokus auf Instanzmethoden kritisiert und nicht wirklich auf andere OOP-Varianten zutrifft, wie das, was Golang verwendet.
CodesInChaos
2
@CodesInChaos Dann vielleicht als "statische Klasse basierend OO" zu
klären
@jozefg: Ich spreche von abstrakten Datentypen. Ich verstehe nicht einmal, wie wichtig algebraische Datentypen für diese Diskussion sind.
Jörg W Mittag
3

Bei der Objektorientierung geht es im Wesentlichen um die prozedurale Datenabstraktion (oder die funktionale Datenabstraktion, wenn Sie orthogonale Nebenwirkungen beseitigen). In gewisser Hinsicht ist Lambda Calculus die älteste und reinste objektorientierte Sprache, da sie nur funktionale Datenabstraktion bietet (da sie außer Funktionen keine Konstrukte enthält).

Nur die Operationen eines einzelnen Objekts können die Datendarstellung dieses Objekts überprüfen. Das können nicht einmal andere Objekte des gleichen Typs . (Dies ist der Hauptunterschied zwischen objektorientierter Datenabstraktion und abstrakten Datentypen: Bei ADTs können Objekte desselben Typs die gegenseitige Datendarstellung überprüfen, nur die Darstellung von Objekten anderer Typen wird ausgeblendet.)

Dies bedeutet, dass mehrere Objekte desselben Typs unterschiedliche Datendarstellungen haben können. Sogar das gleiche Objekt kann zu verschiedenen Zeiten unterschiedliche Datendarstellungen haben. (In Scala wechseln Maps und Sets beispielsweise abhängig von der Anzahl der Elemente zwischen einem Array und einem Hash-Versuch, da bei sehr kleinen Zahlen die lineare Suche in einem Array aufgrund der sehr kleinen konstanten Faktoren schneller ist als die logarithmische Suche in einem Suchbaum .)

Von außerhalb eines Objekts sollten Sie nicht, Sie können seine Datendarstellung nicht kennen. Das ist das Gegenteil von enger Kopplung.

Jörg W. Mittag
quelle
Ich habe Klassen in OOP, die interne Datenstrukturen abhängig von den Umständen wechseln, sodass Objektinstanzen dieser Klassen gleichzeitig sehr unterschiedliche Datendarstellungen verwenden können. Grundlegende Daten versteckt und Kapselung würde ich sagen? Inwiefern unterscheidet sich Map in Scala von einer ordnungsgemäß implementierten Map-Klasse (zum Ausblenden und Einkapseln von Daten) in einer OOP-Sprache?
Marjan Venema
In Ihrem Beispiel können Sie durch die Kapselung Ihrer Daten mit Accessor-Funktionen in einer Klasse (und somit durch die enge Kopplung dieser Funktionen mit diesen Daten) Instanzen dieser Klasse locker mit dem Rest Ihres Programms koppeln. Sie widerlegen den zentralen Punkt des Zitats - sehr schön!
GlenPeterson
2

Eine enge Kopplung zwischen Daten und Funktionen ist schlecht, weil Sie in der Lage sein möchten, sich unabhängig voneinander zu ändern, und eine enge Kopplung erschwert dies, weil Sie nicht eines ändern können, ohne das andere zu kennen und möglicherweise zu ändern.

Sie möchten, dass unterschiedliche Daten, die für die Funktion angezeigt werden, keine Änderungen in der Funktion erfordern, und Sie möchten in ähnlicher Weise Änderungen an der Funktion vornehmen können, ohne dass Änderungen an den Daten erforderlich sind, mit denen sie arbeitet, um diese Funktionsänderungen zu unterstützen.

Michael Durrant
quelle
1
Ja ich möchte das. Ich habe jedoch die Erfahrung gemacht, dass beim Senden von Daten an eine nicht triviale Funktion, für deren Verarbeitung sie nicht explizit entwickelt wurde, diese Funktion zum Absturz neigt. Ich beziehe mich nicht nur auf die Typensicherheit, sondern auf alle Datenbedingungen, die von den Autoren der Funktion nicht erwartet wurden. Wenn die Funktion alt ist und häufig verwendet wird, kann jede Änderung, die den Datenfluss durch neue Daten ermöglicht, dazu führen, dass alte Daten, die noch funktionieren müssen, nicht mehr verwendet werden. Während die Entkopplung für Funktionen im Vergleich zu Daten ideal ist, kann die Realität dieser Entkopplung schwierig und gefährlich sein.
GlenPeterson