Java-Tipp 75: Verwenden Sie verschachtelte Klassen für eine bessere Organisation

Ein typisches Subsystem in einer Java-Anwendung besteht aus einer Reihe von zusammenarbeitenden Klassen und Schnittstellen, die jeweils eine bestimmte Rolle spielen. Einige dieser Klassen und Schnittstellen sind nur im Kontext anderer Klassen oder Schnittstellen von Bedeutung.

Das Entwerfen kontextabhängiger Klassen als verschachtelte Klassen der obersten Ebene (kurz verschachtelte Klassen), die von der kontextservierenden Klasse eingeschlossen werden, macht diese Abhängigkeit klarer. Darüber hinaus erleichtert die Verwendung verschachtelter Klassen die Erkennung der Zusammenarbeit, vermeidet die Verschmutzung von Namespaces und reduziert die Anzahl der Quelldateien.

(Der vollständige Quellcode für diesen Tipp kann im Zip-Format im Abschnitt Ressourcen heruntergeladen werden.)

Verschachtelte Klassen gegen innere Klassen

Verschachtelte Klassen sind einfach statische innere Klassen. Der Unterschied zwischen verschachtelten Klassen und inneren Klassen ist der gleiche wie der Unterschied zwischen statischen und nicht statischen Mitgliedern einer Klasse: Verschachtelte Klassen sind der einschließenden Klasse selbst zugeordnet, während innere Klassen einem Objekt der einschließenden Klasse zugeordnet sind.

Aus diesem Grund benötigen Objekte der inneren Klasse ein Objekt der umschließenden Klasse, verschachtelte Klassenobjekte nicht. Verschachtelte Klassen verhalten sich daher wie Klassen der obersten Ebene und verwenden die einschließende Klasse, um eine paketähnliche Organisation bereitzustellen. Darüber hinaus haben verschachtelte Klassen Zugriff auf alle Mitglieder der einschließenden Klasse.

Motivation

Stellen Sie sich ein typisches Java-Subsystem vor, beispielsweise eine Swing-Komponente, die das MVC-Entwurfsmuster (Model-View-Controller) verwendet. Ereignisobjekte kapseln Änderungsbenachrichtigungen aus dem Modell. Ansichten registrieren das Interesse an verschiedenen Ereignissen, indem sie dem zugrunde liegenden Modell der Komponente Listener hinzufügen. Das Modell benachrichtigt seine Betrachter über Änderungen in seinem eigenen Status, indem es diese Ereignisobjekte an seine registrierten Listener übermittelt. Diese Listener- und Ereignistypen sind häufig spezifisch für den Modelltyp und daher nur im Kontext des Modelltyps sinnvoll. Da jeder dieser Listener- und Ereignistypen öffentlich zugänglich sein muss, muss sich jeder in einer eigenen Quelldatei befinden. In dieser Situation ist die Kopplung zwischen diesen Typen schwer zu erkennen, sofern keine Codierungskonvention verwendet wird. Natürlich kann man für jede Gruppe ein separates Paket verwenden, um die Kopplung zu zeigen.Dies führt jedoch zu einer großen Anzahl von Paketen.

Wenn wir Listener- und Ereignistypen als verschachtelte Typen der Modellschnittstelle implementieren, machen wir die Kopplung offensichtlich. Wir können jeden gewünschten Zugriffsmodifikator für diese verschachtelten Typen verwenden, einschließlich public. Da verschachtelte Typen die umschließende Schnittstelle als Namespace verwenden, werden sie vom Rest des Systems als .Namespace bezeichnet , um eine Verschmutzung des Namespaces in diesem Paket zu vermeiden. Die Quelldatei für die Modellschnittstelle enthält alle unterstützenden Typen, was die Entwicklung und Wartung erleichtert.

Vorher: Ein Beispiel ohne verschachtelte Klassen

Als Beispiel entwickeln wir eine einfache Komponente Slate, deren Aufgabe es ist, Formen zu zeichnen. Genau wie bei Swing-Komponenten verwenden wir das MVC-Entwurfsmuster. Das Modell SlateModeldient als Aufbewahrungsort für Formen. SlateModelListeners Abonnieren Sie die Änderungen im Modell. Das Modell benachrichtigt seine Listener durch Senden von Ereignissen vom Typ SlateModelEvent. In diesem Beispiel benötigen wir drei Quelldateien, eine für jede Klasse:

