Programmering

Java Tips 75: Bruk nestede klasser for bedre organisering

Et typisk delsystem i en Java-applikasjon består av et sett med samarbeidende klasser og grensesnitt, som hver utfører en bestemt rolle. Noen av disse klassene og grensesnittene er meningsfulle bare i sammenheng med andre klasser eller grensesnitt.

Å designe kontekstavhengige klasser som nestede klasser på toppnivå (nestede klasser, kort) som er lukket av kontekstbetjeningsklassen, gjør denne avhengigheten tydeligere. Videre gjør bruk av nestede klasser samarbeidet lettere å gjenkjenne, unngår forurensning av navneområdet og reduserer antall kildefiler.

(Den komplette kildekoden for dette tipset kan lastes ned i zip-format fra seksjonen Ressurser.)

Nestede klasser vs. indre klasser

Nestede klasser er ganske enkelt statiske indre klasser. Forskjellen mellom nestede klasser og indre klasser er den samme som forskjellen mellom statiske og ikke-statiske medlemmer av en klasse: nestede klasser er assosiert med selve den omsluttende klassen, mens indre klasser er assosiert med et objekt fra den omslutende klassen.

På grunn av dette krever indre klasseobjekter et objekt fra den omsluttende klassen, mens nestede klasseobjekter ikke gjør det. Nestede klasser oppfører seg derfor som klasser på toppnivå ved å bruke den vedlagte klassen for å gi en pakke-lignende organisasjon. I tillegg har nestede klasser tilgang til alle medlemmene i klassen som inngår.

Motivasjon

Tenk på et typisk Java-delsystem, for eksempel en Swing-komponent, ved å bruke design-mønsteret Model-View-Controller (MVC). Hendelsesobjekter innkapsler endringsvarsler fra modellen. Visninger registrerer interesse for ulike hendelser ved å legge til lyttere til den underliggende modellen for komponenten. Modellen varsler seerne om endringer i sin egen tilstand ved å levere disse hendelsesobjektene til sine registrerte lyttere. Ofte er disse lytter- og hendelsestypene spesifikke for modelltypen, og gir derfor mening bare i sammenheng med modelltypen. Fordi hver av disse lytter- og hendelsestypene må være offentlig tilgjengelige, må hver være i sin egen kildefil. I denne situasjonen er koblingen mellom disse typene vanskelig å gjenkjenne med mindre noen kodekonvensjoner brukes. Selvfølgelig kan man bruke en egen pakke for hver gruppe for å vise koblingen, men dette resulterer i et stort antall pakker.

Hvis vi implementerer lytter- og hendelsestyper som nestede typer av modellgrensesnittet, gjør vi koblingen åpenbar. Vi kan bruke hvilken som helst tilgangsmodifikator som ønskes med disse nestede typene, inkludert offentlig. I tillegg, da nestede typer bruker det vedlagte grensesnittet som et navneområde, refererer resten av systemet til dem som ., unngå forurensning av navneområdet i pakken. Kildefilen for modellgrensesnittet har alle de støttende typene, noe som gjør utvikling og vedlikehold enklere.

Før: Et eksempel uten nestede klasser

Som et eksempel utvikler vi en enkel komponent, Skifer, hvis oppgave er å tegne former. Akkurat som Swing-komponenter bruker vi MVC-mønsteret. Modellen, SlateModel, fungerer som et lager for former. SlateModelListeners abonnere på endringene i modellen. Modellen varsler lytterne sine ved å sende hendelser av typen SlateModelEvent. I dette eksemplet trenger vi tre kildefiler, en for hver klasse:

// SlateModel.java importerer java.awt.Shape; offentlig grensesnitt SlateModel {// Listener management public void addSlateModelListener (SlateModelListener l); offentlig tomrum removeSlateModelListener (SlateModelListener l); // Administrasjon av formdepot, visninger trenger melding offentlig ugyldig addShape (Shape s); offentlig tomrom removeShape (Shape s); offentlig ugyldig removeAllShapes (); // Shape repository read-only operations public int getShapeCount (); offentlig Shape getShapeAtIndex (int-indeks); } 
// SlateModelListener.java importerer java.util.EventListener; offentlig grensesnitt SlateModelListener utvider EventListener {public void slateChanged (SlateModelEvent event); } 
// SlateModelEvent.java importerer java.util.EventObject; offentlig klasse SlateModelEvent utvider EventObject {public SlateModelEvent (SlateModel model) {super (model); }} 

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

