Wie entscheidet man, ob ein Datenobjekttyp unveränderlich gestaltet werden soll?


23

Ich liebe das unveränderliche "Muster" aufgrund seiner Stärken und in der Vergangenheit fand ich es vorteilhaft, Systeme mit unveränderlichen Datentypen (einige, die meisten oder sogar alle) zu entwerfen. Wenn ich das tue, schreibe ich oft weniger Fehler und das Debuggen ist viel einfacher.

Allerdings scheuen meine Kollegen im Allgemeinen unveränderlich. Sie sind überhaupt nicht unerfahren (weit davon entfernt), aber sie schreiben Datenobjekte auf klassische Weise - private Mitglieder mit einem Getter und einem Setter für jedes Mitglied. Normalerweise nehmen ihre Konstruktoren dann keine Argumente oder nehmen aus Bequemlichkeitsgründen nur einige Argumente. So oft sieht das Erstellen eines Objekts so aus:

Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Vielleicht machen sie das überall. Vielleicht definieren sie nicht einmal einen Konstruktor, der diese beiden Zeichenfolgen verwendet, egal wie wichtig sie sind. Und vielleicht ändern sie den Wert dieser Saiten später nicht und müssen es auch nie. Klar, wenn diese Dinge wahr sind, wäre das Objekt besser als unveränderlich zu gestalten, oder? (Der Konstruktor übernimmt die beiden Eigenschaften, überhaupt keine Setter.)

Wie entscheiden Sie, ob ein Objekttyp unveränderlich gestaltet werden soll? Gibt es eine Reihe guter Kriterien, nach denen man es beurteilen kann?

Ich überlege derzeit, ob ich einige Datentypen in meinem eigenen Projekt auf unveränderlich umstellen soll, aber ich müsste es meinen Kollegen gegenüber rechtfertigen, und die Daten in den Typen könnten sich (sehr selten) ändern - zu welchem ​​Zeitpunkt können Sie sich natürlich ändern auf unveränderliche Weise (erstellen Sie ein neues Objekt und kopieren Sie die Eigenschaften des alten Objekts, mit Ausnahme derjenigen, die Sie ändern möchten). Aber ich bin mir nicht sicher, ob dies nur meine Liebe zu Unveränderlichen ist oder ob es einen tatsächlichen Bedarf gibt, von ihnen zu profitieren.


1
Programm in Erlang und das ganze Problem ist gelöst, alles ist unveränderlich
Zachary K

1
@ZacharyK Eigentlich wollte ich etwas über funktionale Programmierung erwähnen, verzichtete aber auf Grund meiner begrenzten Erfahrung mit funktionalen Programmiersprachen.
Ricket

Antworten:


27

Scheint, als würden Sie sich rückwärts nähern. Man sollte standardmäßig unveränderlich sein. Machen Sie ein Objekt nur dann veränderbar, wenn Sie es unbedingt als unveränderliches Objekt verwenden müssen / können.


11

Der Hauptvorteil von unveränderlichen Objekten ist die Gewährleistung der Gewindesicherheit. In einer Welt, in der mehrere Kerne und Threads die Norm sind, ist dieser Vorteil sehr wichtig geworden.

Die Verwendung von veränderlichen Objekten ist jedoch sehr praktisch. Ihre Leistung ist gut. Solange Sie sie nicht über separate Threads modifizieren und ein gutes Verständnis für Ihre Arbeit haben, sind sie recht zuverlässig.


15
Der Vorteil von unveränderlichen Objekten besteht darin, dass sie leichter zu beurteilen sind. In Multithread-Umgebungen ist dies sehr viel einfacher. Bei einfachen Singlethread-Algorithmen ist dies immer noch einfacher. Sie müssen sich zum Beispiel nie darum kümmern, ob der Zustand konsistent ist, ob das Objekt alle Mutationen bestanden hat, die für die Verwendung in einem bestimmten Kontext erforderlich sind usw. Mit den besten veränderlichen Objekten können Sie sich auch nur wenig um den genauen Zustand kümmern: z. B. Caches.
9000,