// SlateModel.java import java.awt.Shape; öffentliche Schnittstelle SlateModel {// Listener-Verwaltung public void addSlateModelListener (SlateModelListener l); public void removeSlateModelListener (SlateModelListener l); // Shape Repository Management, Ansichten müssen benachrichtigt werden public void addShape (Shape s); public void removeShape (Shape s); public void removeAllShapes (); // Schreibgeschützte Operationen des Formrepositorys public int getShapeCount (); public Shape getShapeAtIndex (int index); }}
// SlateModelListener.java import java.util.EventListener; öffentliche Schnittstelle SlateModelListener erweitert EventListener {public void slateChanged (SlateModelEvent-Ereignis); }}
// SlateModelEvent.java import java.util.EventObject; öffentliche Klasse SlateModelEvent erweitert EventObject {public SlateModelEvent (SlateModel-Modell) {super (Modell); }}

(Der Quellcode für DefaultSlateModel, die Standardimplementierung für dieses Modell, befindet sich in der Datei vor / DefaultSlateModel.java.)

Als nächstes wenden wir uns Slateeiner Ansicht für dieses Modell zu, die seine Malaufgabe an den UI-Delegierten weiterleitet SlateUI:

// Slate.java import javax.swing.JComponent; öffentliche Klasse Slate erweitert JComponent implementiert SlateModelListener {private SlateModel _model; public Slate (SlateModel-Modell) {_model = model; _model.addSlateModelListener (this); setOpaque (true); setUI (neues SlateUI ()); } public Slate () {this (neues DefaultSlateModel ()); } public SlateModel getModel () {return _model; } // Listener-Implementierung public void slateChanged (SlateModelEvent-Ereignis) {repaint (); }}

Schließlich SlateUIdie visuelle GUI-Komponente:

// SlateUI.java import java.awt. *; import javax.swing.JComponent; import javax.swing.plaf.ComponentUI; öffentliche Klasse SlateUI erweitert ComponentUI {public void paint (Grafik g, JComponent c) {SlateModel model = ((Slate) c) .getModel (); g.setColor (c.getForeground ()); Grafik2D g2D = (Grafik2D) g; für (int size = model.getShapeCount (), i = 0; i <size; i ++) {g2D.draw (model.getShapeAtIndex (i)); }}}

Nachher: ​​Ein modifiziertes Beispiel mit verschachtelten Klassen

Die Klassenstruktur im obigen Beispiel zeigt nicht die Beziehung zwischen den Klassen. Um dies zu mildern, haben wir eine Namenskonvention verwendet, bei der alle zugehörigen Klassen ein gemeinsames Präfix haben müssen. Es wäre jedoch klarer, die Beziehung im Code anzuzeigen. Darüber hinaus müssen Entwickler und Betreuer dieser Klassen drei Dateien verwalten: für SlateModel, für SlateEventund für SlateListenerdie Implementierung eines Konzepts. Gleiches gilt für die Verwaltung der beiden Dateien für Slateund SlateUI.

Wir können Dinge verbessern, indem wir Typen der Schnittstelle erstellen SlateModelListenerund SlateModelEventverschachteln SlateModel. Da sich diese verschachtelten Typen in einer Schnittstelle befinden, sind sie implizit statisch. Trotzdem haben wir eine explizite statische Deklaration verwendet, um dem Wartungsprogrammierer zu helfen.

Client-Code bezeichnet sie als SlateModel.SlateModelListenerund SlateModel.SlateModelEvent, dies ist jedoch redundant und unnötig lang. Wir entfernen das Präfix SlateModelaus den verschachtelten Klassen. Mit dieser Änderung verweist der Client-Code auf sie als SlateModel.Listenerund SlateModel.Event. Dies ist kurz und klar und hängt nicht von Codierungsstandards ab.

Denn SlateUIwir machen dasselbe - wir machen es zu einer verschachtelten Klasse Slateund ändern ihren Namen in UI. Da es sich um eine verschachtelte Klasse innerhalb einer Klasse handelt (und nicht innerhalb einer Schnittstelle), müssen wir einen expliziten statischen Modifikator verwenden.

Mit diesen Änderungen benötigen wir nur eine Datei für die modellbezogenen Klassen und eine weitere für die ansichtsbezogenen Klassen. Der SlateModelCode wird jetzt:

// SlateModel.java import java.awt.Shape; import java.util.EventListener; import java.util.EventObject; öffentliche Schnittstelle SlateModel {// Listener-Verwaltung public void addSlateModelListener (SlateModel.Listener l); public void removeSlateModelListener (SlateModel.Listener l); // Shape Repository Management, Ansichten müssen benachrichtigt werden public void addShape (Shape s); public void removeShape (Shape s); public void removeAllShapes (); // Schreibgeschützte Operationen des Formrepositorys public int getShapeCount (); public Shape getShapeAtIndex (int index); // Verwandte verschachtelte Klassen und Schnittstellen der obersten Ebene öffentliche Schnittstelle Listener erweitert EventListener {public void slateChanged (SlateModel.Event-Ereignis); } public class Event erweitert EventObject {public Event (SlateModel-Modell) {super (Modell); }}}

Und der Code für Slatewird geändert in:

