Können wir die Unveränderlichkeit in OOP wirklich nutzen, ohne alle wichtigen OOP-Funktionen zu verlieren?


11

Ich sehe die Vorteile, Objekte in meinem Programm unveränderlich zu machen. Wenn ich wirklich tief über ein gutes Design für meine Anwendung nachdenke, komme ich natürlich oft zu dem Ergebnis, dass viele meiner Objekte unveränderlich sind. Es kommt oft zu einem Punkt, an dem ich alle meine Objekte unveränderlich haben möchte .

Diese Frage befasst sich mit der gleichen Idee, aber keine Antwort legt nahe, was ein guter Ansatz für die Unveränderlichkeit ist und wann sie tatsächlich verwendet werden muss. Gibt es gute unveränderliche Designmuster? Die allgemeine Idee scheint zu sein, "Objekte unveränderlich zu machen, es sei denn, Sie müssen sie unbedingt ändern", was in der Praxis nutzlos ist.

Ich habe die Erfahrung gemacht, dass Unveränderlichkeit meinen Code immer mehr zum funktionalen Paradigma treibt und dieser Fortschritt immer passiert:

  1. Ich brauche beständige (im funktionalen Sinne) Datenstrukturen wie Listen, Karten usw.
  2. Es ist äußerst unpraktisch, mit Querverweisen zu arbeiten (z. B. Baumknoten, die auf seine Kinder verweisen, während Kinder auf ihre Eltern verweisen), wodurch ich überhaupt keine Querverweise verwende, was wiederum meine Datenstrukturen und meinen Code funktionaler macht.
  3. Die Vererbung hört auf, irgendeinen Sinn zu ergeben, und ich beginne stattdessen, Komposition zu verwenden.
  4. Die gesamten Grundideen von OOP wie die Kapselung fallen auseinander und meine Objekte sehen aus wie Funktionen.

Zu diesem Zeitpunkt verwende ich praktisch nichts mehr aus dem OOP-Paradigma und kann einfach zu einer rein funktionalen Sprache wechseln. Daher meine Frage: Gibt es einen konsistenten Ansatz für ein gutes unveränderliches OOP-Design oder ist es immer so, dass Sie, wenn Sie die unveränderliche Idee voll ausschöpfen, immer in einer funktionalen Sprache programmieren, die nichts mehr aus der OOP-Welt benötigt? Gibt es gute Richtlinien, um zu entscheiden, welche Klassen unveränderlich sein sollen und welche veränderlich bleiben sollen, um sicherzustellen, dass OOP nicht auseinander fällt?

Nur der Einfachheit halber werde ich ein Beispiel geben. Lassen Sie uns ChessBoardeine unveränderliche Sammlung unveränderlicher Schachfiguren haben (Erweiterung der abstrakten KlassePiece). Aus der Sicht der OOP ist ein Stück dafür verantwortlich, gültige Bewegungen aus seiner Position auf dem Brett zu generieren. Aber um die Bewegungen zu erzeugen, benötigt das Stück einen Verweis auf sein Brett, während das Brett einen Verweis auf seine Stücke haben muss. Nun, es gibt einige Tricks, um diese unveränderlichen Querverweise abhängig von Ihrer OOP-Sprache zu erstellen, aber sie sind mühsam zu verwalten, besser kein Stück, um auf das Board zu verweisen. Aber dann kann das Stück seine Bewegungen nicht erzeugen, da es den Zustand des Bretts nicht kennt. Dann wird das Stück nur eine Datenstruktur, die den Stücktyp und seine Position enthält. Sie können dann eine polymorphe Funktion verwenden, um Bewegungen für alle Arten von Teilen zu generieren. Dies ist in der funktionalen Programmierung perfekt erreichbar, in OOP jedoch fast unmöglich, ohne Laufzeit-Typprüfungen und andere schlechte OOP-Praktiken ... Dann


3
Ich mag die Basisfrage, aber ich habe Schwierigkeiten mit den Details. ZB warum hört die Vererbung auf, Sinn zu machen, wenn Sie die Unveränderlichkeit maximieren?
Martin Ba

1
Ihre Objekte haben keine Methoden?
Hören Sie auf, Monica am


