Java Savjet 75: Koristite ugniježđene klase za bolju organizaciju

Tipični podsustav u Java aplikaciji sastoji se od skupa suradničkih klasa i sučelja, od kojih svaka ima određenu ulogu. Neke od tih klasa i sučelja imaju značaj samo u kontekstu drugih klasa ili sučelja.

Dizajniranje klasa ovisnih o kontekstu kao ugniježđenih klasa najviše razine (ukratko ugniježđenih klasa) zatvorenih klasom koja služi za kontekst čini ovu ovisnost jasnijom. Nadalje, upotreba ugniježđenih klasa olakšava prepoznavanje suradnje, izbjegava onečišćenje prostora imena i smanjuje broj izvornih datoteka.

(Cjelokupni izvorni kod za ovaj savjet možete preuzeti u zip formatu iz odjeljka Resursi.)

Ugnježđena nastava u odnosu na unutarnju nastavu

Ugnježđene klase su jednostavno statične unutarnje klase. Razlika između ugniježđenih klasa i unutarnjih klasa ista je kao i razlika između statičkih i nestatičnih članova klase: ugniježđene klase povezane su sa samom klasom koja obuhvaća, dok su unutarnje klase povezane s objektom klase koja obuhvaća.

Zbog toga objekti unutarnje klase zahtijevaju objekt zatvorene klase, dok ugniježđeni objekti klase ne. Ugniježđene se klase, prema tome, ponašaju baš kao i klase najviše razine, koristeći klasu koja obuhvaća da bi osigurale organizaciju nalik paketu. Uz to, ugniježđene klase imaju pristup svim članovima zatvorene klase.

Motivacija

Razmotrimo tipični Java podsustav, na primjer komponentu Swing, koristeći obrazac dizajna Model-View-Controller (MVC). Objekti događaja enkapsuliraju obavijesti o promjenama iz modela. Pogledi registriraju zanimanje za razne događaje dodavanjem slušatelja osnovnom modelu komponente. Model obavještava svoje gledatelje o promjenama u vlastitom stanju isporukom tih objekata događaja svojim registriranim slušateljima. Ti su tipovi slušatelja i događaja često specifični za tip modela i stoga imaju smisla samo u kontekstu tipa modela. Budući da svaka od ovih vrsta slušatelja i događaja mora biti javno dostupna, svaka mora biti u vlastitoj izvornoj datoteci. U ovoj je situaciji, ako se ne koristi neka konvencija kodiranja, teško prepoznati spregu između ovih vrsta. Naravno, za svaku skupinu može se koristiti zasebni paket za prikaz sprege,ali to rezultira velikim brojem paketa.

Ako vrste slušatelja i događaja implementiramo kao ugniježđene tipove sučelja modela, spajanje ćemo učiniti očitim. Možemo koristiti bilo koji modifikator pristupa koji želimo s tim ugniježđenim vrstama, uključujući javni. Uz to, budući da ugniježđeni tipovi koriste zatvoreno sučelje kao prostor imena, ostatak sustava na njih se odnosi ., izbjegavajući zagađenje prostora imena unutar tog paketa. Izvorna datoteka za sučelje modela ima sve podržane tipove, što olakšava razvoj i održavanje.

Prije: Primjer bez ugniježđenih klasa

Kao primjer, razvijamo jednostavnu komponentu Slatečiji je zadatak crtati oblike. Baš kao i Swing komponente, koristimo MVC dizajn. Model, SlateModelsluži kao spremište za oblike. SlateModelListeners pretplatite se na promjene u modelu. Model obavještava slušatelje slanjem događaja tipa SlateModelEvent. U ovom primjeru trebamo tri izvorne datoteke, po jednu za svaki razred:

// SlateModel.java import java.awt.Shape; javno sučelje SlateModel {// upravljanje slušateljima javna praznina addSlateModelListener (SlateModelListener l); javna praznina removeSlateModelListener (SlateModelListener l); // upravljanje spremištem oblika, pogledi trebaju obavijest javna praznina addShape (Oblici s); javna praznina removeShape (Shape s); javna praznina removeAllShapes (); // Repozitorij oblika oblika operacije samo za čitanje public int getShapeCount (); javni oblik getShapeAtIndex (int indeks); }
// SlateModelListener.java import java.util.EventListener; javno sučelje SlateModelListener proširuje EventListener {public void slateChanged (SlateModelEvent događaj); }
// SlateModelEvent.java import java.util.EventObject; javna klasa SlateModelEvent proširuje EventObject {javni SlateModelEvent (model SlateModel) {super (model); }}

