Warum Getter- und Setter-Methoden böse sind

Ich hatte nicht vor, eine "ist böse" -Serie zu starten, aber mehrere Leser baten mich zu erklären, warum ich in der Kolumne "Warum erweitert ist böse" im letzten Monat erwähnt habe, dass Sie Get / Set-Methoden vermeiden sollten.

Obwohl Getter / Setter-Methoden in Java üblich sind, sind sie nicht besonders objektorientiert (OO). Tatsächlich können sie die Wartbarkeit Ihres Codes beeinträchtigen. Darüber hinaus ist das Vorhandensein zahlreicher Getter- und Setter-Methoden eine rote Fahne dafür, dass das Programm aus OO-Sicht nicht unbedingt gut gestaltet ist.

Dieser Artikel erklärt, warum Sie keine Getter und Setter verwenden sollten (und wann Sie sie verwenden können), und schlägt eine Entwurfsmethode vor, die Ihnen hilft, aus der Getter / Setter-Mentalität auszubrechen.

Über die Natur des Designs

Bevor ich in eine andere designbezogene Kolumne eintrete (mit einem provokanten Titel, nicht weniger), möchte ich einige Dinge klarstellen.

Ich war verblüfft über einige Leserkommentare, die sich aus der Kolumne "Warum erweitert ist böse" des letzten Monats ergaben (siehe Talkback auf der letzten Seite des Artikels). Einige Leute glaubten, ich hätte argumentiert, dass die Objektorientierung schlecht ist, nur weil extendssie Probleme hat, als ob die beiden Konzepte gleichwertig wären. Das ist sicherlich nicht das, was ich dachte , sagte ich, so lassen Sie mich einige Meta-Fragen klären.

Diese Kolumne und der Artikel des letzten Monats befassen sich mit Design. Design ist von Natur aus eine Reihe von Kompromissen. Jede Wahl hat eine gute und eine schlechte Seite, und Sie treffen Ihre Wahl im Kontext der Gesamtkriterien, die durch die Notwendigkeit definiert werden. Gut und Böse sind jedoch nicht absolut. Eine gute Entscheidung in einem Kontext kann in einem anderen schlecht sein.

Wenn Sie nicht beide Seiten eines Problems verstehen, können Sie keine intelligente Entscheidung treffen. Wenn Sie nicht alle Auswirkungen Ihrer Handlungen verstehen, entwerfen Sie überhaupt nicht. Du stolperst im Dunkeln. Es ist kein Zufall, dass jedes Kapitel im Buch " Design Patterns " der Gang of Four einen Abschnitt "Konsequenzen" enthält, in dem beschrieben wird, wann und warum die Verwendung eines Musters unangemessen ist.

Die Aussage, dass einige Sprachfunktionen oder gängige Programmiersprachen (wie Accessoren) Probleme haben, ist nicht dasselbe wie zu sagen, dass Sie sie unter keinen Umständen verwenden sollten. Und nur weil ein Feature oder eine Redewendung häufig verwendet wird, heißt das nicht, dass Sie es auch verwenden sollten . Nicht informierte Programmierer schreiben viele Programme, und die einfache Verwendung durch Sun Microsystems oder Microsoft verbessert die Programmier- oder Designfähigkeiten einer Person nicht auf magische Weise. Die Java-Pakete enthalten viel großartigen Code. Aber es gibt auch Teile dieses Codes. Ich bin sicher, es ist den Autoren peinlich zuzugeben, dass sie geschrieben haben.

Aus dem gleichen Grund drücken Marketing oder politische Anreize häufig auf Design-Idiome. Manchmal treffen Programmierer schlechte Entscheidungen, aber Unternehmen möchten fördern, was die Technologie kann, und betonen daher, dass die Art und Weise, wie Sie dies tun, nicht ideal ist. Sie machen das Beste aus einer schlechten Situation. Folglich handeln Sie verantwortungslos, wenn Sie eine Programmierpraxis anwenden, nur weil "Sie so vorgehen sollen". Viele fehlgeschlagene Enterprise JavaBeans (EJB) -Projekte beweisen dieses Prinzip. EJB-basierte Technologie ist eine großartige Technologie, wenn sie angemessen eingesetzt wird, kann jedoch bei unsachgemäßer Verwendung ein Unternehmen buchstäblich zum Erliegen bringen.