4
"Aus Sicht der OOP ist ein Stück dafür verantwortlich, gültige Züge von seiner Position auf dem Brett aus zu generieren." - definitiv nicht, ein solches Stück zu entwerfen würde SRP höchstwahrscheinlich schaden.
Doc Brown

1
@lishaak Sie "kämpfen, um das Problem mit der Vererbung in einfachen Worten zu erklären", weil es kein Problem ist; schlechtes Design ist ein Problem. Vererbung ist die Essenz von OOP, die unabdingbare Voraussetzung, wenn Sie so wollen. Viele der sogenannten "Probleme mit der Vererbung" sind tatsächlich Probleme mit Sprachen, die keine explizite Überschreibungssyntax haben, und sie sind in besser gestalteten Sprachen viel weniger problematisch. Ohne virtuelle Methoden und Polymorphismus haben Sie kein OOP. Sie haben prozedurale Programmierung mit lustiger Objektsyntax. Kein Wunder also, dass Sie die Vorteile von OO nicht sehen, wenn Sie dies vermeiden!
Mason Wheeler

Antworten:


24

Können wir die Unveränderlichkeit in OOP wirklich nutzen, ohne alle wichtigen OOP-Funktionen zu verlieren?

Ich verstehe nicht warum nicht. Ich mache das schon seit Jahren, bevor Java 8 sowieso funktionsfähig wurde. Schon mal was von Strings gehört? Schön und unveränderlich von Anfang an.

  1. Ich brauche beständige (im funktionalen Sinne) Datenstrukturen wie Listen, Karten usw.

Ich brauche die schon die ganze Zeit. Es ist einfach unhöflich, meine Iteratoren ungültig zu machen, weil Sie die Sammlung während des Lesens mutiert haben.

  1. Es ist äußerst unpraktisch, mit Querverweisen zu arbeiten (z. B. Baumknoten, die auf seine Kinder verweisen, während Kinder auf ihre Eltern verweisen), wodurch ich überhaupt keine Querverweise verwende, was wiederum meine Datenstrukturen und meinen Code funktionaler macht.

Rundschreiben sind eine besondere Art der Hölle. Unveränderlichkeit wird Sie nicht davor retten.

  1. Die Vererbung hört auf, irgendeinen Sinn zu ergeben, und ich beginne stattdessen, Komposition zu verwenden.

Nun, hier bin ich bei dir, aber ich verstehe nicht, was es mit Unveränderlichkeit zu tun hat. Der Grund, warum ich Komposition mag, ist nicht, dass ich das dynamische Strategiemuster liebe, sondern dass ich dadurch meinen Abstraktionsgrad ändern kann.

  1. Die gesamten Grundideen von OOP wie die Kapselung fallen auseinander und meine Objekte sehen aus wie Funktionen.

Ich schaudere, wenn ich daran denke, was Ihre Vorstellung von "OOP wie Kapselung" ist. Wenn es sich um Getter und Setter handelt, hören Sie einfach auf, diese Kapselung aufzurufen, da dies nicht der Fall ist. Das war es nie. Es ist manuelle aspektorientierte Programmierung. Eine Chance zur Validierung und ein Ort zum Setzen eines Haltepunkts ist schön, aber keine Kapselung. Die Einkapselung bewahrt mein Recht, nicht zu wissen oder sich darum zu kümmern, was in mir vorgeht.

Ihre Objekte sollen wie Funktionen aussehen. Sie sind Taschen voller Funktionen. Es sind Taschen voller Funktionen, die sich zusammen bewegen und sich gemeinsam neu definieren.

Funktionale Programmierung ist derzeit in Mode und die Leute werfen einige Missverständnisse über OOP ab. Lassen Sie sich nicht verwirren, dass dies das Ende von OOP ist. Functional und OOP können sehr gut zusammenleben.

  • Die funktionale Programmierung ist formal in Bezug auf Aufgaben.

  • OOP ist formal in Bezug auf Funktionszeiger.

