Mehr zu Getter und Setter

Es ist ein 25 Jahre altes Prinzip des objektorientierten Designs (OO), dass Sie die Implementierung eines Objekts keiner anderen Klasse im Programm aussetzen sollten. Das Programm ist unnötig schwierig zu warten, wenn Sie die Implementierung verfügbar machen, vor allem, weil das Ändern eines Objekts, das seine Implementierung verfügbar macht, Änderungen an allen Klassen erfordert, die das Objekt verwenden.

Leider verstößt die Getter / Setter-Sprache, die viele Programmierer als objektorientiert betrachten, gegen dieses grundlegende OO-Prinzip. Betrachten Sie das Beispiel einer MoneyKlasse mit einer getValue()Methode, die den "Wert" in Dollar zurückgibt. Sie haben Code wie den folgenden in Ihrem gesamten Programm:

double orderTotal; Geldbetrag = ...; // ... orderTotal + = amount.getValue (); // orderTotal muss in Dollar sein

Das Problem bei diesem Ansatz besteht darin, dass der vorstehende Code eine große Annahme darüber macht, wie die MoneyKlasse implementiert ist (dass der "Wert" in a gespeichert ist double). Code, der Implementierungsannahmen macht, bricht ab, wenn sich die Implementierung ändert. Wenn Sie beispielsweise Ihre Anwendung internationalisieren müssen, um andere Währungen als Dollar zu unterstützen, wird getValue()nichts Sinnvolles zurückgegeben. Sie könnten ein hinzufügen getCurrency(), aber das würde den gesamten Code, der den getValue()Aufruf umgibt , viel komplizierter machen, insbesondere wenn Sie weiterhin die Getter / Setter-Strategie verwenden, um die Informationen zu erhalten, die Sie für die Arbeit benötigen. Eine typische (fehlerhafte) Implementierung könnte folgendermaßen aussehen:

Geldbetrag = ...; // ... value = amount.getValue (); Währung = Betrag.getCurrency (); convert = CurrencyTable.getConversionFactor (Währung, USDOLLAR); gesamt + = Wert * Umwandlung; // ...

Diese Änderung ist zu kompliziert, um durch automatisiertes Refactoring behandelt zu werden. Darüber hinaus müssten Sie diese Art von Änderungen überall in Ihrem Code vornehmen.

Die Lösung für dieses Problem auf Geschäftslogikebene besteht darin, die Arbeit in dem Objekt auszuführen, das über die für die Ausführung der Arbeit erforderlichen Informationen verfügt. Anstatt den "Wert" zu extrahieren, um eine externe Operation auszuführen, sollte die MoneyKlasse alle geldbezogenen Operationen ausführen, einschließlich der Währungsumrechnung. Ein richtig strukturiertes Objekt würde die Summe wie folgt behandeln:

Geldsumme = ...; Geldbetrag = ...; total.increaseBy (Betrag);

Die add()Methode würde die Währung des Operanden ermitteln, alle erforderlichen Währungsumrechnungen durchführen (was eigentlich eine Operation mit Geld ist ) und die Gesamtsumme aktualisieren. Wenn Sie diese Strategie verwendet haben, bei der die Informationen die Arbeit erledigen, kann der Begriff der Währung der MoneyKlasse hinzugefügt werden, ohne dass Änderungen am Code erforderlich sind, der MoneyObjekte verwendet. Das heißt, die Arbeit, einen Dollar nur für eine internationale Implementierung umzugestalten, würde sich auf einen einzigen Ort konzentrieren: die MoneyKlasse.

Das Problem

Die meisten Programmierer haben keine Schwierigkeiten, dieses Konzept auf der Ebene der Geschäftslogik zu verstehen (obwohl es einige Anstrengungen erfordern kann, konsequent so zu denken). Probleme treten jedoch auf, wenn die Benutzeroberfläche (UI) das Bild betritt. Das Problem ist nicht, dass Sie keine Techniken wie die, die ich gerade beschrieben habe, anwenden können, um eine Benutzeroberfläche zu erstellen, sondern dass viele Programmierer in Bezug auf Benutzeroberflächen an eine Getter / Setter-Mentalität gebunden sind. Ich beschuldige dieses Problem grundlegend prozeduralen Code-Konstruktionswerkzeugen wie Visual Basic und seinen Klonen (einschließlich der Java UI-Builder), die Sie zu dieser prozeduralen Denkweise zwingen.

(Exkurs: Einige von Ihnen werden sich der vorherigen Aussage widersetzen und schreien, dass VB auf der heiligen Model-View-Controller-Architektur (MVC) basiert, ebenso wie Sakrosankt. Denken Sie daran, dass MVC vor fast 30 Jahren entwickelt wurde In den 1970er Jahren war der größte Supercomputer mit den heutigen Desktops vergleichbar. Die meisten Computer (wie der DEC PDP-11) waren 16-Bit-Computer mit 64 KB Speicher und Taktraten in zehn Megahertz. Ihre Benutzeroberfläche war wahrscheinlich eine Stapel Lochkarten. Wenn Sie das Glück hatten, ein Videoterminal zu haben, haben Sie möglicherweise ein ASCII-basiertes E / A-System (Console Input / Output) verwendet. Wir haben in den letzten 30 Jahren viel gelernt Java Swing musste MVC durch eine ähnliche "trennbare Modell" -Architektur ersetzen, vor allem, weil reine MVC die UI- und Domänenmodellschichten nicht ausreichend isoliert.)

Definieren wir das Problem auf den Punkt gebracht:

Wenn ein Objekt möglicherweise keine Implementierungsinformationen verfügbar macht (durch get / set-Methoden oder auf andere Weise), liegt es nahe, dass ein Objekt irgendwie seine eigene Benutzeroberfläche erstellen muss. Das heißt, wenn die Art und Weise, wie die Attribute eines Objekts dargestellt werden, vor dem Rest des Programms verborgen ist, können Sie diese Attribute nicht extrahieren, um eine Benutzeroberfläche zu erstellen.

Beachten Sie übrigens, dass Sie die Tatsache, dass ein Attribut vorhanden ist, nicht verbergen. (Ich definiere hier ein Attribut als ein wesentliches Merkmal des Objekts.) Sie wissen, dass ein EmployeeAttribut ein Gehalt oder einen Lohn haben muss, sonst wäre es kein Employee. (Es wäre ein Person, ein Volunteer, ein Vagrantoder etwas anderes, das kein Gehalt hat.) Was Sie nicht wissen - oder wissen wollen - ist, wie dieses Gehalt im Objekt dargestellt wird. Es kann eine double, eine String, eine skalierte longoder eine binär codierte Dezimalstelle sein. Es kann sich um ein "synthetisches" oder "abgeleitetes" Attribut handeln, das zur Laufzeit berechnet wird (z. B. anhand einer Gehaltsstufe oder einer Berufsbezeichnung oder durch Abrufen des Werts aus einer Datenbank). Obwohl eine get-Methode tatsächlich einige dieser Implementierungsdetails verbergen kann,wie wir mit dem gesehen habenMoney Zum Beispiel kann es nicht genug verstecken.

Wie erzeugt ein Objekt seine eigene Benutzeroberfläche und bleibt wartbar? Nur die einfachsten Objekte können so etwas wie eine displayYourself()Methode unterstützen. Realistische Objekte müssen:

  • Zeigen Sie sich in verschiedenen Formaten an (XML, SQL, durch Kommas getrennte Werte usw.).
  • Zeigen Sie verschiedene Ansichten von sich selbst an (eine Ansicht zeigt möglicherweise alle Attribute an, eine andere zeigt möglicherweise nur eine Teilmenge der Attribute an und eine dritte zeigt die Attribute möglicherweise auf andere Weise an).
  • Zeigen Sie sich in verschiedenen Umgebungen an ( JComponentz. B. clientseitig ( ) und bedient an Client (HTML)) und verarbeiten Sie sowohl die Eingabe als auch die Ausgabe in beiden Umgebungen.

Einige der Leser meines vorherigen Getter / Setter-Artikels kamen zu dem Schluss, dass ich befürwortete, dass Sie dem Objekt Methoden hinzufügen, um all diese Möglichkeiten abzudecken, aber dass "Lösung" offensichtlich unsinnig ist. Das resultierende schwere Objekt ist nicht nur viel zu kompliziert, Sie müssen es auch ständig ändern, um neuen Anforderungen an die Benutzeroberfläche gerecht zu werden. Praktisch kann ein Objekt einfach nicht alle möglichen Benutzeroberflächen für sich selbst erstellen, wenn aus keinem anderen Grund als vielen dieser Benutzeroberflächen nicht einmal konzipiert wurde, als die Klasse erstellt wurde.

Erstellen Sie eine Lösung

Die Lösung dieses Problems besteht darin, den UI-Code vom Kerngeschäftsobjekt zu trennen, indem er in eine separate Objektklasse eingefügt wird. Das heißt, Sie sollten einige Funktionen, die sich im Objekt befinden könnten , vollständig in ein separates Objekt aufteilen .

Diese Aufteilung der Methoden eines Objekts tritt in mehreren Entwurfsmustern auf. Sie sind höchstwahrscheinlich mit Strategie vertraut, die mit den verschiedenen java.awt.ContainerKlassen für das Layout verwendet wird. Sie könnten das Layout Problem mit einer Ableitung Lösung lösen: FlowLayoutPanel, GridLayoutPanel, BorderLayoutPaneletc., aber das Mandate zu viele Klassen und viel duplizierten Code in diesen Klassen. Eine einzige Schwergewichts-Klasse - Lösung (Methoden hinzufügen Containermögen layOutAsGrid(), layOutAsFlow()usw.) ist auch nicht praktikabel , weil Sie nicht den Quellcode für die ändern können , Containernur weil Sie ein nicht unterstütztes Layout benötigen. In der Strategie - Muster erstellen Sie eine StrategySchnittstelle ( LayoutManager) von mehreren implementierten Concrete StrategyKlassen ( FlowLayout, GridLayoutusw.). Sie erzählen dann ein ContextObjekt (aContainer) wie man etwas macht, indem man ihm ein StrategyObjekt übergibt . (Sie übergeben ein Containera LayoutManager, das eine Layoutstrategie definiert.)

Das Builder-Muster ähnelt der Strategie. Der Hauptunterschied besteht darin, dass die BuilderKlasse eine Strategie zum Erstellen von JComponentObjekten implementiert (z. B. einen oder einen XML-Stream, der den Status eines Objekts darstellt). BuilderObjekte erstellen ihre Produkte normalerweise auch in einem mehrstufigen Prozess. Das heißt, Aufrufe verschiedener Methoden von Buildersind erforderlich, um den Konstruktionsprozess abzuschließen, und der Builderkennt normalerweise nicht die Reihenfolge, in der die Aufrufe ausgeführt werden, oder die Häufigkeit, mit der eine seiner Methoden aufgerufen wird. Das wichtigste Merkmal des Builders ist, dass das Geschäftsobjekt (das so genannte Context) nicht genau weiß, was das BuilderObjekt erstellt. Das Muster isoliert das Geschäftsobjekt von seiner Darstellung.

Der beste Weg, um zu sehen, wie ein einfacher Builder funktioniert, ist ein Blick auf einen. Schauen wir uns zunächst Contextdas Geschäftsobjekt an, das eine Benutzeroberfläche verfügbar machen muss. Listing 1 zeigt eine vereinfachte EmployeeKlasse. Das Employeehat name, idund salaryAttribute. (Stubs für diese Klassen befinden sich am Ende der Liste, aber diese Stubs sind nur Platzhalter für die reale Sache. Sie können sich - ich hoffe - leicht vorstellen, wie diese Klassen funktionieren würden.)

