Komplette Unveränderlichkeit und objektorientierte Programmierung

43

In den meisten OOP-Sprachen sind Objekte im Allgemeinen mit einer begrenzten Anzahl von Ausnahmen (wie z. B. Tupel und Zeichenfolgen in Python) veränderbar. In den meisten funktionalen Sprachen sind Daten unveränderlich.

Sowohl veränderbare als auch unveränderliche Objekte bringen eine ganze Reihe von Vor- und Nachteilen mit sich.

Es gibt Sprachen, die versuchen, beide Konzepte zu verbinden, wie z. B. Scala, bei denen Sie (explizit deklarierte) veränderbare und unveränderbare Daten haben (bitte korrigieren Sie mich, wenn ich mich irre, meine Kenntnisse über Scala sind mehr als begrenzt).

Meine Frage ist: Ist vollständige (sic!) Unveränderlichkeit - dh, kein Objekt kann sich ändern, wenn es einmal erstellt wurde - in einem OOP-Kontext sinnvoll?

Gibt es Entwürfe oder Implementierungen eines solchen Modells?

Sind (vollständige) Unveränderlichkeit und OOP grundsätzlich gegensätzlich oder orthogonal?

Motivation: In OOP Sie normal arbeiten auf Daten, Ändern (mutiert) , um die zugrunde liegenden Informationen, Referenzen zwischen den Objekten zu halten. ZB ein Klassenobjekt Personmit einem Mitglied, das fatherauf ein anderes PersonObjekt verweist . Wenn Sie den Namen des Vaters ändern, ist dies für das untergeordnete Objekt sofort sichtbar, ohne dass eine Aktualisierung erforderlich ist. Da Sie unveränderlich sind, müssen Sie neue Objekte für Vater und Kind konstruieren. Mit gemeinsamen Objekten, Multithreading, GIL usw. hätten Sie jedoch viel weniger Probleme.

Hyperboreus
quelle
2
Unveränderlichkeit kann in einer OOP-Sprache simuliert werden, indem nur Objektzugriffspunkte als Methoden oder schreibgeschützte Eigenschaften verfügbar gemacht werden, die die Daten nicht verändern. Die Unveränderlichkeit funktioniert in OOP-Sprachen genauso wie in jeder anderen funktionalen Sprache, mit der Ausnahme, dass möglicherweise einige Funktionen der funktionalen Sprache fehlen.
Robert Harvey
4
Die Veränderbarkeit ist weder eine Eigenschaft von OOP-Sprachen wie C # und Java noch eine Eigenschaft der Unveränderlichkeit. Sie legen die Veränderlichkeit oder Unveränderlichkeit fest, indem Sie die Klasse schreiben.
Robert Harvey
9
Ihre Vermutung scheint zu sein, dass die Wandlungsfähigkeit ein Kernmerkmal der Objektorientierung ist. Ist es nicht. Die Veränderbarkeit ist einfach eine Eigenschaft von Objekten oder Werten. Die Objektorientierung umfasst eine Reihe von Begriffen (Verkapselung, Polymorphismus, Vererbung usw.), die wenig oder gar nichts mit Mutation zu tun haben, und Sie würden dennoch die Vorteile dieser Funktionen ableiten. auch wenn du alles unveränderlich gemacht hast.
Robert Harvey
2
@MichaelT Bei der Frage geht es nicht darum, bestimmte Dinge veränderlich zu machen, sondern darum, alle Dinge unveränderlich zu machen.

Antworten:

43

OOP und Unveränderlichkeit sind fast vollständig orthogonal zueinander. Imperative Programmierung und Unveränderlichkeit sind es jedoch nicht.