-1, ich bin mit @ 9000 einverstanden. Die Thread-Sicherheit ist zweitrangig (betrachten Sie Objekte, die unveränderlich erscheinen, aber einen internen veränderlichen Status haben, z. B. aufgrund von Memoisierung). Das Erhöhen der veränderlichen Leistung ist auch eine vorzeitige Optimierung und es ist möglich, alles zu verteidigen, wenn der Benutzer wissen muss, was er tut. Wenn ich die ganze Zeit wüsste, was ich tue, würde ich niemals ein Programm mit einem Fehler schreiben.
Doval

4
@Doval: Da gibt es nichts zu bestreiten. 9000 ist absolut richtig; unveränderliche Objekte sind leichter zu überlegen. Das macht sie zum Teil so nützlich für die gleichzeitige Programmierung. Der andere Teil ist, dass Sie sich keine Sorgen machen müssen, ob sich der Zustand ändert. Eine vorzeitige Optimierung ist hier unerheblich; Bei der Verwendung unveränderlicher Objekte geht es um Design, nicht um Leistung, und eine schlechte Auswahl der Datenstrukturen im Voraus ist eine vorzeitige Pessimisierung.
Robert Harvey

@RobertHarvey Ich bin mir nicht sicher, was Sie unter "schlechte Auswahl der Datenstruktur im Vorfeld" verstehen. Es gibt unveränderliche Versionen der meisten veränderlichen Datenstrukturen, die eine ähnliche Leistung bieten. Wenn Sie eine Liste benötigen und die Wahl zwischen einer unveränderlichen Liste und einer veränderlichen Liste haben, entscheiden Sie sich für eine unveränderliche Liste, bis Sie sicher sind, dass dies ein Engpass in Ihrer Anwendung ist und die veränderliche Version eine bessere Leistung erbringt.
Doval

2
@Doval: Ich habe Okasaki gelesen. Diese Datenstrukturen werden normalerweise nur verwendet, wenn Sie eine Sprache verwenden, die das Funktionsparadigma vollständig unterstützt, z. B. Haskell oder Lisp. Und ich bestreite die Vorstellung, dass unveränderliche Strukturen die Standardwahl sind; Die überwiegende Mehrheit der Business-Computing-Systeme basiert immer noch auf veränderlichen Strukturen (dh relationalen Datenbanken). Mit unveränderlichen Datenstrukturen anzufangen ist eine nette Idee, aber es ist immer noch ein sehr elfenbeinfarbener Turm.
Robert Harvey

6

Es gibt zwei Hauptmethoden, um zu entscheiden, ob ein Objekt unveränderlich ist.

a) Basierend auf der Art des Objekts

Es ist einfach, diese Situationen zu erfassen, da wir wissen, dass sich diese Objekte nach ihrer Erstellung nicht ändern werden. Wenn Sie beispielsweise eine RequestHistoryEntität haben und naturgemäß Entitäten nicht ändern, sobald sie erstellt wurden. Diese Objekte können einfach als unveränderliche Klassen entworfen werden. Beachten Sie, dass das Anforderungsobjekt veränderbar ist, da es seinen Status ändern kann und wem es im Laufe der Zeit usw. zugewiesen wird, der Anforderungsverlauf sich jedoch nicht ändert. Beispielsweise wurde in der letzten Woche ein Verlaufselement erstellt, als es vom Status "Eingereicht" in den Status "Zugewiesen" verschoben wurde, UND dieses Verlaufselement kann sich nie ändern. Das ist also ein klassischer unveränderlicher Fall.

b) Abhängig von der Wahl des Designs, externe Faktoren

Dies ähnelt dem Beispiel java.lang.String. Zeichenfolgen können sich im Laufe der Zeit tatsächlich ändern, wurden jedoch aufgrund von Caching- / Zeichenfolgenpool- / Nebenläufigkeitsfaktoren als unveränderlich festgelegt. In ähnlicher Weise kann das Caching / Concurrency usw. eine gute Rolle dabei spielen, ein Objekt immuatibel zu machen, wenn das Caching / Concurrency und die damit verbundene Leistung in der Anwendung von entscheidender Bedeutung sind. Diese Entscheidung sollte jedoch sehr sorgfältig getroffen werden, nachdem alle Auswirkungen analysiert wurden.

Der Hauptvorteil von unveränderlichen Objekten besteht darin, dass sie keinem Tumble-Weed-Muster ausgesetzt sind. Das Objekt nimmt während der gesamten Lebensdauer keine Änderungen auf und erleichtert die Codierung und Wartung erheblich.


4

Ich überlege derzeit, ob ich einige Datentypen in meinem eigenen Projekt auf unveränderlich umstellen soll, aber ich müsste es meinen Kollegen gegenüber rechtfertigen, und die Daten in den Typen könnten sich (sehr selten) ändern - zu welchem ​​Zeitpunkt können Sie sich natürlich ändern auf unveränderliche Weise (erstellen Sie ein neues Objekt und kopieren Sie die Eigenschaften des alten Objekts, mit Ausnahme derjenigen, die Sie ändern möchten).

Die Minimierung des Status eines Programms ist von großem Vorteil.

Fragen Sie sie, ob sie einen Typ mit veränderlichen Werten in einer Ihrer Klassen für die temporäre Speicherung einer Clientklasse verwenden möchten.

Wenn sie ja sagen, fragen Sie warum? Der veränderbare Zustand gehört in ein solches Szenario nicht. Das Erzwingen, dass sie den Zustand erstellen, zu dem er tatsächlich gehört, und das Erzwingen eines möglichst eindeutigen Zustands Ihrer Datentypen sind gute Gründe.


4

Die Antwort ist etwas sprachabhängig. Ihr Code sieht aus wie Java, wo dieses Problem so schwierig wie möglich ist. In Java können Objekte nur als Referenz übergeben werden, und der Klon ist vollständig fehlerhaft.

Es gibt keine einfache Antwort, aber mit Sicherheit möchten Sie Objekte mit kleinem Wert unveränderlich machen. Java hat Strings korrekt unveränderlich gemacht, aber Datum und Kalender fälschlicherweise veränderlich gemacht.

Machen Sie also auf jeden Fall Objekte mit geringem Wert unveränderlich und implementieren Sie einen Kopierkonstruktor. Vergiss alles über Cloneable, es ist so schlecht gestaltet, dass es nutzlos ist.

Wenn es für Objekte mit größerem Wert unpraktisch ist, sie unveränderlich zu machen, können Sie sie einfach kopieren.


Hört sich sehr danach an, wie man beim Schreiben von C oder C ++ zwischen Stack und Heap wählt :)
Ricket

@Ricket: nicht so sehr IMO. Stack / Heap hängt von der Lebensdauer des Objekts ab. In C ++ ist es sehr verbreitet, dass sich veränderbare Objekte auf dem Stapel befinden.
Kevin Cline

1

Und vielleicht ändern sie den Wert dieser Saiten später nicht und müssen es auch nie. Klar, wenn diese Dinge wahr sind, wäre das Objekt besser als unveränderlich zu gestalten, oder?

Etwas kontraintuitiv ist es ein ziemlich gutes Argument, dass es egal ist, ob die Objekte unveränderlich sind oder nicht, niemals die Zeichenfolgen später ändern zu müssen. Der Programmierer behandelt sie bereits als effektiv unveränderlich, unabhängig davon, ob der Compiler sie erzwingt oder nicht.

Unveränderlichkeit tut normalerweise nicht weh, hilft aber auch nicht immer. Sie können leicht feststellen, ob Ihr Objekt von der Unveränderlichkeit profitiert, wenn Sie jemals eine Kopie des Objekts erstellen oder einen Mutex erwerben müssen, bevor Sie es ändern . Wenn es sich nie ändert, dann verschafft Unveränderlichkeit Ihnen nichts und macht die Dinge manchmal komplizierter.

Sie tun einen guten Punkt über das Risiko haben , ein Objekt in einem ungültigen Zustand konstruieren, aber das ist wirklich ein anderes Thema von Unveränderlichkeit. Ein Objekt kann sowohl wandelbar sein und immer in einem gültigen Zustand nach dem Bau.

Die Ausnahme von dieser Regel ist, dass Java weder benannte Parameter noch Standardparameter unterstützt. Daher kann es manchmal unhandlich werden, eine Klasse zu entwerfen, die ein gültiges Objekt nur mit überladenen Konstruktoren garantiert. Nicht so sehr mit dem Fall mit zwei Eigenschaften, aber es gibt auch etwas zu sagen, was die Konsistenz betrifft, wenn dieses Muster in anderen Teilen Ihres Codes bei ähnlichen, aber größeren Klassen häufig vorkommt.