Dies verwendet insbesondere Contextdas, was ich als bidirektionalen Builder betrachte. Der klassische Gang of Four Builder geht in eine Richtung (Ausgabe), aber ich habe auch eine hinzugefügt Builder, mit der sich ein EmployeeObjekt initialisieren kann. Es Buildersind zwei Schnittstellen erforderlich. Die Employee.ExporterSchnittstelle (Listing 1, Zeile 8) übernimmt die Ausgaberichtung. Es definiert eine Schnittstelle zu einem BuilderObjekt, die die Darstellung des aktuellen Objekts erstellt. Der Employeedelegiert die eigentliche UI-Konstruktion an die Builderin der export()Methode (in Zeile 31). Das Builderwird nicht an die tatsächlichen Felder übergeben, sondern verwendet Strings, um eine Darstellung dieser Felder zu übergeben.

Listing 1. Mitarbeiter: Der Builder-Kontext

1 import java.util.Locale; 2 3 öffentliche Klasse Mitarbeiter 4 {privater Name Name; 5 private EmployeeId id; 6 privates Geldgehalt; 7 8 öffentliche Schnittstelle Exporter 9 {void addName (String name); 10 void addID (String id); 11 void addSalary (String-Gehalt); 12} 13 14 öffentliche Schnittstelle Importer 15 {String requireName (); 16 String providID (); 17 String requireSalary (); 18 void open (); 19 void close (); 20} 21 22 öffentlicher Mitarbeiter (Importer Builder) 23 {builder.open (); 24 this.name = neuer Name (builder.provideName ()); 25 this.id = new EmployeeId (builder.provideID ()); 26 this.salary = neues Geld (builder.provideSalary (), 27 neues Gebietsschema ("en", "US")); 28 builder.close (); 29} 30 31 public void export (Exporter-Builder) 32 {builder.addName (name.toString ()); 33 builder.addID (id.toString ()); 34 builder.addSalary (Gehalt.toString ()); 35} 36 37// ... 38} 39 // ------------------------------------ ------------------------------ 40 // Unit-Test-Zeug 41 // 42 Klassenname 43 {privater String-Wert; 44 public Name (String value) 45 {this.value = value; 46} 47 public String toString () {Rückgabewert; }; 48} 49 50 class EmployeeId 51 {privater String-Wert; 52 public EmployeeId (String value) 53 {this.value = value; 54} 55 public String toString () {Rückgabewert; } 56} 57 58 Klasse Money 59 {privater String-Wert; 60 öffentliches Geld (Zeichenfolgenwert, Gebietsschema) 61 {this.value = value; 62} 63 public String toString () {Rückgabewert; } 64}

Schauen wir uns ein Beispiel an. Der folgende Code erstellt die Benutzeroberfläche von Abbildung 1:

Mitarbeiter wilma = ...; JComponentExporter uiBuilder = neuer JComponentExporter (); // Erstelle den Builder wilma.export (uiBuilder); // Benutzeroberfläche erstellen JComponent userInterface = uiBuilder.getJComponent (); // ... someContainer.add (userInterface);

Listing 2 zeigt die Quelle für die JComponentExporter. Wie Sie sehen können, konzentriert sich der gesamte UI-bezogene Code auf das Concrete Builder(the JComponentExporter), und das Context(the Employee) steuert den Erstellungsprozess, ohne genau zu wissen, was er erstellt.

Listing 2. Exportieren auf eine clientseitige Benutzeroberfläche

1 import javax.swing. *; 2 import java.awt. *; 3 import java.awt.event. *; 4 5 Klasse JComponentExporter implementiert Employee.Exporter 6 {privater Stringname, ID, Gehalt; 7 8 public void addName (String name) {this.name = name; } 9 public void addID (String id) {this.id = id; } 10 public void addSalary (String-Gehalt) {this.salary = Gehalt; } 11 12 JComponent getJComponent () 13 {JComponent panel = new JPanel (); 14 panel.setLayout (neues GridLayout (3,2)); 15 panel.add (neues JLabel ("Name:")); 16 panel.add (neues JLabel (Name)); 17 panel.add (neues JLabel ("Employee ID:")); 18 panel.add (neues JLabel (id)); 19 panel.add (neues JLabel ("Gehalt:")); 20 panel.add (neues JLabel (Gehalt)); 21 Rücklaufplatte; 22} 23}