Astuce Java 75: Utilisez des classes imbriquées pour une meilleure organisation

Un sous-système typique d'une application Java se compose d'un ensemble de classes et d'interfaces collaboratrices, chacune jouant un rôle spécifique. Certaines de ces classes et interfaces n'ont de sens que dans le contexte d'autres classes ou interfaces.

La conception de classes dépendant du contexte en tant que classes imbriquées de niveau supérieur (classes imbriquées, en abrégé) entourées par la classe de service de contexte rend cette dépendance plus claire. De plus, l'utilisation de classes imbriquées facilite la reconnaissance de la collaboration, évite la pollution des espaces de noms et réduit le nombre de fichiers source.

(Le code source complet de cette astuce peut être téléchargé au format zip à partir de la section Ressources.)

Classes imbriquées vs classes internes

Les classes imbriquées sont simplement des classes internes statiques. La différence entre les classes imbriquées et les classes internes est la même que la différence entre les membres statiques et non statiques d'une classe: les classes imbriquées sont associées à la classe englobante elle-même, tandis que les classes internes sont associées à un objet de la classe englobante.

Pour cette raison, les objets de classe interne nécessitent un objet de la classe englobante, contrairement aux objets de classe imbriqués. Les classes imbriquées, par conséquent, se comportent comme des classes de niveau supérieur, en utilisant la classe englobante pour fournir une organisation de type package. En outre, les classes imbriquées ont accès à tous les membres de la classe englobante.

Motivation

Prenons un sous-système Java typique, par exemple un composant Swing, utilisant le modèle de conception Model-View-Controller (MVC). Les objets événement encapsulent les notifications de modification du modèle. Les vues enregistrent l'intérêt pour divers événements en ajoutant des écouteurs au modèle sous-jacent du composant. Le modèle notifie à ses spectateurs les modifications de son propre état en fournissant ces objets d'événement à ses écouteurs enregistrés. Souvent, ces types d'écouteur et d'événement sont spécifiques au type de modèle et n'ont donc de sens que dans le contexte du type de modèle. Étant donné que chacun de ces types d'écouteur et d'événement doit être accessible au public, chacun doit se trouver dans son propre fichier source. Dans cette situation, à moins qu'une convention de codage ne soit utilisée, le couplage entre ces types est difficile à reconnaître. Bien sûr, on peut utiliser un emballage séparé pour chaque groupe pour montrer le couplage,mais cela se traduit par un grand nombre de paquets.

Si nous implémentons des types d'écouteur et d'événement en tant que types imbriqués de l'interface du modèle, nous rendons le couplage évident. Nous pouvons utiliser n'importe quel modificateur d'accès souhaité avec ces types imbriqués, y compris public. De plus, comme les types imbriqués utilisent l'interface englobante comme espace de noms, le reste du système y fait référence en .évitant la pollution de l'espace de noms à l'intérieur de ce package. Le fichier source de l'interface du modèle contient tous les types de prise en charge, ce qui facilite le développement et la maintenance.

Avant: un exemple sans classes imbriquées

A titre d'exemple, nous développons un composant simple Slate, dont la tâche est de dessiner des formes. Tout comme les composants Swing, nous utilisons le modèle de conception MVC. Le modèle SlateModel,, sert de référentiel pour les formes. SlateModelListeners souscrivez aux modifications du modèle. Le modèle notifie ses écouteurs en envoyant des événements de type SlateModelEvent. Dans cet exemple, nous avons besoin de trois fichiers source, un pour chaque classe:

// SlateModel.java import java.awt.Shape; interface publique SlateModel {// Gestion des écouteurs public void addSlateModelListener (SlateModelListener l); public void removeSlateModelListener (SlateModelListener l); // Gestion du référentiel de formes, les vues nécessitent une notification public void addShape (Shape s); public void removeShape (Shape s); public void removeAllShapes (); // Opérations en lecture seule du référentiel de formes public int getShapeCount (); public Shape getShapeAtIndex (index int); }
// SlateModelListener.java import java.util.EventListener; interface publique SlateModelListener étend EventListener {public void slateChanged (événement SlateModelEvent); }
// SlateModelEvent.java import java.util.EventObject; classe publique SlateModelEvent étend EventObject {public SlateModelEvent (modèle SlateModel) {super (modèle); }}