Ich finde es merkwürdig, dass Diskussionen über Veränderlichkeit die Beziehung zwischen Unveränderlichkeit und der Art und Weise, wie ein Objekt sicher freigelegt oder geteilt werden kann, nicht erkennen. Ein Verweis auf ein tief unveränderliches Klassenobjekt kann problemlos mit nicht vertrauenswürdigem Code geteilt werden. Ein Verweis auf eine Instanz einer veränderlichen Klasse kann gemeinsam genutzt werden, wenn jedem, der über einen Verweis verfügt, vertraut werden kann, dass er das Objekt niemals ändert oder einem Code aussetzt, der dies möglicherweise tut. Ein Verweis auf eine Klasseninstanz, die möglicherweise mutiert ist, sollte im Allgemeinen überhaupt nicht freigegeben werden. Wenn nur ein Verweis auf ein Objekt irgendwo im Universum existiert ...
Supercat

... und nichts hat seinen "Identitäts-Hashcode" abgefragt, ihn zum Sperren verwendet oder auf andere Weise auf seine "verborgenen ObjectFunktionen" zugegriffen. Eine direkte Änderung eines Objekts wäre semantisch nicht anders, als die Referenz mit einer Referenz auf ein neues Objekt zu überschreiben, das es war identisch, aber für die angegebene Änderung. Eine der größten semantischen Schwächen in Java, IMHO, ist, dass es keine Mittel gibt, mit denen Code anzeigen kann, dass eine Variable nur den einzigen nicht-flüchtigen Verweis auf etwas enthalten soll. Verweise sollten nur an Methoden übergeben werden können, die nach ihrer Rückkehr möglicherweise keine Kopie behalten können.
Supercat

0

Möglicherweise habe ich eine zu niedrige Sichtweise und wahrscheinlich, weil ich C und C ++ verwende, was es nicht so einfach macht, alles unveränderlich zu machen, aber ich sehe unveränderliche Datentypen als Optimierungsdetail , um mehr zu schreiben Effiziente Funktionen, die frei von Nebenwirkungen sind und Funktionen wie das Rückgängigmachen von Systemen und die zerstörungsfreie Bearbeitung auf einfache Weise bereitstellen.

Dies könnte zum Beispiel enorm teuer sein:

/// @return A new mesh whose vertices have been transformed
/// by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

... wenn Meshnicht als persistente Datenstruktur konzipiert, sondern als Datentyp, der vollständig kopiert werden musste (was in manchen Szenarien Gigabyte umfassen kann), selbst wenn wir nur a ändern werden Teil davon (wie im obigen Szenario, in dem wir nur Scheitelpunktpositionen ändern).

Dann greife ich zur Unveränderlichkeit und gestalte die Datenstruktur so, dass nicht veränderte Teile davon flach kopiert und die Referenzen gezählt werden können, damit die oben beschriebene Funktion einigermaßen effizient ist, ohne dass ganze Netze tief kopiert werden müssen, während die Datenstruktur noch geschrieben werden kann Die Funktion ist nebenwirkungsfrei, was die Thread-Sicherheit, die Ausnahmesicherheit, die Möglichkeit, den Vorgang rückgängig zu machen, ihn zerstörungsfrei anzuwenden usw. dramatisch vereinfacht.

In meinem Fall ist es zu kostspielig (zumindest vom Standpunkt der Produktivität aus), alles unveränderlich zu machen, daher speichere ich es für die Klassen, die zu teuer sind, um sie vollständig zu kopieren. Bei diesen Klassen handelt es sich normalerweise um umfangreiche Datenstrukturen wie Maschen und Bilder. Im Allgemeinen verwende ich eine veränderbare Schnittstelle, um Änderungen an diesen über ein "Builder" -Objekt auszudrücken und eine neue unveränderliche Kopie zu erhalten. Und ich tue nicht so viel, um unveränderliche Garantien auf der zentralen Ebene der Klasse zu erreichen, sondern um mir zu helfen, die Klasse in Funktionen einzusetzen, die frei von Nebenwirkungen sein können. Mein Wunsch, Meshunveränderlich zu machen, besteht nicht darin, Netze unveränderlich zu machen, sondern es zu ermöglichen, auf einfache Weise Funktionen zu schreiben, die frei von Nebeneffekten sind, die ein Netz eingeben und ein neues ausgeben, ohne dafür einen hohen Speicher- und Rechenaufwand zu zahlen.