Mein Punkt ist, dass Sie nicht blind programmieren sollten. Sie müssen verstehen, welches Chaos eine Funktion oder Redewendung anrichten kann. Auf diese Weise können Sie viel besser entscheiden, ob Sie diese Funktion oder diese Redewendung verwenden möchten. Ihre Entscheidungen sollten sowohl informiert als auch pragmatisch sein. Der Zweck dieser Artikel ist es, Ihnen zu helfen, Ihre Programmierung mit offenen Augen anzugehen.

Datenabstraktion

Ein grundlegendes Gebot von OO-Systemen ist, dass ein Objekt keine seiner Implementierungsdetails offenlegen sollte. Auf diese Weise können Sie die Implementierung ändern, ohne den Code zu ändern, der das Objekt verwendet. Daraus folgt, dass Sie in OO-Systemen Getter- und Setter-Funktionen vermeiden sollten, da diese meist Zugriff auf Implementierungsdetails bieten.

Um zu sehen, warum, bedenken Sie, dass eine getX()Methode in Ihrem Programm möglicherweise 1.000 Aufrufe enthält und jeder Aufruf davon ausgeht, dass der Rückgabewert von einem bestimmten Typ ist. Sie können getX()den Rückgabewert beispielsweise in einer lokalen Variablen speichern , und dieser Variablentyp muss mit dem Rückgabewerttyp übereinstimmen. Wenn Sie die Art und Weise, wie das Objekt implementiert wird, so ändern müssen, dass sich der Typ von X ändert, sind Sie in großen Schwierigkeiten.

Wenn X ein war int, aber jetzt ein sein muss, werden long1.000 Kompilierungsfehler angezeigt. Wenn Sie das Problem falsch beheben, indem Sie den Rückgabewert in intumwandeln, wird der Code sauber kompiliert, funktioniert jedoch nicht. (Der Rückgabewert wird möglicherweise abgeschnitten.) Sie müssen den Code für jeden dieser 1.000 Aufrufe ändern, um die Änderung zu kompensieren. Ich möchte auf keinen Fall so viel arbeiten.

Ein Grundprinzip von OO-Systemen ist die Datenabstraktion . Sie sollten die Art und Weise, in der ein Objekt einen Nachrichtenhandler implementiert, vollständig vor dem Rest des Programms verbergen. Dies ist ein Grund, warum alle Ihre Instanzvariablen (die nicht konstanten Felder einer Klasse) sein sollten private.

Wenn Sie eine Instanzvariable publicerstellen, können Sie das Feld nicht ändern, da sich die Klasse im Laufe der Zeit weiterentwickelt, da Sie den externen Code, der das Feld verwendet, beschädigen würden. Sie möchten nicht nach 1.000 Verwendungen einer Klasse suchen, nur weil Sie diese Klasse ändern.

Dieses Prinzip des Versteckens der Implementierung führt zu einem guten Test der Qualität eines OO-Systems: Können Sie massive Änderungen an einer Klassendefinition vornehmen - sogar das Ganze wegwerfen und durch eine völlig andere Implementierung ersetzen -, ohne den Code zu beeinträchtigen, der dies verwendet Klassenobjekte? Diese Art der Modularisierung ist die zentrale Voraussetzung für die Objektorientierung und erleichtert die Wartung erheblich. Ohne das Ausblenden der Implementierung macht es wenig Sinn, andere OO-Funktionen zu verwenden.

Getter- und Setter-Methoden (auch als Accessoren bezeichnet) sind aus demselben Grund publicgefährlich wie Felder: Sie bieten externen Zugriff auf Implementierungsdetails. Was ist, wenn Sie den Typ des Zugriffsfelds ändern müssen? Sie müssen auch den Rückgabetyp des Accessors ändern. Sie verwenden diesen Rückgabewert an zahlreichen Stellen, daher müssen Sie auch den gesamten Code ändern. Ich möchte die Auswirkungen einer Änderung auf eine einzelne Klassendefinition beschränken. Ich möchte nicht, dass sie sich auf das gesamte Programm auswirken.

Da Accessoren gegen das Kapselungsprinzip verstoßen, können Sie vernünftigerweise argumentieren, dass ein System, das Accessoren stark oder unangemessen verwendet, einfach nicht objektorientiert ist. Wenn Sie einen Entwurfsprozess durchlaufen und nicht nur codieren, finden Sie in Ihrem Programm kaum Accessoren. Der Prozess ist wichtig. Ich habe zu diesem Thema am Ende des Artikels mehr zu sagen.