(Le code source de DefaultSlateModel, l'implémentation par défaut de ce modèle, se trouve dans le fichier avant / DefaultSlateModel.java.)

Ensuite, nous tournons notre attention Slate, en vue de ce modèle, qui transmet sa tâche peinture au délégué de l' interface utilisateur, SlateUI:

// Slate.java import javax.swing.JComponent; classe publique Slate étend JComponent implémente SlateModelListener {private SlateModel _model; public Slate (modèle SlateModel) {_model = modèle; _model.addSlateModelListener (this); setOpaque (vrai); setUI (nouveau SlateUI ()); } public Slate () {this (nouveau DefaultSlateModel ()); } public SlateModel getModel () {return _model; } // Implémentation de l'écouteur public void slateChanged (événement SlateModelEvent) {repaint (); }}

Enfin, SlateUIle composant d'interface graphique visuelle:

// SlateUI.java import java.awt. *; import javax.swing.JComponent; import javax.swing.plaf.ComponentUI; public class SlateUI étend ComponentUI {public void paint (Graphics g, JComponent c) {SlateModel model = ((Slate) c) .getModel (); g.setColor (c.getForeground ()); Graphics2D g2D = (Graphics2D) g; for (int size = model.getShapeCount (), i = 0; i <size; i ++) {g2D.draw (model.getShapeAtIndex (i)); }}}

Après: un exemple modifié utilisant des classes imbriquées

La structure de classe dans l'exemple ci-dessus ne montre pas la relation entre les classes. Pour atténuer cela, nous avons utilisé une convention de dénomination qui exige que toutes les classes associées aient un préfixe commun, mais il serait plus clair d'afficher la relation dans le code. De plus, les développeurs et les mainteneurs de ces classes doivent gérer trois fichiers: pour SlateModel, pour SlateEventet pour SlateListener, pour implémenter un concept. Il en va de même pour la gestion des deux fichiers pour Slateet SlateUI.

Nous pouvons améliorer les choses en créant SlateModelListeneret en SlateModelEventimbriquant des types d' SlateModelinterface. Étant donné que ces types imbriqués se trouvent à l'intérieur d'une interface, ils sont implicitement statiques. Néanmoins, nous avons utilisé une déclaration statique explicite pour aider le programmeur de maintenance.

Le code client les appellera SlateModel.SlateModelListeneret SlateModel.SlateModelEvent, mais cela est redondant et inutilement long. Nous supprimons le préfixe SlateModeldes classes imbriquées. Avec ce changement, le code client les appellera SlateModel.Listeneret SlateModel.Event. Ceci est bref et clair et ne dépend pas des normes de codage.

Car SlateUI, nous faisons la même chose - nous en faisons une classe imbriquée de Slateet changeons son nom en UI. Comme il s'agit d'une classe imbriquée dans une classe (et non dans une interface), nous devons utiliser un modificateur statique explicite.

Avec ces modifications, nous n'avons besoin que d'un seul fichier pour les classes liées au modèle et d'un autre pour les classes liées à la vue. Le SlateModelcode devient maintenant:

// SlateModel.java import java.awt.Shape; import java.util.EventListener; import java.util.EventObject; interface publique SlateModel {// Gestion des écouteurs public void addSlateModelListener (SlateModel.Listener l); public void removeSlateModelListener (SlateModel.Listener l); // Gestion du référentiel de formes, les vues nécessitent une notification public void addShape (Shape s); public void removeShape (Shape s); public void removeAllShapes (); // Opérations en lecture seule du référentiel de formes public int getShapeCount (); public Shape getShapeAtIndex (index int); // Classes et interfaces imbriquées de niveau supérieur associées interface publique Listener extend EventListener {public void slateChanged (SlateModel.Event event); } classe publique Event s'étend EventObject {Evénement public (modèle SlateModel) {super (modèle); }}}

Et le code pour Slateest changé en:

// Slate.java import java.awt. *; import javax.swing.JComponent; import javax.swing.plaf.ComponentUI; classe publique Slate étend JComponent implémente SlateModel.Listener {public Slate (SlateModel model) {_model = model; _model.addSlateModelListener (this); setOpaque (vrai); setUI (nouveau Slate.UI ()); } public Slate () {this (nouveau DefaultSlateModel ()); } public SlateModel getModel () {return _model; } // Implémentation d'écouteur public void slateChanged (événement SlateModel.Event) {repaint (); } La classe publique statique UI étend ComponentUI {public void paint (Graphics g, JComponent c) {SlateModel model = ((Slate) c) .getModel (); g.setColor (c.getForeground ()); Graphics2D g2D = (Graphics2D) g; for (int size = model.getShapeCount (), i = 0; i <size; i ++) {g2D.draw (model.getShapeAtIndex (i)); }}}}

(Le code source de l'implémentation par défaut du modèle modifié DefaultSlateModel, se trouve dans le fichier après / DefaultSlateModel.java.)

Dans la SlateModelclasse, il n'est pas nécessaire d'utiliser des noms complets pour les classes et interfaces imbriquées. Par exemple, juste Listenersuffirait à la place de SlateModel.Listener. Cependant, l'utilisation de noms complets aide les développeurs qui copient des signatures de méthode à partir de l'interface et les collent dans des classes d'implémentation.

Le JFC et l'utilisation des classes imbriquées

La bibliothèque JFC utilise des classes imbriquées dans certains cas. Par exemple, class BasicBordersin package javax.swing.plaf.basicdéfinit plusieurs classes imbriquées telles que BasicBorders.ButtonBorder. Dans ce cas, la classe BasicBordersn'a pas d'autres membres et agit simplement comme un package. Utiliser un package séparé à la place aurait été tout aussi efficace, sinon plus approprié. Il s'agit d'une utilisation différente de celle présentée dans cet article.

L'utilisation de l'approche de cette astuce dans la conception JFC affecterait l'organisation des types d'écouteurs et d'événements liés aux types de modèles. Par exemple, javax.swing.event.TableModelListeneret javax.swing.event.TableModelEventserait implémenté respectivement comme une interface imbriquée et une classe imbriquée à l'intérieur 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 est un architecte certifié Sun de la technologie Java (Java 2). Il est titulaire d'une maîtrise en génie électrique avec une spécialisation en génie de la communication. Il a six ans d'expérience dans la conception et le développement de plusieurs projets logiciels impliquant une interface graphique, des réseaux et des systèmes distribués. Il a développé des systèmes logiciels orientés objet en Java au cours des deux dernières années et en C ++ au cours des cinq dernières années. Ramnivas travaille actuellement chez Real-Time Innovations Inc. en tant qu'ingénieur logiciel. Chez RTI, il travaille actuellement à la conception et au développement de ControlShell, le cadre de programmation basé sur des composants pour la construction de systèmes complexes en temps réel.