Programmering

Java Tip 75: Brug indlejrede klasser til bedre organisering

Et typisk undersystem i en Java-applikation består af et sæt samarbejdsklasser og grænseflader, der hver især udfører en specifik rolle. Nogle af disse klasser og grænseflader er kun meningsfulde i sammenhæng med andre klasser eller grænseflader.

At designe kontekstafhængige klasser som indlejrede klasser på øverste niveau (forkortet nestede klasser), der er lukket af den kontekstserverende klasse, gør denne afhængighed klarere. Desuden gør brugen af ​​indlejrede klasser samarbejdet lettere at genkende, undgår forurening af navneområdet og reducerer antallet af kildefiler.

(Den komplette kildekode til dette tip kan downloades i zip-format fra sektionen Ressourcer.)

Indlejrede klasser vs. indre klasser

Indlejrede klasser er simpelthen statiske indre klasser. Forskellen mellem indlejrede klasser og indre klasser er den samme som forskellen mellem statiske og ikke-statiske medlemmer af en klasse: Indlejrede klasser er forbundet med selve den indesluttende klasse, mens indre klasser er associeret med et objekt fra den indesluttende klasse.

På grund af dette kræver indre klasseobjekter et objekt fra den indesluttende klasse, mens indlejrede klasseobjekter ikke gør det. Indlejrede klasser opfører sig derfor ligesom topklasser ved hjælp af den vedlagte klasse til at give en pakke-lignende organisation. Derudover har indlejrede klasser adgang til alle medlemmer af den indesluttende klasse.

Motivering

Overvej et typisk Java-undersystem, for eksempel en Swing-komponent, der bruger Model-View-Controller (MVC) designmønster. Hændelsesobjekter indkapsler ændringer underretninger fra modellen. Visninger registrerer interesse for forskellige begivenheder ved at tilføje lyttere til den underliggende model for komponenten. Modellen underretter sine seere om ændringer i sin egen tilstand ved at levere disse begivenhedsobjekter til sine registrerede lyttere. Ofte er disse lytter- og begivenhedstyper specifikke for modeltypen og giver derfor kun mening i forbindelse med modelltypen. Fordi hver af disse lytter- og begivenhedstyper skal være offentligt tilgængelige, skal hver være i sin egen kildefil. I denne situation er koblingen mellem disse typer, medmindre der anvendes nogen kodningskonvention, vanskelig at genkende. Naturligvis kan man bruge en separat pakke til hver gruppe for at vise koblingen, men dette resulterer i et stort antal pakker.

Hvis vi implementerer lytter- og begivenhedstyper som indlejrede typer af modelgrænsefladen, gør vi koblingen indlysende. Vi kan bruge enhver adgangsmodifikator, der ønskes med disse indlejrede typer, herunder offentlig. Desuden henviser resten af ​​systemet til, da indlejrede typer bruger den vedlagte grænseflade som et navneområde ., undgå forurening af navneområdet inde i pakken. Kildefilen til modelgrænsefladen har alle de understøttende typer, hvilket gør udvikling og vedligeholdelse lettere.

Før: Et eksempel uden indlejrede klasser

Som et eksempel udvikler vi en simpel komponent, Skifer, hvis opgave er at tegne figurer. Ligesom Swing-komponenter bruger vi MVC-designmønsteret. Modellen, SlateModel, fungerer som et lager for figurer. SlateModelListeners abonnerer på ændringer i modellen. Modellen underretter sine lyttere ved at sende begivenheder af typen SlateModelEvent. I dette eksempel har vi brug for tre kildefiler, en til hver klasse:

// SlateModel.java importerer java.awt.Shape; offentlig grænseflade SlateModel {// Lytterstyring offentlig ugyldighed addSlateModelListener (SlateModelListener l); offentlig tomrum removeSlateModelListener (SlateModelListener l); // Administration af formlager, visninger skal meddeles offentlig ugyldig addShape (figur s); offentlig tomrum removeShape (Shape s); offentlig tomrum removeAllShapes (); // Skrivebeskyttede operationer til formarkivet public int getShapeCount (); offentlig Shape getShapeAtIndex (int-indeks); } 
// SlateModelListener.java importerer java.util.EventListener; offentlig grænseflade SlateModelListener udvider EventListener {public void slateChanged (SlateModelEvent event); } 
// SlateModelEvent.java importerer java.util.EventObject; offentlig klasse SlateModelEvent udvider EventObject {public SlateModelEvent (SlateModel model) {super (model); }} 