OOP kann durch zwei Kernfunktionen zusammengefasst werden:

  • Kapselung : Ich greife nicht direkt auf den Inhalt von Objekten zu, sondern kommuniziere über eine bestimmte Schnittstelle („Methoden“) mit diesem Objekt. Diese Schnittstelle kann interne Daten vor mir verbergen. Technisch gesehen bezieht sich dies eher auf die modulare Programmierung als auf OOP. Der Zugriff auf Daten über eine definierte Schnittstelle entspricht in etwa einem abstrakten Datentyp.

  • Dynamischer Versand : Wenn ich eine Methode für ein Objekt aufrufe, wird die ausgeführte Methode zur Laufzeit aufgelöst. (In klassenbasiertem OOP kann ich beispielsweise eine sizeMethode für eine IListInstanz aufrufen, der Aufruf wird jedoch möglicherweise in eine Implementierung in einer LinkedListKlasse aufgelöst.) Dynamischer Versand ist eine Möglichkeit, polymorphes Verhalten zuzulassen.

Kapselung ist ohne Veränderbarkeit weniger sinnvoll (es gibt keinen internen Zustand , der durch Einmischung von außen verfälscht werden könnte), aber sie macht Abstraktionen auch dann leichter, wenn alles unveränderlich ist.

Ein imperatives Programm besteht aus Anweisungen, die nacheinander ausgeführt werden. Eine Anweisung hat Nebenwirkungen wie das Ändern des Programmstatus. Bei Unveränderlichkeit kann der Status nicht geändert werden (natürlich kann ein neuer Status erstellt werden). Imperative Programmierung ist daher grundsätzlich nicht mit Unveränderlichkeit vereinbar.