Wirklich, dass es. Dykstra sagte uns, dass gotoes schädlich sei, also wurden wir formell und erstellten eine strukturierte Programmierung. Einfach so geht es bei diesen beiden Paradigmen darum, Wege zu finden, um Dinge zu erledigen und gleichzeitig die Fallstricke zu vermeiden, die entstehen, wenn man diese lästigen Dinge beiläufig erledigt.

Lass mich dir etwas zeigen:

f n (x)

Das ist eine Funktion. Es ist eigentlich ein Kontinuum von Funktionen:

f 1 (x)
f 2 (x)
...
f n (x)

Ratet mal, wie wir das in OOP-Sprachen ausdrücken?

n.f(x)

Das Wenige ndort wählt aus, welche Implementierung von fverwendet wird UND es entscheidet, welche Konstanten in dieser Funktion verwendet werden (was offen gesagt dasselbe bedeutet). Beispielsweise:

f 1 (x) = x + 1
f 2 (x) = x + 2

Das ist das gleiche, was Verschlüsse bieten. Wenn sich Schließungen auf ihren umschließenden Bereich beziehen, beziehen sich Objektmethoden auf ihren Instanzstatus. Objekte können Verschlüsse besser machen. Ein Abschluss ist eine einzelne Funktion, die von einer anderen Funktion zurückgegeben wird. Ein Konstruktor gibt einen Verweis auf eine ganze Reihe von Funktionen zurück:

g 1 (x) = x 2 + 1
g 2 (x) = x 2 + 2

Ja, du hast es erraten:

n.g(x)

f und g sind Funktionen, die sich gemeinsam ändern und gemeinsam bewegen. Also stecken wir sie in dieselbe Tasche. Das ist ein Objekt wirklich. Halten Sie nnur konstant (unveränderlich) bedeutet , es ist einfacher , vorherzusagen , was diese tun werden , wenn Sie sie nennen.

Das ist nur die Struktur. Die Art, wie ich über OOP denke, ist eine Menge kleiner Dinge, die mit anderen kleinen Dingen sprechen. Hoffentlich zu einer kleinen ausgewählten Gruppe kleiner Dinge. Wenn ich codiere, stelle ich mich als das Objekt vor. Ich betrachte die Dinge aus der Sicht des Objekts. Und ich versuche faul zu sein, damit ich das Objekt nicht überarbeite. Ich nehme einfache Nachrichten auf, arbeite ein wenig daran und sende einfache Nachrichten nur an meine besten Freunde. Wenn ich mit diesem Objekt fertig bin, hüpfe ich in ein anderes und betrachte die Dinge aus ihrer Perspektive.

Klassenverantwortungskarten waren die ersten, die mir beigebracht haben, so zu denken. Mann, ich war damals verwirrt über sie, aber verdammt, wenn sie heute noch nicht relevant sind.

Lassen Sie uns ein Schachbrett als unveränderliche Sammlung unveränderlicher Schachfiguren haben (Erweiterung der abstrakten Klasse Piece). Aus der Sicht der OOP ist ein Stück dafür verantwortlich, gültige Bewegungen aus seiner Position auf dem Brett zu generieren. Aber um die Bewegungen zu erzeugen, benötigt das Stück einen Verweis auf sein Brett, während das Brett einen Verweis auf seine Stücke haben muss.

Arg! Wieder mit den unnötigen Zirkelverweisen.

Wie wäre es mit: A ChessBoardDataStructureverwandelt xy-Schnüre in Stückreferenzen. Diese Stücke haben eine Methode, die x, y und ein bestimmtes nimmt ChessBoardDataStructureund daraus eine Sammlung brandneuer ChessBoardDataStructures macht. Schiebt das dann in etwas, das den besten Zug auswählen kann. Jetzt ChessBoardDataStructurekann unveränderlich sein und so können die Stücke. Auf diese Weise haben Sie immer nur einen weißen Bauern im Gedächtnis. Es gibt nur einige Verweise darauf an genau den richtigen xy-Stellen. Objektorientiert, funktional und unveränderlich.

Warten Sie, haben wir nicht schon über Schach gesprochen?



1
Und zu guter Letzt der Vertrag von Orlando .
Jörg W Mittag