(Kildekoden til DefaultSlateModel, standardimplementeringen for denne model, er i filen før / DefaultSlateModel.java.)

Dernæst vender vi vores opmærksomhed mod Skifer, en visning for denne model, som videresender sin maleropgave til UI-delegaten, SlateUI:

// Slate.java import javax.swing.JComponent; offentlig klasse Slate udvider JComponent implementerer SlateModelListener {private SlateModel _model; public Slate (SlateModel model) {_model = model; _model.addSlateModelListener (dette); setOpaque (sand); setUI (ny SlateUI ()); } public Slate () {this (new DefaultSlateModel ()); } offentlig SlateModel getModel () {return _model; } // Lytterimplementering offentlig ugyldig slateChanged (SlateModelEvent-begivenhed) {ommaling (); }} 

Langt om længe, SlateUI, den visuelle GUI-komponent:

// SlateUI.java importerer java.awt. *; import javax.swing.JComponent; import javax.swing.plaf.ComponentUI; offentlig klasse SlateUI udvider ComponentUI {offentlig ugyldig maling (Grafik g, JComponent c) {SlateModel model = ((Slate) c) .getModel (); g.setColor (c.getForeground ()); Graphics2D g2D = (Graphics2D) g; til (int størrelse = model.getShapeCount (), i = 0; i <størrelse; i ++) {g2D.draw (model.getShapeAtIndex (i)); }}} 

Efter: Et modificeret eksempel ved hjælp af indlejrede klasser

Klassestrukturen i eksemplet ovenfor viser ikke forholdet mellem klasserne. For at afbøde dette har vi brugt en navngivningskonvention, der kræver, at alle relaterede klasser har et fælles præfiks, men det ville være tydeligere at vise forholdet i kode. Desuden skal udviklere og vedligeholdere af disse klasser administrere tre filer: for SlateModel, til SlateEventog for SlateListener, at implementere et koncept. Det samme gælder for administration af de to filer til Skifer og SlateUI.

Vi kan forbedre tingene ved at lave SlateModelListener og SlateModelEvent indlejrede typer af SlateModel interface. Fordi disse indlejrede typer er inde i en grænseflade, er de implicit statiske. Ikke desto mindre har vi brugt en eksplicit statisk erklæring til at hjælpe vedligeholdelsesprogrammereren.

Kundekode henviser til dem som SlateModel.SlateModelListener og SlateModel.SlateModelEvent, men dette er overflødigt og unødvendigt langt. Vi fjerner præfikset SlateModel fra de indlejrede klasser. Med denne ændring vil klientkode henvise til dem som SlateModel.Lytter og SlateModel.Event. Dette er kort og tydeligt og afhænger ikke af kodningsstandarder.

Til SlateUI, vi gør det samme - vi gør det til en indlejret klasse af Skifer og skift navn til UI. Fordi det er en indlejret klasse inde i en klasse (og ikke inde i en grænseflade), skal vi bruge en eksplicit statisk modifikator.

Med disse ændringer har vi kun brug for en fil til de modelrelaterede klasser og en mere til de visningsrelaterede klasser. Det SkiferModel kode bliver nu:

// SlateModel.java importerer java.awt.Shape; importere java.util.EventListener; importere java.util.EventObject; offentlig grænseflade SlateModel {// Listener management public void addSlateModelListener (SlateModel.Listener l); offentlig tomrum removeSlateModelListener (SlateModel.Listener l); // Administration af formlager, visninger skal meddeles offentlig ugyldig addShape (figur s); offentlig tomrum removeShape (Shape s); offentlig tomrum removeAllShapes (); // Skrivebeskyttede operationer til formarkivet public int getShapeCount (); offentlig Shape getShapeAtIndex (int-indeks); // Relaterede topniveau indlejrede klasser og grænseflader offentlig grænseflade Lytter udvider EventListener {public void slateChanged (SlateModel.Event event); } public class Event udvider EventObject {public Event (SlateModel model) {super (model); }}} 

Og koden til Skifer ændres til:

// Slate.java import java.awt. *; import javax.swing.JComponent; import javax.swing.plaf.ComponentUI; public class Slate udvider JComponent implementerer SlateModel.Listener {public Slate (SlateModel model) {_model = model; _model.addSlateModelListener (dette); setOpaque (sand); setUI (ny Slate.UI ()); } public Slate () {this (new DefaultSlateModel ()); } offentlig SlateModel getModel () {return _model; } // Lytterimplementering offentlig ugyldig slateChanged (SlateModel.Event begivenhed) {repaint (); } offentlig statisk klasse UI udvider ComponentUI {public void paint (Grafik g, JComponent c) {SlateModel model = ((Slate) c) .getModel (); g.setColor (c.getForeground ()); Graphics2D g2D = (Graphics2D) g; til (int størrelse = model.getShapeCount (), i = 0; i <størrelse; i ++) {g2D.draw (model.getShapeAtIndex (i)); }}}} 