// Slate.java import java.awt. *; import javax.swing.JComponent; import javax.swing.plaf.ComponentUI; öffentliche Klasse Slate erweitert JComponent implementiert SlateModel.Listener {public Slate (SlateModel-Modell) {_model = model; _model.addSlateModelListener (this); setOpaque (true); setUI (new Slate.UI ()); } public Slate () {this (neues DefaultSlateModel ()); } public SlateModel getModel () {return _model; } // Listener-Implementierung public void slateChanged (SlateModel.Event-Ereignis) {repaint (); } Benutzeroberfläche der öffentlichen statischen Klasse erweitert ComponentUI {public void paint (Grafik g, JComponent c) {SlateModel model = ((Slate) c) .getModel (); g.setColor (c.getForeground ()); Grafik2D g2D = (Grafik2D) g; für (int size = model.getShapeCount (), i = 0; i <size; i ++) {g2D.draw (model.getShapeAtIndex (i)); }}}}

(Der Quellcode für die Standardimplementierung für das geänderte Modell DefaultSlateModelbefindet sich in der Datei nach / DefaultSlateModel.java.)

Innerhalb der SlateModelKlasse müssen keine vollständig qualifizierten Namen für verschachtelte Klassen und Schnittstellen verwendet werden. Zum Beispiel Listenerwürde nur anstelle von ausreichen SlateModel.Listener. Die Verwendung vollständig qualifizierter Namen hilft Entwicklern jedoch, Methodensignaturen von der Schnittstelle zu kopieren und in implementierende Klassen einzufügen.

Der JFC und die Verwendung verschachtelter Klassen

Die JFC-Bibliothek verwendet in bestimmten Fällen verschachtelte Klassen. Beispielsweise definiert die Klasse BasicBordersim Paket javax.swing.plaf.basicmehrere verschachtelte Klassen wie z BasicBorders.ButtonBorder. In diesem Fall hat die Klasse BasicBorderskeine anderen Mitglieder und fungiert einfach als Paket. Die Verwendung eines separaten Pakets wäre ebenso effektiv gewesen, wenn nicht angemessener. Dies ist eine andere Verwendung als die in diesem Artikel vorgestellte.

Die Verwendung des Ansatzes dieses Tipps im JFC-Design würde sich auf die Organisation von Listener- und Ereignistypen in Bezug auf Modelltypen auswirken. Zum Beispiel javax.swing.event.TableModelListenerund javax.swing.event.TableModelEventwürde jeweils als verschachtelte Schnittstelle und als verschachtelte Klasse im Inneren implementiert javax.swing.table.TableModel.

This change, together with shortening the names, would result in a listener interface named javax.swing.table.TableModel.Listener and an event class named javax.swing.table.TableModel.Event. TableModel would then be fully self-contained with all the necessary support classes and interfaces rather than having need of support classes and interface spread out over three files and two packages.

Guidelines for using nested classes

As with any other pattern, judicious use of nested classes results in design that is simpler and more easily understood than traditional package organization. However, incorrect usage leads to unnecessary coupling, which makes the role of nested classes unclear.

Note that in the nested example above, we make use of nested types only for types that cannot stand without context of enclosing type. We do not, for example, make SlateModel a nested interface of Slate because there may be other view types using the same model.

Given any two classes, apply the following guidelines to decide if you should use nested classes. Use nested classes to organize your classes only if the answer to both questions below is yes:

  1. Is it possible to clearly classify one of the classes as the primary class and the other as a supporting class?

  2. Is the supporting class meaningless if the primary class is removed from the subsystem?

Conclusion

The pattern of using nested classes couples the related types tightly. It avoids namespace pollution by using the enclosing type as namespace. It results in fewer source files, without losing the ability to publicly expose supporting types.

As with any other pattern, use this pattern judiciously. In particular, ensure that nested types are truly related and have no meaning without the context of the enclosing type. Correct usage of the pattern doesn't increase coupling, but merely clarifies the existent coupling.

Ramnivas Laddad ist ein Sun Certified Architect für Java-Technologie (Java 2). Er hat einen Master-Abschluss in Elektrotechnik mit Spezialisierung auf Kommunikationstechnik. Er verfügt über sechs Jahre Erfahrung im Entwerfen und Entwickeln mehrerer Softwareprojekte, die GUI, Netzwerke und verteilte Systeme umfassen. Er hat in den letzten zwei Jahren objektorientierte Softwaresysteme in Java und in den letzten fünf Jahren in C ++ entwickelt. Ramnivas arbeitet derzeit als Softwareentwickler bei Real-Time Innovations Inc. Derzeit arbeitet er bei RTI am Design und der Entwicklung von ControlShell, dem komponentenbasierten Programmierframework zum Erstellen komplexer Echtzeitsysteme.