1
@Euphoric: Ich denke, dass es eine ziemlich Standardformulierung ist, die den Standarddefinitionen für "funktionale Programmierung", "imperative Programmierung", "logische Programmierung" usw. entspricht. Andernfalls ist C eine funktionale Sprache, da Sie FP darin codieren können. Eine Logiksprache, weil Sie die Logikprogrammierung darin codieren können, eine dynamische Sprache, weil Sie die dynamische Typisierung darin codieren können, eine Schauspielersprache, weil Sie ein Akteursystem darin codieren können, und so weiter. Und Haskell ist die weltweit wichtigste imperative Sprache, da Sie Nebenwirkungen tatsächlich benennen, in Variablen speichern und als ... übergeben können
Jörg W Mittag

1
Ich denke, wir können ähnliche Ideen verwenden wie in Matthias Felleisens genialem Artikel über Sprachausdruck. Das Verschieben eines OO-Programms von beispielsweise Java nach C♯ kann nur mit lokalen Transformationen erfolgen. Das Verschieben nach C erfordert jedoch eine globale Umstrukturierung (im Grunde müssen Sie einen Mechanismus zum Versenden von Nachrichten einführen und alle Funktionsaufrufe über dieses Programm umleiten) Java und C♯ können OO ausdrücken und C kann es "nur" codieren.
Jörg W Mittag

1
@lishaak Weil du es für verschiedene Dinge spezifizierst. Dies hält es auf einer Abstraktionsebene und verhindert das Duplizieren der Speicherung von Positionsinformationen, die sonst inkonsistent werden könnten. Wenn Sie die zusätzliche Eingabe einfach erneut senden, geben Sie sie in eine Methode ein, sodass Sie sie nur einmal eingeben. Ein Teil muss sich nicht daran erinnern, wo es sich befindet, wenn Sie ihm mitteilen, wo es sich befindet. Jetzt speichern Stücke nur Informationen wie Farbe, Bewegungen und Bild / Abkürzung, die immer wahr und unveränderlich sind.
candied_orange

2

Die nützlichsten Konzepte, die OOP meiner Meinung nach in den Mainstream eingeführt hat, sind:

  • Richtige Modularisierung.
  • Datenverkapselung.
  • Klare Trennung zwischen privaten und öffentlichen Schnittstellen.
  • Klare Mechanismen für die Code-Erweiterbarkeit.

All diese Vorteile können auch ohne die traditionellen Implementierungsdetails wie Vererbung oder sogar Klassen realisiert werden. Alan Kays ursprüngliche Idee eines "objektorientierten Systems" verwendete "Nachrichten" anstelle von "Methoden" und war Erlang näher als z. B. C ++. Schauen Sie sich Go an, das viele traditionelle OOP-Implementierungsdetails überflüssig macht, sich aber dennoch einigermaßen objektorientiert anfühlt.

Wenn Sie unveränderliche Objekte verwenden, können Sie weiterhin die meisten herkömmlichen OOP-Extras verwenden: Schnittstellen, dynamischer Versand, Kapselung. Sie brauchen keine Setter und oft nicht einmal Getter für einfachere Objekte. Sie können auch die Vorteile der Unveränderlichkeit nutzen: Sie sind immer sicher, dass sich ein Objekt in der Zwischenzeit nicht geändert hat, kein defensives Kopieren, keine Datenrennen und es ist einfach, die Methoden rein zu machen.

Schauen Sie sich an, wie Scala versucht, Unveränderlichkeits- und FP-Ansätze mit OOP zu kombinieren. Zugegeben, es ist nicht die einfachste und eleganteste Sprache. Es ist jedoch ziemlich praktisch erfolgreich. Schauen Sie sich auch Kotlin an, das viele Tools und Ansätze für eine ähnliche Mischung bietet.

Das übliche Problem, einen anderen Ansatz zu versuchen, als die Sprachschöpfer vor N Jahren gedacht hatten, ist die "Impedanzfehlanpassung" mit der Standardbibliothek. OTOH-Java- und .NET-Ökosysteme bieten derzeit eine angemessene Standardbibliotheksunterstützung für unveränderliche Datenstrukturen. Natürlich gibt es auch Bibliotheken von Drittanbietern.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.