(Izvorni kod za DefaultSlateModel, zadana implementacija za ovaj model nalazi se u datoteci before / DefaultSlateModel.java.)

Dalje, skrećemo pozornost na Slatepogled za ovaj model koji svoj zadatak slikanja prosljeđuje delegatu korisničkog sučelja SlateUI:

// Slate.java uvoz javax.swing.JComponent; javna klasa Slate proširuje JComponent implementira SlateModelListener {private SlateModel _model; javni Slate (model SlateModel) {_model = model; _model.addSlateModelListener (ovo); setOpaque (true); setUI (novi SlateUI ()); } javni Slate () {this (novi DefaultSlateModel ()); } javni SlateModel getModel () {return _model; } // Implementacija slušatelja public void slateChanged (događaj SlateModelEvent) {repaint (); }}

Konačno, SlateUIvizualna komponenta GUI:

// SlateUI.java import java.awt. *; import javax.swing.JComponent; uvoz javax.swing.plaf.ComponentUI; javna klasa SlateUI proširuje ComponentUI {boja javne praznine (Grafika g, JComponent c) {SlateModel model = ((Slate) c) .getModel (); g.setColor (c.getForeground ()); Graphics2D g2D = (Graphics2D) g; za (int size = model.getShapeCount (), i = 0; i <size; i ++) {g2D.draw (model.getShapeAtIndex (i)); }}}

Nakon: Izmijenjeni primjer pomoću ugniježđenih klasa

Struktura klase u gornjem primjeru ne pokazuje odnos između klasa. Da bismo to ublažili, koristili smo konvenciju imenovanja koja zahtijeva da sve povezane klase imaju zajednički prefiks, ali bilo bi jasnije prikazati odnos u kodu. Nadalje, programeri i održavatelji ovih klasa moraju upravljati s tri datoteke: za SlateModel, za SlateEventi za SlateListenerprovedbu jednog koncepta. Isto vrijedi i za upravljanje dvjema datotekama za Slatei SlateUI.

Stvari možemo poboljšati izradom SlateModelListeneri SlateModelEventugniježđenim vrstama SlateModelsučelja. Budući da su ti ugniježđeni tipovi unutar sučelja, oni su implicitno statični. Unatoč tome, koristili smo eksplicitnu statičku deklaraciju kako bismo pomogli programeru održavanja.

Klijentski kod odnosit će se na njih kao SlateModel.SlateModelListeneri SlateModel.SlateModelEvent, ali to je suvišno i nepotrebno dugo. Uklanjamo prefiks SlateModeliz ugniježđenih klasa. Ovom će se promjenom klijentski kod odnositi na njih kao SlateModel.Listeneri SlateModel.Event. Ovo je kratko i jasno i ne ovisi o standardima kodiranja.

Jer SlateUI, radimo istu stvar - pravimo ugniježđenu klasu Slatei mijenjamo joj ime u UI. Budući da je to ugniježđena klasa unutar klase (a ne unutar sučelja), moramo koristiti eksplicitni statički modifikator.

Uz ove promjene trebamo samo jednu datoteku za klase povezane s modelom i još jednu za klase povezane s prikazom. SlateModelKod sada postaje:

// SlateModel.java import java.awt.Shape; import java.util.EventListener; import java.util.EventObject; javno sučelje SlateModel {// upravljanje slušateljima javna praznina addSlateModelListener (SlateModel.Listener l); javna praznina removeSlateModelListener (SlateModel.Listener l); // upravljanje spremištem oblika, pogledi trebaju obavijest javna praznina addShape (Oblici s); javna praznina removeShape (Shape s); javna praznina removeAllShapes (); // Repozitorij oblika oblika operacije samo za čitanje public int getShapeCount (); javni oblik getShapeAtIndex (int indeks); // Povezane ugniježđene klase i sučelja najvišeg nivoa Slušatelj javnog sučelja proširuje EventListener {public void slateChanged (SlateModel.Event event); } događaj javne klase proširuje EventObject {javni događaj (model SlateModel) {super (model); }}}