Als Ergebnis habe ich nur 4 unveränderliche Datentypen in meiner gesamten Codebasis, und sie sind alle kräftige Datenstrukturen, aber ich benutze sie stark, um mir beim Schreiben von Funktionen zu helfen, die frei von Nebenwirkungen sind. Diese Antwort könnte zutreffen, wenn Sie wie ich in einer Sprache arbeiten, die es nicht so einfach macht, alles unveränderlich zu machen. In diesem Fall können Sie sich darauf konzentrieren, den Großteil Ihrer Funktionen so zu gestalten, dass Nebenwirkungen vermieden werden. In diesem Fall möchten Sie möglicherweise bestimmte Datenstrukturen (PDS-Typen) als Optimierungsdetail unveränderlich machen, um teure Vollkopien zu vermeiden. In der Zwischenzeit, wenn ich eine Funktion wie diese habe:

/// @return The v1 * v2.
Vector3f vec_mul(Vector3f v1, Vector3f v2);

... dann habe ich keine Versuchung, Vektoren unveränderlich zu machen, da sie billig genug sind, um sie nur vollständig zu kopieren. Hier kann kein Leistungsvorteil erzielt werden, wenn Vektoren in unveränderliche Strukturen umgewandelt werden, mit denen nicht modifizierte Teile kopiert werden können. Diese Kosten würden die Kosten für das Kopieren des gesamten Vektors aufwiegen.


-1
Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Wenn Foo nur zwei Eigenschaften hat, ist es einfach, einen Konstruktor zu schreiben, der zwei Argumente akzeptiert. Angenommen, Sie fügen eine andere Eigenschaft hinzu. Dann müssen Sie einen weiteren Konstruktor hinzufügen:

public Foo(String a, String b, SomethingElse c)

Zu diesem Zeitpunkt ist es noch überschaubar. Aber was ist, wenn es 10 Immobilien gibt? Sie möchten keinen Konstruktor mit 10 Argumenten. Sie können das Builder-Muster zum Erstellen von Instanzen verwenden, dies erhöht jedoch die Komplexität. Zu diesem Zeitpunkt werden Sie sich überlegen: "Warum habe ich nicht einfach für alle Eigenschaften Setter hinzugefügt, wie es normale Leute tun"?


4
Ist ein Konstruktor mit zehn Argumenten schlechter als die NullPointerException, die auftritt, wenn property7 nicht festgelegt wurde? Ist das Builder-Muster komplexer als der Code, der bei jeder Methode auf nicht initialisierte Eigenschaften überprüft wird? Es ist besser, einmal zu überprüfen, wann das Objekt erstellt wurde.
Kevin Cline


1
Warum wollen Sie keinen Konstruktor mit 10 Argumenten? An welchem ​​Punkt ziehen Sie die Linie? Ich denke, ein Konstruktor mit 10 Argumenten ist ein kleiner Preis für die Vorteile der Unveränderlichkeit. Außerdem haben Sie entweder eine Zeile mit 10 durch Kommas getrennten Dingen (optional in mehrere Zeilen oder sogar 10 Zeilen, wenn es besser aussieht), oder Sie haben sowieso 10 Zeilen, wenn Sie jede Eigenschaft einzeln
einrichten

1
@Ricket: Weil es das Risiko erhöht, die Argumente in die falsche Reihenfolge zu bringen . Wenn Sie Setter oder das Builder-Muster verwenden, ist das ziemlich unwahrscheinlich.
Mike Baranczak

4
Wenn Sie einen Konstruktor mit 10 Argumenten haben, ist es vielleicht an der Zeit, diese Argumente in eine eigene Klasse (oder eigene Klassen) zu kapseln und / oder Ihr Klassendesign kritisch zu betrachten.
Adam Lear
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.