Consejo 75 de Java: use clases anidadas para una mejor organización

Un subsistema típico en una aplicación Java consiste en un conjunto de clases e interfaces colaboradoras, cada una de las cuales desempeña una función específica. Algunas de estas clases e interfaces son significativas solo en el contexto de otras clases o interfaces.

El diseño de clases dependientes del contexto como clases anidadas de nivel superior (clases anidadas, para abreviar) encerradas por la clase de servicio de contexto hace que esta dependencia sea más clara. Además, el uso de clases anidadas hace que la colaboración sea más fácil de reconocer, evita la contaminación del espacio de nombres y reduce el número de archivos fuente.

(El código fuente completo de esta sugerencia se puede descargar en formato zip desde la sección de Recursos).

Clases anidadas frente a clases internas

Las clases anidadas son simplemente clases internas estáticas. La diferencia entre clases anidadas y clases internas es la misma que la diferencia entre miembros estáticos y no estáticos de una clase: las clases anidadas están asociadas con la propia clase adjunta, mientras que las clases internas están asociadas con un objeto de la clase adjunta.

Debido a esto, los objetos de clase interna requieren un objeto de la clase adjunta, mientras que los objetos de clase anidados no. Las clases anidadas, por lo tanto, se comportan como clases de nivel superior, utilizando la clase adjunta para proporcionar una organización similar a un paquete. Además, las clases anidadas tienen acceso a todos los miembros de la clase adjunta.

Motivación

Considere un subsistema típico de Java, por ejemplo, un componente Swing, utilizando el patrón de diseño Modelo-Vista-Controlador (MVC). Los objetos de evento encapsulan las notificaciones de cambios del modelo. Las vistas registran interés en varios eventos agregando oyentes al modelo subyacente del componente. El modelo notifica a sus espectadores de los cambios en su propio estado al entregar estos objetos de evento a sus oyentes registrados. A menudo, estos tipos de evento y escucha son específicos del tipo de modelo y, por lo tanto, solo tienen sentido en el contexto del tipo de modelo. Debido a que cada uno de estos tipos de evento y escucha deben ser de acceso público, cada uno debe estar en su propio archivo fuente. En esta situación, a menos que se utilice alguna convención de codificación, el acoplamiento entre estos tipos es difícil de reconocer. Por supuesto, se puede usar un paquete separado para cada grupo para mostrar el acoplamiento,pero esto da como resultado una gran cantidad de paquetes.

Si implementamos tipos de escucha y eventos como tipos anidados de la interfaz del modelo, hacemos que el acoplamiento sea obvio. Podemos usar cualquier modificador de acceso deseado con estos tipos anidados, incluido public. Además, como los tipos anidados usan la interfaz adjunta como espacio de nombres, el resto del sistema se refiere a ellos como ., evitando la contaminación del espacio de nombres dentro de ese paquete. El archivo de origen de la interfaz del modelo tiene todos los tipos de soporte, lo que facilita el desarrollo y el mantenimiento.

Antes: un ejemplo sin clases anidadas

Como ejemplo, desarrollamos un componente simple Slate, cuya tarea es dibujar formas. Al igual que los componentes Swing, utilizamos el patrón de diseño MVC. El modelo, SlateModelsirve como depósito de formas. SlateModelListeners suscríbase a los cambios en el modelo. El modelo notifica a sus oyentes enviando eventos de tipo SlateModelEvent. En este ejemplo, necesitamos tres archivos fuente, uno para cada clase:

// SlateModel.java import java.awt.Shape; public interface SlateModel {// Gestión de oyentes public void addSlateModelListener (SlateModelListener l); public void removeSlateModelListener (SlateModelListener l); // Gestión del repositorio de formas, las vistas necesitan notificación public void addShape (Shape s); public void removeShape (Shape s); public void removeAllShapes (); // Dar forma al repositorio de operaciones de solo lectura public int getShapeCount (); public Shape getShapeAtIndex (índice int); }
// SlateModelListener.java import java.util.EventListener; La interfaz pública SlateModelListener extiende EventListener {public void slateChanged (evento SlateModelEvent); }
// SlateModelEvent.java import java.util.EventObject; La clase pública SlateModelEvent extiende EventObject {SlateModelEvent público (modelo SlateModel) {super (modelo); }}

(El código fuente de DefaultSlateModella implementación predeterminada de este modelo se encuentra en el archivo antes de / DefaultSlateModel.java).

A continuación, dirigimos nuestra atención a Slateuna vista de este modelo, que reenvía su tarea de pintura al delegado de la interfaz de usuario SlateUI:

// Slate.java import javax.swing.JComponent; public class Slate amplía JComponent implementa SlateModelListener {private SlateModel _model; Pizarra pública (modelo SlateModel) {_model = modelo; _model.addSlateModelListener (esto); setOpaque (verdadero); setUI (nuevo SlateUI ()); } public Slate () {this (new DefaultSlateModel ()); } public SlateModel getModel () {return _model; } // Implementación del oyente public void slateChanged (evento SlateModelEvent) {repaint (); }}

Finalmente, SlateUIel componente de GUI visual:

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

Después: un ejemplo modificado usando clases anidadas

La estructura de clases en el ejemplo anterior no muestra la relación entre las clases. Para mitigar esto, usamos una convención de nomenclatura que requiere que todas las clases relacionadas tengan un prefijo común, pero sería más claro mostrar la relación en código. Además, los desarrolladores y mantenedores de estas clases deben administrar tres archivos: para SlateModel, para SlateEventy para SlateListener, para implementar un concepto. Lo mismo ocurre con la gestión de los dos archivos para Slatey SlateUI.

Podemos mejorar las cosas por hacer SlateModelListenery SlateModelEventtipos anidados de la SlateModelinterfaz. Debido a que estos tipos anidados están dentro de una interfaz, son implícitamente estáticos. No obstante, hemos utilizado una declaración estática explícita para ayudar al programador de mantenimiento.

El código del cliente se referirá a ellos como SlateModel.SlateModelListenery SlateModel.SlateModelEvent, pero esto es redundante e innecesariamente largo. Eliminamos el prefijo SlateModelde las clases anidadas. Con este cambio, el código de cliente se referirá a ellos como SlateModel.Listenery SlateModel.Event. Esto es breve y claro y no depende de estándares de codificación.

Porque SlateUIhacemos lo mismo: lo convertimos en una clase anidada de Slatey cambiamos su nombre a UI. Debido a que es una clase anidada dentro de una clase (y no dentro de una interfaz), debemos usar un modificador estático explícito.

Con estos cambios, solo necesitamos un archivo para las clases relacionadas con el modelo y uno más para las clases relacionadas con la vista. El SlateModelcódigo ahora se convierte en:

// SlateModel.java import java.awt.Shape; import java.util.EventListener; import java.util.EventObject; public interface SlateModel {// Gestión de oyentes public void addSlateModelListener (SlateModel.Listener l); public void removeSlateModelListener (SlateModel.Listener l); // Gestión del repositorio de formas, las vistas necesitan notificación public void addShape (Shape s); public void removeShape (Shape s); public void removeAllShapes (); // Dar forma al repositorio de operaciones de solo lectura public int getShapeCount (); public Shape getShapeAtIndex (índice int); // Interfaces y clases anidadas de nivel superior relacionadas Interfaz pública Listener extiende EventListener {public void slateChanged (evento SlateModel.Event); } evento de clase pública extiende EventObject {evento público (modelo SlateModel) {super (modelo); }}}

Y el código de Slatese cambia a:

// Slate.java import java.awt. *; import javax.swing.JComponent; import javax.swing.plaf.ComponentUI; public class Slate amplía JComponent implementa SlateModel.Listener {public Slate (modelo SlateModel) {_model = modelo; _model.addSlateModelListener (esto); setOpaque (verdadero); setUI (nuevo Slate.UI ()); } public Slate () {this (new DefaultSlateModel ()); } public SlateModel getModel () {return _model; } // Implementación del oyente public void slateChanged (evento SlateModel.Event) {repaint (); } La interfaz de usuario de clase estática pública extiende ComponentUI {pintura vacía pública (Graphics g, JComponent c) {SlateModel model = ((Slate) c) .getModel (); g.setColor (c.getForeground ()); Graphics2D g2D = (Graphics2D) g; para (int tamaño = model.getShapeCount (), i = 0; i <tamaño; i ++) {g2D.draw (model.getShapeAtIndex (i)); }}}}

(El código fuente de la implementación predeterminada para el modelo modificado DefaultSlateModel, está en el archivo después de / DefaultSlateModel.java).

Dentro de la SlateModelclase, no es necesario utilizar nombres completos para clases e interfaces anidadas. Por ejemplo, solo Listenersería suficiente en lugar de SlateModel.Listener. Sin embargo, el uso de nombres completos ayuda a los desarrolladores que copian firmas de métodos de la interfaz y las pegan en clases de implementación.

El JFC y el uso de clases anidadas

La biblioteca JFC usa clases anidadas en ciertos casos. Por ejemplo, la clase BasicBordersen el paquete javax.swing.plaf.basicdefine varias clases anidadas como BasicBorders.ButtonBorder. En este caso, la clase BasicBordersno tiene otros miembros y simplemente actúa como un paquete. En su lugar, utilizar un paquete separado habría sido igualmente eficaz, si no más apropiado. Este es un uso diferente al presentado en este artículo.

El uso del enfoque de este consejo en el diseño de JFC afectaría la organización de los tipos de eventos y oyentes relacionados con los tipos de modelos. Por ejemplo, javax.swing.event.TableModelListenery javax.swing.event.TableModelEventse implementarían respectivamente como una interfaz anidada y una clase anidada dentro 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 es un arquitecto certificado por Sun de tecnología Java (Java 2). Tiene una Maestría en Ingeniería Eléctrica con especialización en Ingeniería de Comunicaciones. Tiene seis años de experiencia en el diseño y desarrollo de varios proyectos de software que involucran GUI, redes y sistemas distribuidos. Ha desarrollado sistemas de software orientados a objetos en Java durante los últimos dos años y en C ++ durante los últimos cinco años. Ramnivas trabaja actualmente en Real-Time Innovations Inc. como ingeniero de software. En RTI, actualmente está trabajando para diseñar y desarrollar ControlShell, el marco de programación basado en componentes para construir sistemas complejos en tiempo real.