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:
- Ich brauche beständige (im funktionalen Sinne) Datenstrukturen wie Listen, Karten usw.
- 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.
- Die Vererbung hört auf, irgendeinen Sinn zu ergeben, und ich beginne stattdessen, Komposition zu verwenden.
- 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 ChessBoard
eine 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
Antworten:
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.
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.
Rundschreiben sind eine besondere Art der Hölle. Unveränderlichkeit wird Sie nicht davor retten.
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.
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
goto
es 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
n
dort wählt aus, welche Implementierung vonf
verwendet 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
n
nur 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.
Arg! Wieder mit den unnötigen Zirkelverweisen.
Wie wäre es mit: A
ChessBoardDataStructure
verwandelt xy-Schnüre in Stückreferenzen. Diese Stücke haben eine Methode, die x, y und ein bestimmtes nimmtChessBoardDataStructure
und daraus eine Sammlung brandneuerChessBoardDataStructure
s macht. Schiebt das dann in etwas, das den besten Zug auswählen kann. JetztChessBoardDataStructure
kann 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?
quelle
Die nützlichsten Konzepte, die OOP meiner Meinung nach in den Mainstream eingeführt hat, sind:
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.
quelle