Ist es in Ordnung, den Zustand eines unveränderlichen Objekts offenzulegen?

Lesezeit: 9 Minuten

Benutzer-Avatar
StickyCube

Nachdem ich kürzlich auf das Konzept der unveränderlichen Objekte gestoßen bin, würde ich gerne wissen, wie man den Zugriff auf den Zustand am besten kontrolliert. Auch wenn der objektorientierte Teil meines Gehirns mich dazu bringt, mich beim Anblick öffentlicher Mitglieder vor Angst zu ducken, sehe ich bei so etwas keine technischen Probleme:

public class Foo {
    public final int x;
    public final int y;

    public Foo( int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Ich würde mich wohler fühlen, wenn ich die Felder als deklarieren würde private und Bereitstellung von Getter-Methoden für jeden, aber dies scheint übermäßig komplex zu sein, wenn der Zustand explizit nur gelesen wird.

Was ist die beste Methode, um Zugriff auf den Zustand eines unveränderlichen Objekts zu gewähren?

Es hängt ganz davon ab, wie Sie das Objekt verwenden werden. Öffentliche Felder sind nicht von Natur aus böse, es ist nur schlecht, alles standardmäßig öffentlich zu machen. Beispielsweise macht die Klasse java.awt.Point ihre x- und y-Felder öffentlich, und sie sind nicht einmal endgültig. Ihr Beispiel scheint eine gute Verwendung öffentlicher Felder zu sein, aber andererseits möchten Sie vielleicht nicht alle internen Felder eines anderen unveränderlichen Objekts verfügbar machen. Es gibt keine Auffangregel.

  • @KevinWorkman In den meisten Fällen werden alle Overhead-Spuren durch Methoden-Inlining gelöscht.

    – Marko Topolnik

    20. Februar 2014 um 14:36 ​​Uhr

  • @Marko Topolnik: Wie Kevin betonte, gibt es keine Auffangregel. Aber ich würde lieber Interfaces+Factories als “Standard” sehen und keine öffentlichen Klassen mit öffentlichen Konstruktoren. Ich dachte gelegentlich “Oh, ich hätte hier eine Schnittstelle statt einer konkreten Klasse verwenden sollen” – aber ich noch nie dachte “Oh, ich hätte hier eine konkrete Klasse statt einer Schnittstelle verwenden sollen” 😉

    – Marco13

    20. Februar 2014 um 15:18 Uhr

  • @Marco13 Andererseits lautet mein Motto “Jede Codezeile muss sich bewähren.” Es scheint zu ganz anderen Voreinstellungen zu führen.

    – Marko Topolnik

    20. Februar 2014 um 17:47 Uhr

  • @MarkoTopolnik, in der Tat, aber ich würde auch denken, dass dies zur Auswahl einer anderen Sprache als Java führen würde. 🙂

    – Charles Duffy

    20. Februar 2014 um 22:41 Uhr

  • @Zack: Sie können keinen Code umgestalten, der Ihren Code verwendet. Wenn Sie Ihren Code veröffentlichen (auch intern in Ihrem Unternehmen), ist es fast unmöglich, Schnittstellen zu ändern.

    – Dawor

    21. Februar 2014 um 9:48 Uhr

Ich habe in der Vergangenheit das Gleiche gedacht, aber am Ende mache ich normalerweise Variablen privat und verwende Getter und Setter, sodass ich später immer noch die Möglichkeit habe, Änderungen an der Implementierung vorzunehmen, während ich die gleiche Schnittstelle behalte.

Das erinnerte mich an etwas, das ich kürzlich in „Clean Code“ von Robert C. Martin gelesen habe. In Kapitel 6 gibt er eine etwas andere Perspektive. Zum Beispiel auf Seite 95 sagt er

“Objekte verstecken ihre Daten hinter Abstraktionen und legen Funktionen offen, die mit diesen Daten arbeiten. Datenstrukturen legen ihre Daten offen und haben keine sinnvollen Funktionen.”

Und auf Seite 100:

Die Quasi-Verkapselung von Bohnen scheint einigen OO-Puristen ein besseres Gefühl zu geben, bietet aber normalerweise keinen weiteren Vorteil.

Basierend auf dem Codebeispiel scheint die Foo-Klasse eine Datenstruktur zu sein. Basierend auf dem, was ich aus der Diskussion in Clean Code verstanden habe (was mehr ist als nur die beiden Anführungszeichen, die ich gegeben habe), besteht der Zweck der Klasse darin, Daten bereitzustellen, nicht die Funktionalität, und Getter und Setter zu haben, nützt wahrscheinlich nicht viel.

Auch hier bin ich meiner Erfahrung nach normalerweise weitergegangen und habe den “Bohnen” -Ansatz privater Daten mit Gettern und Settern verwendet. Aber andererseits hat mich noch nie jemand gebeten, ein Buch darüber zu schreiben, wie man besseren Code schreibt, also hat Martin vielleicht etwas zu sagen.

  • Es gibt einen Mangel an Konzepten in Java. Viele Anwendungsfälle für eine Java-Klasse haben nichts mit Kapselung und Abstraktion zu tun. Andere Sprachen bieten so praktische Funktionen wie Vektoren, Tupel und Hashes mit prägnanter wörtlicher Syntax. Sie sind völlig transparent und legen alle ihre Daten offen – was a gut Ding.

    – Marko Topolnik

    20. Februar 2014 um 15:03 Uhr


  • Ein Getter ist eine sehr schwache Form der Kapselung, Sie legen alle Eigenschaften Ihrer Klasse und mit genau denselben Typen offen. Ein Setter, wenn auch viel schlimmer. Die Methoden (Nachrichten, die eine Klasse in der OO-Theorie empfängt) sind Befehle an ein Objekt, um eine bestimmte Arbeit auszuführen, und nicht die Anforderung von Informationen. IMMO gibt es sehr wenige vertretbare Verwendungen von Gettern, und ich sehe nie eine für einen Setter.

    – Alfredo Casado

    20. Februar 2014 um 15:12 Uhr


  • @AlfredoCasado Das Erstaunlichste ist, wie schwer es ist, diese offensichtliche Wahrheit einem Java-“Best Practitioner” zu erklären, einer leider sehr verbreiteten Rasse. Aber beachten Sie das im Allgemeinen dort sind Anwendungsfälle für Getter/Setter, nur nicht für reine Datenobjekte. Beispielsweise kann hinter einem Konfigurationsobjekt durchaus eine gewisse Logik stehen Eigenschaft setzen Aktion.

    – Marko Topolnik

    20. Februar 2014 um 19:50 Uhr

  • @AlfredoCasado: +1, ich werde verrückt, wenn ich versuche, das Hardcore-OO-Typen zu erklären. Die starke Verwendung von Gettern ist ein ziemlich solider Hinweis darauf, dass Sie nur ausführlichen prozeduralen Code schreiben.

    – Phoshi

    21. Februar 2014 um 10:40 Uhr

  • @Davor natürlich kannst du das tun, aber das ist kein “Getter”, das ist eine Methode mit einem Namen, der mit “get” beginnt, nicht dasselbe und wahrscheinlich ein sehr schlechter Name für eine Methode.

    – Alfredo Casado

    22. Februar 2014 um 19:09 Uhr


Wenn Ihr Objekt so lokal verwendet wird, dass Sie sich nicht um die Probleme mit zukünftigen API-Änderungen kümmern, müssen Sie Getter nicht zusätzlich zu den Instanzvariablen hinzufügen. Aber das ist ein allgemeines Thema, nicht spezifisch für unveränderliche Objekte.

Der Vorteil der Verwendung von Gettern ergibt sich aus einer zusätzlichen Indirektionsschicht, die kann sind praktisch, wenn Sie ein Objekt entwerfen, das weit verbreitet sein wird und dessen Nützlichkeit sich in unabsehbarer Zukunft erstrecken wird.

  • “, was praktisch sein kann, wenn Sie ein Objekt entwerfen, das weit verbreitet sein wird und dessen Nützlichkeit sich in unvorhersehbare Zukunft erstrecken wird.” kannst du ein kleines beispiel geben?

    – Aussenseiter

    20. Februar 2014 um 17:05 Uhr

  • Die einfachsten Beispiele sind Objekte, die die öffentliche API einer Bibliothek darstellen. Innerhalb eines Projekts können Sie interne Module haben, die sich ähnlich wie Bibliotheken verhalten.

    – Marko Topolnik

    20. Februar 2014 um 17:51 Uhr

  • +1. Meine Regel lautet: “Werde ich in Zukunft mit dem Refactoring von IDEs “Encapsulate Field” einverstanden sein? Wenn ja, öffentliche Felder, bis sie anderweitig benötigt werden.” Dies umfasst die Erstellung einer öffentlichen Bibliotheks-API, die mein Refactoring nicht erreichen kann.

    – orip

    25. Februar 2014 um 20:05 Uhr

  • @orip Eine sehr gute Art, es auszudrücken — und bitte teilen Sie uns mit, wie oft Sie es getan haben eigentlich musste a kapseln final aufstellen? Oder überhaupt irgendein Feld?

    – Marko Topolnik

    25. Februar 2014 um 20:07 Uhr

Benutzer-Avatar
Brian Agnew

Ungeachtet der Unveränderlichkeit enthüllen Sie immer noch die Implementierung dieser Klasse. Irgendwann möchten Sie die Implementierung ändern (oder vielleicht verschiedene Ableitungen erstellen, z. B. mit dem Point-Beispiel, Sie möchten vielleicht eine ähnliche Point-Klasse mit Polarkoordinaten), und Ihr Client-Code ist dem ausgesetzt.

Das obige Muster kann durchaus nützlich sein, aber ich würde es im Allgemeinen auf sehr lokalisierte Instanzen beschränken (z. B. Passing Tupel von Informationen um mich herum – ich neige dazu, zu finden, dass Objekte mit scheinbar nicht verwandten Informationen jedoch entweder schlechte Kapselungen sind oder dass die Informationen ist verwandt, und mein Tupel verwandelt sich in ein vollwertiges Objekt)

Die große Sache, die Sie im Auge behalten sollten, ist, dass Funktionsaufrufe eine universelle Schnittstelle bieten. Jedes Objekt kann über Funktionsaufrufe mit anderen Objekten interagieren. Sie müssen nur noch die richtigen Signaturen definieren und los geht’s. Der einzige Haken ist, dass Sie ausschließlich über diese Funktionsaufrufe interagieren müssen, was oft gut funktioniert, aber in einigen Fällen umständlich sein kann.

Der Hauptgrund für die direkte Offenlegung von Zustandsvariablen wäre die Möglichkeit, primitive Operatoren direkt auf diese Felder anzuwenden. Wenn dies gut gemacht wird, kann dies die Lesbarkeit und den Komfort verbessern: z. B. das Hinzufügen von komplexen Zahlen mit +oder Zugriff auf eine verschlüsselte Sammlung mit []. Die Vorteile davon können überraschend sein, vorausgesetzt, dass Ihre Verwendung der Syntax traditionellen Konventionen folgt.

Der Haken ist, dass Operatoren keine universelle Schnittstelle sind. Nur ein ganz bestimmter Satz eingebauter Typen kann sie verwenden, diese können nur so verwendet werden, wie es die Sprache erwartet, und Sie können keine neuen definieren. Sobald Sie also Ihre öffentliche Schnittstelle mit Primitiven definiert haben, haben Sie sich darauf festgelegt, dieses Primitiv zu verwenden, und nur dieses Primitiv (und andere Dinge, die leicht darauf gecastet werden können). Um etwas anderes zu verwenden, müssen Sie jedes Mal, wenn Sie damit interagieren, um dieses Primitiv herumtanzen, und das bringt Sie aus einer DRY-Perspektive um: Dinge können sehr schnell sehr zerbrechlich werden.

Einige Sprachen machen Operatoren zu einer universellen Schnittstelle, Java jedoch nicht. Dies ist keine Anklage gegen Java: Seine Designer haben sich bewusst gegen das Überladen von Operatoren entschieden, und sie hatten gute Gründe dafür. Selbst wenn Sie mit Objekten arbeiten, die gut zu den traditionellen Bedeutungen von Operatoren zu passen scheinen, kann es überraschend nuanciert sein, sie auf eine tatsächlich sinnvolle Weise arbeiten zu lassen, und wenn Sie es nicht absolut hinkriegen, werden Sie es tun das später bezahlen. Es ist oft viel einfacher, eine funktionsbasierte Schnittstelle lesbar und nutzbar zu machen, als diesen Prozess zu durchlaufen, und Sie erhalten am Ende sogar ein besseres Ergebnis, als wenn Sie Operatoren verwendet hätten.

Dort war Kompromisse, die mit dieser Entscheidung verbunden sind, jedoch. Dort sind Zeiten, in denen eine operatorbasierte Schnittstelle wirklich besser funktioniert als eine funktionsbasierte, aber ohne Operatorüberladung ist diese Option einfach nicht verfügbar. Der Versuch, Operatoren irgendwie einzuschmeicheln, wird Sie in einige Designentscheidungen zwingen, die Sie wahrscheinlich nicht wirklich in Stein gemeißelt haben möchten. Die Java-Designer dachten, dass sich dieser Kompromiss lohnt, und vielleicht hatten sie damit sogar Recht. Aber Entscheidungen wie diese kommen nicht ohne Folgen, und in solchen Situationen treffen die Folgen zu.

Kurz gesagt, das Problem besteht nicht darin, Ihre Implementierung offenzulegen, an sich. Das Problem besteht darin, sich in diese Implementierung einzusperren.

Benutzer-Avatar
Softwareentwickler

Tatsächlich bricht es die Kapselung, um jede Eigenschaft eines Objekts auf irgendeine Weise offenzulegen – jede Eigenschaft ist ein Implementierungsdetail. Nur weil das jeder macht, ist es noch lange nicht richtig. Die Verwendung von Accessoren und Mutatoren (Getter und Setter) macht es nicht besser. Stattdessen sollten die CQRS-Muster verwendet werden, um die Kapselung aufrechtzuerhalten.

Ich kenne nur eine Requisite, die Getter für endgültige Eigenschaften hat. Dies ist der Fall, wenn Sie über eine Schnittstelle auf die Eigenschaften zugreifen möchten.

    public interface Point {
       int getX();
       int getY();
    }

    public class Foo implements Point {...}
    public class Foo2 implements Point {...}

Ansonsten sind die öffentlichen Endfelder in Ordnung.

1112240cookie-checkIst es in Ordnung, den Zustand eines unveränderlichen Objekts offenzulegen?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy