Java съвет 75: Използвайте вложени класове за по-добра организация

Типичната подсистема в приложение на Java се състои от набор от сътрудничещи класове и интерфейси, всеки от които изпълнява определена роля. Някои от тези класове и интерфейси имат смисъл само в контекста на други класове или интерфейси.

Проектирането на зависими от контекста класове като вложени класове от най-високо ниво (вложени класове, за кратко), затворени от класа, обслужващ контекста, прави тази зависимост по-ясна. Освен това използването на вложени класове улеснява разпознаването на сътрудничеството, избягва замърсяването на пространството от имена и намалява броя на изходните файлове.

(Пълният изходен код за този съвет може да бъде изтеглен в zip формат от раздела Ресурси.)

Вложени класове срещу вътрешни класове

Вложените класове са просто статични вътрешни класове. Разликата между вложени класове и вътрешни класове е същата като разликата между статични и нестатични членове на клас: вложени класове са свързани със самия затварящ клас, докато вътрешните класове са свързани с обект на затварящия клас.

Поради това обектите на вътрешния клас изискват обект от затварящия клас, докато вложени обекти на клас не. Следователно вложените класове се държат точно като класове от най-високо ниво, като използват заграждащия клас, за да осигурят подобна на пакет организация. Освен това вложените класове имат достъп до всички членове на затварящия клас.

Мотивация

Помислете за типична подсистема на Java, например компонент Swing, като използвате модела за проектиране на Model-View-Controller (MVC). Обектните събития капсулират известия за промени от модела. Изгледите регистрират интерес към различни събития, като добавят слушатели към основния модел на компонента. Моделът уведомява своите зрители за промени в собственото си състояние, като доставя тези обекти на събития на регистрираните си слушатели. Често тези типове слушатели и събития са специфични за типа модел и следователно имат смисъл само в контекста на типа модел. Тъй като всеки от тези типове слушатели и събития трябва да бъдат публично достъпни, всеки трябва да бъде в свой собствен изходен файл. В тази ситуация, освен ако не се използва някаква конвенция за кодиране, връзката между тези типове е трудна за разпознаване. Разбира се, може да се използва отделен пакет за всяка група, за да се покаже съединението,но това води до голям брой пакети.

Ако внедряваме типове слушатели и събития като вложени типове на интерфейса на модела, правим свързването очевидно. Можем да използваме всеки модификатор на достъп, който желаем с тези вложени типове, включително публичен. В допълнение, тъй като вложени типове използват затварящия интерфейс като пространство на имена, останалата част от системата се отнася към тях като .избягва замърсяването на пространството от имена вътре в този пакет. Изходният файл за интерфейса на модела има всички поддържащи типове, което улеснява разработването и поддръжката.

Преди: Пример без вложени класове

Като пример разработваме прост компонент Slate, чиято задача е да рисува фигури. Подобно на компонентите Swing, ние използваме шаблона за дизайн на MVC. Моделът, SlateModelслужи като хранилище за фигури. SlateModelListeners се абонирайте за промените в модела. Моделът уведомява своите слушатели, като изпраща събития от типа SlateModelEvent. В този пример се нуждаем от три изходни файла, по един за всеки клас:

// SlateModel.java импортиране на java.awt.Shape; публичен интерфейс SlateModel {// управление на слушатели public void addSlateModelListener (SlateModelListener l); публична пустота removeSlateModelListener (SlateModelListener l); // Управление на хранилище на фигури, изгледите се нуждаят от известие публична невалидна addShape (Shape s); public void removeShape (Shape s); public void removeAllShapes (); // Оформяне на хранилище за операции само за четене public int getShapeCount (); публична форма getShapeAtIndex (индекс int); }
// SlateModelListener.java импортиране на java.util.EventListener; публичен интерфейс SlateModelListener разширява EventListener {public void slateChanged (SlateModelEvent събитие); }
// SlateModelEvent.java импортиране на java.util.EventObject; публичен клас SlateModelEvent разширява EventObject {публичен SlateModelEvent (модел SlateModel) {супер (модел); }}

(Изходният код за DefaultSlateModelизпълнението по подразбиране за този модел е във файла преди / DefaultSlateModel.java.)

След това насочваме вниманието си към Slateизглед за този модел, който препраща задачата си за рисуване към делегата на потребителския интерфейс SlateUI:

// Slate.java импортиране на javax.swing.JComponent; публичен клас Slate разширява JComponent внедрява SlateModelListener {private SlateModel _model; публичен шифер (модел SlateModel) {_model = модел; _model.addSlateModelListener (това); setOpaque (вярно); setUI (нов SlateUI ()); } public Slate () {this (нов DefaultSlateModel ()); } публичен SlateModel getModel () {return _model; } // Изпълнение на слушателя public void slateChanged (SlateModelEvent събитие) {repaint (); }}

И накрая, SlateUIвизуалният GUI компонент:

// SlateUI.java импортиране на java.awt. *; импортиране на javax.swing.JComponent; импортиране на javax.swing.plaf.ComponentUI; публичен клас SlateUI разширява ComponentUI {public void paint (Graphics g, JComponent c) {SlateModel model = ((Slate) c) .getModel (); g.setColor (c.getForeground ()); Graphics2D g2D = (Graphics2D) g; за (int size = model.getShapeCount (), i = 0; i <size; i ++) {g2D.draw (model.getShapeAtIndex (i)); }}}

След: Модифициран пример, използващ вложени класове

Структурата на класовете в горния пример не показва връзката между класовете. За да смекчим това, използвахме конвенция за именуване, която изисква всички свързани класове да имат общ префикс, но би било по-ясно да се покаже връзката в код. Освен това разработчиците и поддръжниците на тези класове трябва да управляват три файла: за SlateModel, за SlateEventи за SlateListener, за да приложат една концепция. Същото важи и за управлението на двата файла за Slateи SlateUI.

Можем да подобрим нещата, като създадем SlateModelListenerи SlateModelEventвложени видове SlateModelинтерфейс. Тъй като тези вложени типове са вътре в интерфейс, те са имплицитно статични. Въпреки това използвахме изрична статична декларация, за да помогнем на програмиста за поддръжка.

Клиентският код ще ги нарича „ SlateModel.SlateModelListenerи“ SlateModel.SlateModelEvent, но това е излишно и ненужно дълго. Премахваме префикса SlateModelот вложените класове. С тази промяна клиентският код ще ги отнесе като SlateModel.Listenerи SlateModel.Event. Това е кратко и ясно и не зависи от стандартите за кодиране.

Защото SlateUIправим същото нещо - правим го вложен клас Slateи променяме името му на UI. Тъй като това е вложен клас в клас (а не вътре в интерфейс), трябва да използваме явен статичен модификатор.

С тези промени се нуждаем само от един файл за класовете, свързани с модела, и още един за класовете, свързани с изгледа. В SlateModelкода сега става:

// SlateModel.java импортиране на java.awt.Shape; импортиране на java.util.EventListener; импортиране на java.util.EventObject; публичен интерфейс SlateModel {// управление на слушатели public void addSlateModelListener (SlateModel.Listener l); публична пустота removeSlateModelListener (SlateModel.Listener l); // Управление на хранилище на фигури, изгледите се нуждаят от известие публична невалидна addShape (Shape s); public void removeShape (Shape s); public void removeAllShapes (); // Оформяне на хранилище за операции само за четене public int getShapeCount (); публична форма getShapeAtIndex (индекс int); // Свързани вложени класове от най-високо ниво и интерфейси Обществен интерфейс Listener разширява EventListener {public void slateChanged (събитие SlateModel.Event); } публичен клас Събитие разширява EventObject {публично събитие (модел SlateModel) {супер (модел); }}}

И кодът за Slateсе променя на:

// Slate.java импортиране java.awt. *; импортиране на javax.swing.JComponent; импортиране на javax.swing.plaf.ComponentUI; публичен клас Slate разширява JComponent реализира SlateModel.Listener {публичен Slate (модел SlateModel) {_model = модел; _model.addSlateModelListener (това); setOpaque (вярно); setUI (нов Slate.UI ()); } public Slate () {this (нов DefaultSlateModel ()); } публичен SlateModel getModel () {return _model; } // Внедряване на слушател public void slateChanged (SlateModel.Event събитие) {repaint (); } публичен статичен потребителски интерфейс на клас разширява ComponentUI {публична невалидна боя (Graphics g, JComponent c) {SlateModel model = ((Slate) c) .getModel (); g.setColor (c.getForeground ()); Graphics2D g2D = (Graphics2D) g; за (int size = model.getShapeCount (), i = 0; i <size; i ++) {g2D.draw (model.getShapeAtIndex (i)); }}}}

(Изходният код за изпълнение по подразбиране за променения модел DefaultSlateModel, е във файла след / DefaultSlateModel.java.)

В рамките на SlateModelкласа е излишно да се използват напълно квалифицирани имена за вложени класове и интерфейси. Например просто Listenerби било достатъчно вместо SlateModel.Listener. Използването на напълно квалифицирани имена обаче помага на разработчиците, които копират подписите на метода от интерфейса и ги поставят в изпълняващи класове.

JFC и използването на вложени класове

JFC библиотеката използва вложени класове в определени случаи. Например, class BasicBordersв пакета javax.swing.plaf.basicдефинира няколко вложени класа като BasicBorders.ButtonBorder. В този случай класът BasicBordersняма други членове и просто действа като пакет. Използването на отделен пакет вместо това би било също толкова ефективно, ако не и по-подходящо. Това е различна употреба от тази, представена в тази статия.

Използването на подхода на този съвет при проектирането на JFC би повлияло на организацията на типове слушатели и събития, свързани с типовете модели. Например, javax.swing.event.TableModelListenerи javax.swing.event.TableModelEventще бъде реализиран съответно като вложен интерфейс и вложен клас вътре 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.

Рамнивас Ладад е сертифициран от слънцето архитект на Java Technology (Java 2). Има магистърска степен по електротехника със специализация по комуникационно инженерство. Той има шест години опит в проектирането и разработването на няколко софтуерни проекта, включващи GUI, мрежи и разпределени системи. Той е разработил обектно-ориентирани софтуерни системи в Java през последните две години и в C ++ през последните пет години. В момента Рамнивас работи в Real-Time Innovations Inc. като софтуерен инженер. В момента в RTI работи по проектирането и разработването на ControlShell, базирана на компоненти програмна рамка за изграждане на сложни системи в реално време.