Deretter retter vi oppmerksomheten mot Skifer, en visning for denne modellen, som videresender sin maleroppgave til UI-delegaten, SlateUI:

// Slate.java import javax.swing.JComponent; offentlig klasse Slate utvider JComponent implementerer SlateModelListener {private SlateModel _model; public Slate (SlateModel model) {_model = model; _model.addSlateModelListener (dette); setOpaque (true); setUI (ny SlateUI ()); } public Slate () {this (new DefaultSlateModel ()); } offentlig SlateModel getModel () {return _model; } // Listener implementering public void slateChanged (SlateModelEvent event) {repaint (); }} 

Endelig, SlateUI, den visuelle GUI-komponenten:

// SlateUI.java importerer java.awt. *; importere javax.swing.JComponent; importere javax.swing.plaf.ComponentUI; offentlig klasse SlateUI utvider ComponentUI {public void paint (Grafikk 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)); }}} 

Etter: Et modifisert eksempel ved bruk av nestede klasser

Klassestrukturen i eksemplet ovenfor viser ikke forholdet mellom klassene. For å redusere dette har vi brukt en navngivningskonvensjon som krever at alle relaterte klasser har et felles prefiks, men det ville være tydeligere å vise forholdet i kode. Videre må utviklere og vedlikeholdere av disse klassene administrere tre filer: for SlateModel, for SlateEvent, og for SlateListener, for å implementere ett konsept. Det samme gjelder ved å administrere de to filene for Skifer og SlateUI.

Vi kan forbedre ting ved å lage SlateModelListener og SlateModelEvent nestede typer av SlateModel grensesnitt. Fordi disse nestede typene er inne i et grensesnitt, er de implisitt statiske. Likevel har vi brukt en eksplisitt statisk erklæring for å hjelpe vedlikeholdsprogrammereren.

Kundekode vil referere til dem som SlateModel.SlateModelListener og SlateModel.SlateModelEvent, men dette er overflødig og unødvendig langt. Vi fjerner prefikset SlateModel fra de nestede klassene. Med denne endringen vil klientkoden referere til dem som SlateModel.Listener og SlateModel.Event. Dette er kort og tydelig og avhenger ikke av kodingsstandarder.

Til SlateUI, vi gjør det samme - vi gjør det til en nestet klasse av Skifer og endre navnet til UI. Fordi det er en nestet klasse i en klasse (og ikke i et grensesnitt), må vi bruke en eksplisitt statisk modifikator.

Med disse endringene trenger vi bare en fil for de modellrelaterte klassene og en til for de visningsrelaterte klassene. De SlateModel koden blir nå:

// SlateModel.java importerer java.awt.Shape; importere java.util.EventListener; importere java.util.EventObject; offentlig grensesnitt SlateModel {// Listener management public void addSlateModelListener (SlateModel.Listener l); offentlig ugyldig removeSlateModelListener (SlateModel.Listener l); // Administrasjon av formdepot, visninger trenger melding offentlig ugyldig addShape (Shape s); offentlig tomrom removeShape (Shape s); offentlig ugyldig removeAllShapes (); // Shape repository read-only operations public int getShapeCount (); offentlig Shape getShapeAtIndex (int-indeks); // Relatert toppnivå nestede klasser og grensesnitt offentlig grensesnitt Lytter utvider EventListener {public void slateChanged (SlateModel.Event event); } public class Event utvider EventObject {public Event (SlateModel model) {super (model); }}} 

Og koden for Skifer endres til:

// Slate.java importerer java.awt. *; importere javax.swing.JComponent; importere javax.swing.plaf.ComponentUI; public class Slate utvider JComponent implementerer SlateModel.Listener {public Slate (SlateModel model) {_model = model; _model.addSlateModelListener (dette); setOpaque (true); setUI (ny Slate.UI ()); } public Slate () {this (new DefaultSlateModel ()); } offentlig SlateModel getModel () {return _model; } // Listener implementering public void slateChanged (SlateModel.Event event) {repaint (); } offentlig statisk klasse UI utvider ComponentUI {public void paint (Grafikk 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)); }}}} 