I kôd za Slateje promijenjen u:

// Slate.java import java.awt. *; import javax.swing.JComponent; uvoz javax.swing.plaf.ComponentUI; javna klasa Slate proširuje JComponent implementira SlateModel.Listener {javni Slate (model SlateModel) {_model = model; _model.addSlateModelListener (ovo); setOpaque (true); setUI (novi Slate.UI ()); } public Slate () {this (novi DefaultSlateModel ()); } javni SlateModel getModel () {return _model; } // Implementacija slušatelja public void slateChanged (SlateModel.Event event) {repaint (); } korisničko sučelje javne statičke klase proširuje ComponentUI {public void paint (Grafika g, JComponent c) {SlateModel model = ((Slate) c) .getModel (); g.setColor (c.getForeground ()); Graphics2D g2D = (Graphics2D) g; za (int size = model.getShapeCount (), i = 0; i <size; i ++) {g2D.draw (model.getShapeAtIndex (i)); }}}}

(Izvorni kod za zadanu implementaciju za promijenjeni model DefaultSlateModel, nalazi se u datoteci nakon / DefaultSlateModel.java.)

Unutar SlateModelklase nepotrebno je koristiti potpuno kvalificirana imena za ugniježđene klase i sučelja. Na primjer, samo Listenerbi bilo dovoljno umjesto SlateModel.Listener. Međutim, upotreba potpuno kvalificiranih imena pomaže programerima koji kopiraju potpise metoda s sučelja i lijepe ih u izvedbene klase.

JFC i upotreba ugniježđenih klasa

JFC knjižnica koristi ugniježđene klase u određenim slučajevima. Na primjer, klasa BasicBordersu paketu javax.swing.plaf.basicdefinira nekoliko ugniježđenih klasa kao što su BasicBorders.ButtonBorder. U ovom slučaju, razred BasicBordersnema drugih članova i jednostavno djeluje kao paket. Korištenje zasebnog paketa umjesto toga bilo bi jednako učinkovito, ako ne i prikladnije. Ovo se koristi drugačije od one predstavljene u ovom članku.

Korištenje pristupa ovog savjeta u JFC dizajnu utjecalo bi na organizaciju tipova slušatelja i događaja povezanih s tipovima modela. Na primjer, javax.swing.event.TableModelListeneri javax.swing.event.TableModelEventimplementirao bi se kao ugniježđeno sučelje i ugniježđena klasa unutra javax.swing.table.TableModel.

Ova bi promjena, zajedno sa skraćivanjem imena, rezultirala imenovanjem sučelja slušatelja javax.swing.table.TableModel.Listeneri imenovanjem klase događaja javax.swing.table.TableModel.Event. TableModeltada bi bio potpuno samozatajan sa svim potrebnim klasama i sučeljima podrške, umjesto da im je potrebna klasa podrške i sučelje raspoređeno u tri datoteke i dva paketa.

Smjernice za upotrebu ugniježđenih klasa

Kao i kod bilo kojeg drugog uzorka, razborita uporaba ugniježđenih klasa rezultira dizajnom koji je jednostavniji i lakše razumljiv od tradicionalne organizacije paketa. Međutim, netočna uporaba dovodi do nepotrebnog spajanja, što čini ulogu ugniježđenih klasa nejasnom.

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 je arhitekt Java tehnologije (Java 2) certificiran od strane Sunca. Magistrirao je elektrotehniku ​​sa specijalizacijom iz komunikacijskog inženjerstva. Ima šest godina iskustva u dizajniranju i razvoju nekoliko softverskih projekata koji uključuju GUI, umrežavanje i distribuirane sustave. Objektno orijentirane softverske sustave razvio je u Javi u posljednje dvije godine i u C ++ u posljednjih pet godina. Ramnivas trenutno radi u tvrtki Real-Time Innovations Inc. kao softverski inženjer. U RTI-u trenutno radi na dizajniranju i razvoju ControlShell-a, programskog okvira temeljenog na komponentama za izgradnju složenih sustava u stvarnom vremenu.