Das Fehlen von Getter / Setter-Methoden bedeutet nicht, dass einige Daten nicht durch das System fließen. Es ist jedoch am besten, die Datenbewegung so gering wie möglich zu halten. Ich habe die Erfahrung gemacht, dass die Wartbarkeit umgekehrt proportional zur Datenmenge ist, die sich zwischen Objekten bewegt. Obwohl Sie möglicherweise noch nicht sehen, wie, können Sie den größten Teil dieser Datenbewegung tatsächlich eliminieren.

Indem Sie sorgfältig entwerfen und sich darauf konzentrieren, was Sie tun müssen, anstatt wie Sie es tun, eliminieren Sie die überwiegende Mehrheit der Getter / Setter-Methoden in Ihrem Programm. Fragen Sie nicht nach den Informationen, die Sie für die Arbeit benötigen. Fragen Sie das Objekt, das die Informationen enthält, um die Arbeit für Sie zu erledigen.Die meisten Accessoren finden ihren Weg in den Code, weil die Designer nicht an das dynamische Modell gedacht haben: die Laufzeitobjekte und die Nachrichten, die sie sich gegenseitig senden, um die Arbeit zu erledigen. Sie beginnen (falsch) mit dem Entwerfen einer Klassenhierarchie und versuchen dann, diese Klassen in das dynamische Modell einzubinden. Dieser Ansatz funktioniert nie. Um ein statisches Modell zu erstellen, müssen Sie die Beziehungen zwischen den Klassen ermitteln. Diese Beziehungen entsprechen genau dem Nachrichtenfluss. Eine Zuordnung zwischen zwei Klassen besteht nur, wenn Objekte einer Klasse Nachrichten an Objekte der anderen Klasse senden. Der Hauptzweck des statischen Modells besteht darin, diese Zuordnungsinformationen beim dynamischen Modellieren zu erfassen.

Ohne ein klar definiertes dynamisches Modell raten Sie nur, wie Sie die Objekte einer Klasse verwenden. Infolgedessen werden Zugriffsmethoden häufig im Modell angezeigt, da Sie so viel Zugriff wie möglich bereitstellen müssen, da Sie nicht vorhersagen können, ob Sie ihn benötigen oder nicht. Diese Art von Design-by-Guessing-Strategie ist bestenfalls ineffizient. Sie verschwenden Zeit damit, nutzlose Methoden zu schreiben (oder den Klassen unnötige Funktionen hinzuzufügen).

Accessoren landen auch aus Gewohnheit in Designs. Wenn prozedurale Programmierer Java verwenden, beginnen sie damit, vertrauten Code zu erstellen. Prozedurale Sprachen haben keine Klassen, aber sie haben das C struct(denken Sie: Klasse ohne Methoden). Es erscheint daher naheliegend, a nachzuahmen, structindem Klassendefinitionen praktisch ohne Methoden und nur mit publicFeldern erstellt werden. Diese prozeduralen Programmierer lesen jedoch irgendwo, dass Felder sein sollten private, damit sie die Felder erstellen privateund publicZugriffsmethoden bereitstellen . Aber sie haben den öffentlichen Zugang nur erschwert. Sie haben das System sicherlich nicht objektorientiert gemacht.

Zeichne dich

Eine Auswirkung der vollständigen Feldkapselung liegt in der Erstellung der Benutzeroberfläche. Wenn Sie keine Accessoren verwenden können, kann eine UI-Builder-Klasse keine getAttribute()Methode aufrufen . Stattdessen haben Klassen Elemente wie drawYourself(...)Methoden.

Eine getIdentity()Methode kann natürlich auch funktionieren, vorausgesetzt, sie gibt ein Objekt zurück, das die IdentitySchnittstelle implementiert . Diese Schnittstelle muss eine Methode enthalten drawYourself()(oder mir eine JComponentMethode geben, die Ihre Identität darstellt). Obwohl getIdentitybeginnt mit „get“ , ist es nicht ein Accessor , weil es einfach kein Feld zurück. Es gibt ein komplexes Objekt mit angemessenem Verhalten zurück. Selbst wenn ich ein IdentityObjekt habe, habe ich keine Ahnung, wie eine Identität intern dargestellt wird.