(Kildekoden til standardimplementeringen for den ændrede model, DefaultSlateModel, er i filen efter / DefaultSlateModel.java.)

Indenfor SlateModel klasse, er det unødvendigt at bruge fuldt kvalificerede navne til indlejrede klasser og grænseflader. For eksempel bare Lytter ville være tilstrækkelig i stedet for SlateModel.Lytter. Brug af fuldt kvalificerede navne hjælper dog udviklere, der kopierer metodesignaturer fra grænsefladen og indsætter dem i implementeringsklasser.

JFC og brug af indlejrede klasser

JFC-biblioteket bruger i visse tilfælde indlejrede klasser. For eksempel klasse BasicBorders i pakke javax.swing.plaf.basic definerer flere indlejrede klasser såsom BasicBorders.ButtonBorder. I dette tilfælde klasse BasicBorders har ingen andre medlemmer og fungerer simpelthen som en pakke. Brug af en separat pakke i stedet ville have været lige så effektiv, hvis ikke mere passende. Dette er en anden anvendelse end den, der præsenteres i denne artikel.

Brug af dette tips tilgang til JFC-design vil påvirke tilrettelæggelsen af ​​lyttere og begivenhedstyper relateret til modeltyper. For eksempel, javax.swing.event.TableModelListener og javax.swing.event.TableModelEvent ville blive implementeret henholdsvis som en indlejret grænseflade og en indlejret klasse indeni javax.swing.table.TableModel.

Denne ændring sammen med forkortelse af navnene ville resultere i en navngivet lyttergrænseflade javax.swing.table.TableModel.Listener og en begivenhedsklasse navngivet javax.swing.table.TableModel.Event. TableModel ville så være fuldstændig selvstændig med alle de nødvendige supportklasser og grænseflader i stedet for at have behov for supportklasser og interface spredt over tre filer og to pakker.

Retningslinjer for brug af indlejrede klasser

Som med ethvert andet mønster resulterer fornuftig brug af indlejrede klasser i design, der er enklere og lettere forstået end traditionel pakkeorganisation. Forkert brug fører imidlertid til unødvendig kobling, hvilket gør rollen som indlejrede klasser uklar.

Bemærk, at i det indlejrede eksempel ovenfor bruger vi kun indlejrede typer til typer, der ikke kan stå uden sammenhæng med omslutningstype. Vi laver for eksempel ikke SlateModel en indlejret grænseflade til Skifer fordi der kan være andre visningstyper, der bruger den samme model.

Hvis der er to klasser, skal du anvende følgende retningslinjer for at afgøre, om du skal bruge indlejrede klasser. Brug indlejrede klasser til kun at organisere dine klasser, hvis svaret på begge spørgsmål nedenfor er ja:

  1. Er det muligt klart at klassificere en af ​​klasserne som den primære klasse og den anden som en understøttende klasse?

  2. Er den understøttende klasse meningsløs, hvis den primære klasse fjernes fra delsystemet?

Konklusion

Mønsteret for at bruge indlejrede klasser parrer de relaterede typer tæt. Det undgår forurening af navneområdet ved hjælp af den omgivende type som navneområde. Det resulterer i færre kildefiler uden at miste muligheden for offentligt at udsætte understøttende typer.

Som med ethvert andet mønster, brug dette mønster med omtanke. Sørg især for, at indlejrede typer er virkelig beslægtede og ikke har nogen betydning uden den sammenhængende type. Korrekt brug af mønsteret øger ikke koblingen, men tydeliggør blot den eksisterende kobling.

Ramnivas Laddad er en Sun Certified Architect of Java Technology (Java 2). Han har en kandidatgrad i elektroteknik med speciale i kommunikationsteknik. Han har seks års erfaring med at designe og udvikle flere softwareprojekter, der involverer GUI, netværk og distribuerede systemer. Han har udviklet objektorienterede softwaresystemer i Java i de sidste to år og i C ++ i de sidste fem år. Ramnivas arbejder i øjeblikket hos Real-Time Innovations Inc. som softwareingeniør. På RTI arbejder han i øjeblikket med at designe og udvikle ControlShell, den komponentbaserede programmeringsramme til opbygning af komplekse realtidssystemer.