Es kommt nun vor, dass OOP historisch immer mit imperativer Programmierung verbunden war (Simula basiert auf Algol) und alle gängigen OOP-Sprachen imperative Wurzeln haben (C ++, Java, C #, ... sind alle in C verwurzelt). Dies bedeutet nicht, dass OOP selbst zwingend oder veränderlich wäre. Dies bedeutet lediglich, dass die Implementierung von OOP durch diese Sprachen eine Veränderlichkeit ermöglicht.

amon
quelle
2
Vielen Dank, insbesondere für die Definition der beiden Kernfunktionen.
Hyperboreus
Dynamic Dispatch ist keine Kernfunktion von OOP. Encapsulation ist es auch nicht (wie Sie bereits zugegeben haben).
Hören Sie auf, Monica am
5
@OrangeDog Ja, es gibt keine allgemein akzeptierte Definition von OOP, aber ich brauchte eine Definition, um damit zu arbeiten. Also habe ich etwas ausgewählt, das der Wahrheit so nahe wie möglich kommt, ohne eine ganze Dissertation darüber zu schreiben. Ich betrachte den dynamischen Versand jedoch als das einzige Hauptmerkmal, das OOP von anderen Paradigmen unterscheidet. Etwas, das wie OOP aussieht, bei dem jedoch alle Anrufe statisch aufgelöst wurden, ist eigentlich nur modulare Programmierung mit Ad-hoc-Polymorphismus. Objekte sind ein Paar von Methoden und Daten und als solche gleichbedeutend mit Abschlüssen.
amon
2
"Objekte sind eine Paarung von Methoden und Daten". Alles was Sie brauchen, ist etwas, das nichts mit Unveränderlichkeit zu tun hat.
Hören Sie auf, Monica am
3
@CodeYogi Das Ausblenden von Daten ist die häufigste Art der Kapselung. Die Art und Weise, wie Daten von einem Objekt intern gespeichert werden, ist jedoch nicht das einzige Implementierungsdetail, das ausgeblendet werden sollte. Ebenso wichtig ist es zu verbergen, wie die öffentliche Schnittstelle implementiert ist, z. B. ob ich Hilfsmethoden verwende. Solche Hilfsmethoden sollten im Allgemeinen auch privat sein. Zusammenfassend lässt sich sagen: Die Kapselung ist ein Prinzip, während das Verbergen von Daten eine Kapselungstechnik ist.
amon
25

Beachten Sie, dass es unter objektorientierten Programmierern eine Kultur gibt, in der die Leute davon ausgehen, dass die meisten Ihrer Objekte veränderbar sind, wenn Sie OOP ausführen. Dies ist jedoch eine andere Frage als die, ob OOP eine Veränderlichkeit erfordert . Außerdem scheint sich diese Kultur langsam zu mehr Unveränderlichkeit zu verändern, da die Menschen der funktionalen Programmierung ausgesetzt sind.

Scala ist ein wirklich gutes Beispiel dafür, dass für die Objektorientierung keine Veränderlichkeit erforderlich ist. Während Scala die Wandlungsfähigkeit unterstützt , wird von seiner Verwendung abgeraten. Idiomatic Scala ist sehr objektorientiert und nahezu unveränderlich. Es ermöglicht hauptsächlich die Veränderbarkeit für die Kompatibilität mit Java, und weil unter bestimmten Umständen unveränderliche Objekte ineffizient oder verschlungen sind, um damit zu arbeiten.

Vergleichen Sie beispielsweise eine Scala-Liste und eine Java-Liste . Die unveränderliche Liste von Scala enthält alle die gleichen Objektmethoden wie die veränderbare Liste von Java. Tatsächlich, weil Java statische Funktionen für Operationen wie sort verwendet und Scala Methoden im funktionalen Stil wie map. Alle Kennzeichen von OOP - Kapselung, Vererbung und Polymorphismus - sind in einer Form verfügbar, die objektorientierten Programmierern vertraut ist und in geeigneter Weise verwendet wird.

Der einzige Unterschied besteht darin, dass Sie beim Ändern der Liste ein neues Objekt erhalten. Das erfordert oft, dass Sie andere Entwurfsmuster verwenden als bei veränderlichen Objekten, aber Sie müssen OOP nicht ganz aufgeben.

Karl Bielefeldt
quelle
17

Unveränderlichkeit kann in einer OOP-Sprache simuliert werden, indem nur Objektzugriffspunkte als Methoden oder schreibgeschützte Eigenschaften verfügbar gemacht werden, die die Daten nicht verändern. Die Unveränderlichkeit funktioniert in OOP-Sprachen genauso wie in jeder anderen funktionalen Sprache, mit der Ausnahme, dass möglicherweise einige Funktionen der funktionalen Sprache fehlen.

Ihre Vermutung scheint zu sein, dass die Wandlungsfähigkeit ein Kernmerkmal der Objektorientierung ist. Aber Veränderlichkeit ist einfach eine Eigenschaft von Objekten oder Werten. Die Objektorientierung umfasst eine Reihe von Begriffen (Verkapselung, Polymorphismus, Vererbung usw.), die wenig oder gar nichts mit Mutation zu tun haben, und Sie würden trotzdem die Vorteile dieser Funktionen nutzen, selbst wenn Sie alles unveränderlich machen würden.

Auch müssen nicht alle funktionalen Sprachen unveränderlich sein. Clojure verfügt über eine spezielle Annotation, mit der Typen geändert werden können, und die meisten "praktischen" funktionalen Sprachen können veränderbare Typen angeben.

Eine bessere Frage könnte sein: "Ist vollständige Unveränderlichkeit in der imperativen Programmierung sinnvoll ?" Ich würde sagen, die offensichtliche Antwort auf diese Frage ist nein. Um eine vollständige Unveränderlichkeit in der imperativen Programmierung zu erreichen, müssten Sie auf forSchleifen verzichten (da Sie eine Schleifenvariable mutieren müssten), und zwar zugunsten der Rekursion, und jetzt programmieren Sie im Wesentlichen sowieso funktional.

Robert Harvey
quelle
Danke. Könnten Sie bitte Ihren letzten Absatz etwas näher erläutern ("offensichtlich" könnte etwas subjektiv sein).
Hyperboreus
Bereits getan ....
Robert Harvey
1
@Hyperboreus Es gibt viele Möglichkeiten, um Polymorphismus zu erreichen. Subtypisierung mit dynamischem Versand, statischer Ad-hoc-Polymorphismus (auch bekannt als Funktionsüberladung) und parametrischer Polymorphismus (auch bekannt als Generika) sind die gängigsten Methoden, und alle Methoden haben ihre Stärken und Schwächen. Moderne OOP-Sprachen kombinieren all diese drei Möglichkeiten, während sich Haskell hauptsächlich auf parametrischen Polymorphismus und Ad-hoc-Polymorphismus stützt.
amon
3
@RobertHarvey Sie sagen, dass Sie eine Wandlungsfähigkeit benötigen, weil Sie eine Schleife ausführen müssen (ansonsten müssen Sie eine Rekursion verwenden). Bevor ich vor zwei Jahren mit Haskell anfing, dachte ich, ich brauche auch veränderbare Variablen. Ich sage nur, dass es noch andere Möglichkeiten zum "Schleifen" gibt (Abbilden, Falten, Filtern usw.). Warum brauchen Sie sonst veränderbare Variablen, wenn Sie sich von der Tabelle verabschiedet haben?
Cimmanon
1
@RobertHarvey Aber genau darum geht es bei Programmiersprachen: Was ist Ihnen ausgesetzt und nicht, was passiert unter der Haube. Letzteres liegt in der Verantwortung des Compilers oder Interpreters, nicht des Anwendungsentwicklers. Ansonsten zurück zum Assembler.
Hyperboreus
5

Es ist oft nützlich, Objekte als Verkapselung von Werten oder Entitäten zu kategorisieren. Der Unterschied besteht darin, dass Code, der einen Verweis auf einen Wert enthält, niemals dessen Status in einer Weise ändern sollte, die der Code selbst nicht initiiert hat. Im Gegensatz dazu kann Code, der einen Verweis auf ein Unternehmen enthält, erwarten, dass er sich außerhalb der Kontrolle des Referenzinhabers ändert.

Während es möglich ist, einen Kapselungswert mit Objekten veränderlichen oder unveränderlichen Typs zu verwenden, kann sich ein Objekt nur als Wert verhalten, wenn mindestens eine der folgenden Bedingungen zutrifft:

  1. Kein Verweis auf das Objekt wird jemals einem Gegenstand ausgesetzt, der den darin eingeschlossenen Zustand verändern könnte.

  2. Der Inhaber von mindestens einem der Verweise auf das Objekt kennt alle Verwendungen, für die ein noch vorhandener Verweis verwendet werden könnte.

Da alle Instanzen unveränderlicher Typen automatisch die erste Anforderung erfüllen, ist es einfach, sie als Werte zu verwenden. Im Gegensatz dazu ist es viel schwieriger, sicherzustellen, dass beide Anforderungen erfüllt sind, wenn veränderbare Typen verwendet werden. Während Verweise auf unveränderliche Typen als Mittel zum Einkapseln des darin eingekapselten Zustands frei weitergegeben werden können, erfordert das Umgehen des in veränderlichen Typen gespeicherten Zustands entweder das Erstellen unveränderlicher Wrapper-Objekte oder das Kopieren des durch privat gehaltene Objekte eingekapselten Zustands in andere Objekte Entweder geliefert von oder konstruiert für den Empfänger der Daten.

Unveränderliche Typen eignen sich sehr gut zum Übergeben von Werten und sind häufig zumindest in gewissem Umfang zum Manipulieren von Werten geeignet. Sie sind jedoch nicht so gut im Umgang mit Entitäten. Das nächste, was man zu einer Entität in einem System mit rein unveränderlichen Typen haben kann, ist eine Funktion, die in Anbetracht des Systemzustands die Attribute eines Teils davon meldet oder eine neue Systemzustandsinstanz erzeugt, die a ähnelt geliefert, mit Ausnahme eines bestimmten Teils davon, der in wählbarer Weise unterschiedlich sein wird. Wenn der Zweck einer Entität darin besteht, einen Code mit etwas zu verknüpfen, das in der realen Welt existiert, kann es für die Entität möglicherweise unmöglich sein, das Offenlegen eines veränderlichen Zustands zu vermeiden.

Wenn man beispielsweise einige Daten über eine TCP-Verbindung empfängt, könnte man ein neues Objekt "Zustand der Welt" erzeugen, das diese Daten in seinem Puffer enthält, ohne dass sich dies auf Verweise auf den alten "Zustand der Welt", aber auf alte Kopien von auswirkt Der Weltzustand, der den letzten Datenstapel nicht enthält, ist fehlerhaft und sollte nicht verwendet werden, da er nicht mehr mit dem Zustand des realen TCP-Sockets übereinstimmt.

Superkatze
quelle
4

In c # sind einige Typen unveränderlich wie Zeichenfolgen.

Dies scheint weiterhin darauf hinzudeuten, dass die Wahl stark in Betracht gezogen wurde.

Natürlich ist es sehr leistungsintensiv, unveränderliche Typen zu verwenden, wenn Sie diesen Typ hunderttausend Mal ändern müssen. Aus diesem Grund wird in diesem Fall empfohlen, die StringBuilderKlasse anstelle der stringKlasse zu verwenden.

Ich habe ein Experiment mit einem Profiler gemacht und die Verwendung des unveränderlichen Typs ist wirklich mehr CPU- und RAM-Anforderungen.

Es ist auch intuitiv, wenn Sie bedenken, dass Sie zum Ändern von nur einem Buchstaben in einer Zeichenfolge von 4000 Zeichen jedes Zeichen in einen anderen Bereich des RAM kopieren müssen.

Revious
quelle
6
Das häufige Ändern unveränderlicher Daten muss nicht wie bei wiederholter stringVerkettung katastrophal langsam sein . Für nahezu alle Arten von Daten / Anwendungsfällen kann (oftmals bereits) eine effiziente persistente Struktur erfunden werden. Die meisten von ihnen haben ungefähr die gleiche Leistung, auch wenn die konstanten Faktoren manchmal schlechter sind.
@delnan Ich denke auch, dass sich der letzte Absatz der Antwort mehr auf ein Implementierungsdetail als auf (Un) Wandlungsfähigkeit bezieht.
Hyperboreus
@Hyperboreus: Denkst du, ich sollte diesen Teil löschen? Aber wie kann sich eine Zeichenfolge ändern, wenn sie unveränderlich ist? Ich meine .. meiner Meinung nach, aber sicher kann ich mich irren, das könnte der Hauptgrund sein, warum Objekte nicht unveränderlich sind.
Revious
1
@Revious Auf keinen Fall. Lassen Sie es, damit es zu Diskussionen und interessanteren Meinungen und Standpunkten kommt.
Hyperboreus
1
@Revious Ja, das Lesen wäre langsamer, wenn auch nicht so langsam wie das Ändern von a string(der traditionellen Darstellung). Ein "String" (in der Darstellung, von der ich spreche) nach 1000 Modifikationen wäre wie ein frisch erstellter String (Moduloinhalt); Keine nützliche oder weit verbreitete persistente Datenstruktur verschlechtert die Qualität nach X-Operationen. Speicherfragmentierung ist kein ernstes Problem (Sie würden viele Zuweisungen haben, ja, aber Fragmentierung ist in modernen Garbage Collectors kein Problem )
0

Vollständige Unveränderlichkeit von allem ergibt in OOP oder den meisten anderen Paradigmen aus einem sehr wichtigen Grund keinen Sinn:

Jedes nützliche Programm hat Nebenwirkungen.

Ein Programm, das nichts verändert, ist wertlos. Möglicherweise haben Sie es nicht einmal ausgeführt, da der Effekt identisch ist.

Auch wenn Sie denken, dass Sie nichts ändern, und einfach eine Liste von Zahlen zusammenfassen, die Sie irgendwie erhalten haben, denken Sie daran, dass Sie etwas mit dem Ergebnis anfangen müssen - ob Sie es auf Standardausgabe drucken, in eine Datei schreiben, oder wo auch immer. Dazu muss ein Puffer mutiert und der Status des Systems geändert werden.

Es kann sehr sinnvoll sein, die Veränderbarkeit auf die Teile zu beschränken , die geändert werden müssen. Aber wenn sich absolut nichts ändern muss, dann tun Sie nichts, was es wert ist, getan zu werden.

cHao
quelle
4
Ich verstehe nicht, wie sich Ihre Antwort auf die Frage bezieht, da ich keine reinen funktionalen Sprachen angesprochen habe. Nehmen wir zum Beispiel Erlang: Unveränderliche Daten, keine zerstörerische Zuordnung, keine Ungewissheit über Nebenwirkungen. Sie haben auch einen Zustand in einer funktionalen Sprache, nur dass der Zustand durch die Funktionen "fließt", im Gegensatz zu den Funktionen, die an dem Zustand arbeiten. Der Status ändert sich, er mutiert jedoch nicht, sondern ein zukünftiger Status ersetzt den aktuellen Status. Bei der Unveränderlichkeit geht es nicht darum, ob ein Speicherpuffer geändert wird oder nicht, es geht darum, ob diese Mutationen von außen sichtbar sind.
Hyperboreus
Und ein zukünftiger Staat ersetzt den jetzigen wie genau? In einem OO-Programm ist dieser Zustand eine Eigenschaft eines Objekts irgendwo. Das Ersetzen des Zustands erfordert das Ändern eines Objekts (oder das Ersetzen durch ein anderes, was eine Änderung der Objekte erfordert, die auf ihn verweisen (oder das Ersetzen durch ein anderes, was ... wie. Sie verstehen, worauf es ankommt). Sie könnten sich eine Art monadischen Hack einfallen lassen, bei dem jede Aktion zu einer völlig neuen Anwendung führt ... aber selbst dann muss der aktuelle Status des Programms irgendwo aufgezeichnet werden.
CHAO
7
-1. Das ist falsch. Sie verwechseln Nebenwirkungen mit Mutationen, und obwohl sie von funktionalen Sprachen häufig gleich behandelt werden, sind sie unterschiedlich. Jedes nützliche Programm hat Nebenwirkungen. Nicht jedes nützliche Programm hat eine Mutation.
Michael Shaw
@Michael: Wenn es um OOP geht, sind Mutation und Nebenwirkungen so eng miteinander verbunden, dass sie nicht realistisch voneinander getrennt werden können. Wenn Sie keine Mutation haben, können Sie keine Nebenwirkungen haben, ohne massiv zu hacken.
CHAO
-2

Ich denke, es hängt davon ab, ob Ihre Definition von OOP darin besteht, dass ein Nachrichtenübermittlungsstil verwendet wird.

Reine Funktionen müssen nichts ändern, da sie Werte zurückgeben, die Sie in neuen Variablen speichern können.

var brandNewVariable = pureFunction(foo);

Mit dem Nachrichtenübergabestil weisen Sie ein Objekt an, neue Daten zu speichern, anstatt es zu fragen, welche neuen Daten in einer neuen Variablen gespeichert werden sollen.

sameOldObject.changeMe(foo);

Es ist möglich, Objekte zu haben und sie nicht zu mutieren, indem man ihre Methoden zu reinen Funktionen macht, die zufällig im Inneren des Objekts statt außerhalb leben.

var brandNewVariable = nonMutatingObject.askMe(foo);

Es ist jedoch nicht möglich, den Stil der Nachrichtenübermittlung mit unveränderlichen Objekten zu mischen.

presley
quelle