Natürlich drawYourself()bedeutet eine Strategie, dass ich (nach Luft schnappen!) UI-Code in die Geschäftslogik eingefügt habe. Überlegen Sie, was passiert, wenn sich die Anforderungen der Benutzeroberfläche ändern. Angenommen, ich möchte das Attribut auf eine völlig andere Weise darstellen. Heute ist eine "Identität" ein Name; morgen ist es ein Name und eine ID-Nummer; am Tag danach ist es ein Name, eine ID-Nummer und ein Bild. Ich beschränke den Umfang dieser Änderungen auf eine Stelle im Code. Wenn ich eine JComponentKlasse habe, die Ihre Identität repräsentiert, habe ich die Art und Weise, wie Identitäten dargestellt werden, vom Rest des Systems isoliert.

Denken Sie daran, dass ich keinen UI-Code in die Geschäftslogik eingefügt habe. Ich habe die UI-Ebene in Form von AWT (Abstract Window Toolkit) oder Swing geschrieben, die beide Abstraktionsebenen sind. Der eigentliche UI-Code befindet sich in der AWT / Swing-Implementierung. Das ist der springende Punkt einer Abstraktionsschicht - Ihre Geschäftslogik von der Mechanik eines Subsystems zu isolieren. Ich kann problemlos auf eine andere grafische Umgebung portieren, ohne den Code zu ändern. Das einzige Problem ist also ein wenig Unordnung. Sie können diese Unordnung leicht beseitigen, indem Sie den gesamten UI-Code in eine innere Klasse verschieben (oder das Fassadenmuster verwenden).

JavaBeans

Sie könnten Einwände erheben, indem Sie sagen: "Aber was ist mit JavaBeans?" Was ist mit denen? Sie können JavaBeans sicherlich ohne Getter und Setter erstellen. Das BeanCustomizer, BeanInfound BeanDescriptoralle Klassen gibt es für genau diesen Zweck. Die JavaBean-Spezifikationsdesigner haben das Getter / Setter-Idiom ins Bild geworfen, weil sie dachten, es wäre eine einfache Möglichkeit, schnell eine Bohne herzustellen - etwas, das Sie tun können, während Sie lernen, wie man es richtig macht. Das hat leider niemand gemacht.

Accessoren wurden ausschließlich erstellt, um bestimmte Eigenschaften zu kennzeichnen, damit ein UI-Builder-Programm oder ein gleichwertiges Programm sie identifizieren kann. Sie sollten diese Methoden nicht selbst aufrufen. Sie sind für ein automatisiertes Tool vorhanden. Dieses Tool verwendet die Introspection-APIs in der ClassKlasse, um die Methoden zu finden und das Vorhandensein bestimmter Eigenschaften aus den Methodennamen zu extrapolieren. In der Praxis hat diese auf Selbstbeobachtung basierende Redewendung nicht funktioniert. Es hat den Code viel zu kompliziert und prozedural gemacht. Programmierer, die die Datenabstraktion nicht verstehen, rufen die Accessoren tatsächlich auf, und infolgedessen ist der Code weniger wartbar. Aus diesem Grund wird eine Metadatenfunktion in Java 1.5 integriert (voraussichtlich Mitte 2004). Also statt:

privates int Eigentum; public int getProperty () {Eigenschaft zurückgeben; } public void setProperty (int value} {property = value;}

Sie können Folgendes verwenden:

private @property int property; 

Das UI-Konstruktionstool oder ein gleichwertiges Tool verwendet die Introspection-APIs, um die Eigenschaften zu finden, anstatt Methodennamen zu untersuchen und die Existenz einer Eigenschaft aus einem Namen abzuleiten. Daher beschädigt kein Laufzeit-Accessor Ihren Code.

Wann ist ein Accessor in Ordnung?

Erstens ist es, wie bereits erwähnt, für eine Methode in Ordnung, ein Objekt in Bezug auf eine Schnittstelle zurückzugeben, die das Objekt implementiert, da diese Schnittstelle Sie von Änderungen an der implementierenden Klasse isoliert. Diese Art von Methode (die eine Schnittstellenreferenz zurückgibt) ist nicht wirklich ein "Getter" im Sinne einer Methode, die nur den Zugriff auf ein Feld ermöglicht. Wenn Sie die interne Implementierung des Anbieters ändern, ändern Sie einfach die Definition des zurückgegebenen Objekts, um die Änderungen zu berücksichtigen. Sie schützen weiterhin den externen Code, der das Objekt verwendet, über seine Schnittstelle.