(Kildekoden for standardimplementeringen for den endrede modellen, DefaultSlateModel, er i filen etter / DefaultSlateModel.java.)

Innen SlateModel klasse, er det unødvendig å bruke fullt kvalifiserte navn for nestede klasser og grensesnitt. For eksempel bare Lytter ville være tilstrekkelig i stedet for SlateModel.Listener. Bruk av fullt kvalifiserte navn hjelper imidlertid utviklere som kopierer metodesignaturer fra grensesnittet og limer dem inn i implementeringsklasser.

JFC og bruk av nestede klasser

JFC-biblioteket bruker nestede klasser i visse tilfeller. For eksempel klasse BasicBorders i pakke javax.swing.plaf.basic definerer flere nestede klasser som BasicBorders.ButtonBorder. I dette tilfellet, klasse BasicBorders har ingen andre medlemmer og fungerer rett og slett som en pakke. Å bruke en egen pakke i stedet ville ha vært like effektiv, om ikke mer hensiktsmessig. Dette er en annen bruk enn den som presenteres i denne artikkelen.

Å bruke dette tipset tilnærming i JFC-design vil påvirke organisasjonen av lyttere og hendelsestyper relatert til modelltyper. For eksempel, javax.swing.event.TableModelListener og javax.swing.event.TableModelEvent vil bli implementert henholdsvis som et nestet grensesnitt og en nestet klasse inni javax.swing.table.TableModel.

Denne endringen, sammen med forkortelse av navnene, vil resultere i et lyttergrensesnitt som heter javax.swing.table.TableModel.Listener og en arrangementsklasse kalt javax.swing.table.TableModel.Event. TableModel ville da være helt selvstendig med alle nødvendige støtteklasser og grensesnitt i stedet for å ha behov for støtteklasser og grensesnitt spredt over tre filer og to pakker.

Retningslinjer for bruk av nestede klasser

Som med ethvert annet mønster, medfører hensynsfull bruk av nestede klasser design som er enklere og lettere forstått enn tradisjonell pakkeorganisasjon. Imidlertid fører feil bruk til unødvendig kobling, noe som gjør rollen til nestede klasser uklar.

Merk at i det nestede eksemplet ovenfor bruker vi bare nestede typer for typer som ikke kan stå uten kontekst av innelukkende type. Vi lager for eksempel ikke SlateModel et nestet grensesnitt av Skifer fordi det kan være andre visningstyper som bruker samme modell.

Gitt noen to klasser, bruk følgende retningslinjer for å avgjøre om du skal bruke nestede klasser. Bruk nestede klasser til å organisere klassene dine bare hvis svaret på begge spørsmålene nedenfor er ja:

  1. Er det mulig å tydelig klassifisere en av klassene som primærklassen og den andre som en støtteklasse?

  2. Er støtteklassen meningsløs hvis primærklassen fjernes fra delsystemet?

Konklusjon

Mønsteret for å bruke nestede klasser parer de relaterte typene tett. Det unngår forurensning av navneområdet ved å bruke den vedlagte typen som navneområde. Det resulterer i færre kildefiler, uten å miste muligheten til å avsløre støttetyper offentlig.

Som med alle andre mønstre, bruk dette mønsteret med omhu. Spesielt må du sørge for at nestede typer er virkelig beslektede og ikke har noen betydning uten konteksten til den vedlagte typen. Riktig bruk av mønsteret øker ikke koblingen, men tydeliggjør bare den eksisterende koblingen.

Ramnivas Laddad er en Sun Certified Architect of Java Technology (Java 2). Han har en mastergrad i elektroteknikk med spesialisering i kommunikasjonsteknikk. Han har seks års erfaring med å designe og utvikle flere programvareprosjekter som involverer GUI, nettverk og distribuerte systemer. Han har utviklet objektorienterte programvaresystemer i Java de siste to årene og i C ++ de siste fem årene. Ramnivas jobber for tiden i Real-Time Innovations Inc. som programvareingeniør. På RTI jobber han for tiden med å designe og utvikle ControlShell, det komponentbaserte programmeringsrammeverket for å bygge komplekse sanntidssystemer.
$config[zx-auto] not found$config[zx-overlay] not found