Forleden lyttede jeg til National Public Radio’s Car Talk, en populær ugentlig udsendelse, hvor opkaldene stiller spørgsmål om deres biler. Før hver programpause beder showets værter opkaldene om at ringe til 1-800-CAR-TALK, hvilket svarer til 1-800-227-8255. Førstnævnte er naturligvis meget lettere at huske end sidstnævnte, bl.a. fordi ordene “CAR TALK” er en sammensætning: to ord, der repræsenterer syv cifre. Mennesker finder det generelt lettere at håndtere sammensætninger end deres individuelle komponenter. Når man udvikler objektorienteret software, er det ligeledes ofte praktisk at manipulere sammensætninger på samme måde, som man manipulerer individuelle komponenter. Denne forudsætning udgør det grundlæggende princip for Composite-designmønstret, som er emnet for denne Java Design Patterns-aflevering.

Det sammensatte mønster

Hvor vi dykker ned i det sammensatte mønster, skal jeg først definere sammensatte objekter: objekter, der indeholder andre objekter; f.eks. kan en tegning være sammensat af grafiske primitivelementer, såsom linjer, cirkler, rektangler, tekst osv.

Java-udviklere har brug for Composite-mønstret, fordi vi ofte skal manipulere sammensatte objekter på nøjagtig samme måde, som vi manipulerer primitive objekter. F.eks. skal grafiske primitivelementer som linjer eller tekst tegnes, flyttes og ændres i størrelse. Men vi ønsker også at udføre den samme operation på kompositter, f.eks. tegninger, der er sammensat af disse primitive elementer. Ideelt set vil vi gerne udføre operationer på både primitive objekter og kompositter på nøjagtig samme måde, uden at skelne mellem de to. Hvis vi skal skelne mellem primitive objekter og composites for at udføre de samme operationer på de to typer objekter, vil vores kode blive mere kompleks og vanskeligere at implementere, vedligeholde og udvide.

I Design Patterns beskriver forfatterne Composite-mønstret således:

Sammensæt objekter i træstrukturer for at repræsentere del-hele hierarkier. Med Composite kan klienter behandle individuelle objekter og sammensætninger af objekter ensartet.

Det er nemt at implementere Composite-mønsteret. Composite-klasser udvider en basisklasse, der repræsenterer primitive objekter. Figur 1 viser et klassediagram, der illustrerer Composite-mønsterets struktur.

Figur 1. Et klassediagram i Composite-mønsteret

I figur 1’s klassediagram har jeg brugt klassens navne fra Design Patterns diskussion af Composite-mønsteret: Component repræsenterer en basisklasse (eller muligvis en grænseflade) for primitive objekter, og Composite repræsenterer en sammensat klasse. F.eks. kan Component-klassen repræsentere en basisklasse for grafiske primitiver, mens Composite-klassen kan repræsentere en Drawing-klasse. Figur 1’s Leaf-klasse repræsenterer et konkret primitivt objekt; f.eks. en Line-klasse eller en Text-klasse. Operation1()– og Operation2()-metoderne repræsenterer domænespecifikke metoder, der er implementeret af både Component– og Composite-klasserne.

Composite-klassen vedligeholder en samling af komponenter. Typisk implementeres Composite-metoder ved at iterere over denne samling og påkalde den relevante metode for hver Component i samlingen. En Drawing-klasse kan f.eks. implementere sin draw()-metode på følgende måde:

For hver metode, der er implementeret i Component-klassen, implementerer Composite-klassen en metode med samme signatur, der itererererer over kompositens komponenter, som illustreret ved draw()-metoden, der er anført ovenfor.

Klassen Composite udvider klassen Component, så du kan videregive en komposit til en metode, der forventer en komponent; tænk f.eks. på følgende metode:

// This method is implemented in a class that's unrelated to the// Component and Composite classespublic void repaint(Component component) { // The component can be a composite, but since it extends // the Component class, this method need not // distinguish between components and composites component.draw();}

Den foregående metode får overdraget en komponent – enten en simpel komponent eller en komposit – hvorefter den påkalder denne komponents draw()-metode. Da Composite-klassen udvider Component, behøver repaint()-metoden ikke at skelne mellem komponenter og kompositter – den påkalder blot draw()-metoden for komponenten (eller kompositten).

Figur 1’s klassediagram for kompositmønsteret illustrerer et problem med mønsteret: Du skal skelne mellem komponenter og kompositter, når du refererer til en Component, og du skal påkalde en kompositspecifik metode, f.eks. addComponent(). Du opfylder typisk dette krav ved at tilføje en metode, f.eks. isComposite(), til Component-klassen. Denne metode returnerer false for komponenter og overskrives i Composite-klassen til at returnere true. Derudover skal du også kaste Component-referencen til en Composite-instans, som her:

Bemærk, at addComponent()-metoden får overdraget en Component-reference, som enten kan være en primitiv komponent eller en sammensat komponent. Da denne komponent kan være en komposit, kan du sammensætte komponenter i en træstruktur, som angivet i det førnævnte citat fra Design Patterns.

Figur 2 viser en alternativ implementering af Composite-mønstret.

Figur 2. Et alternativt Composite-mønsterklassediagram

Hvis du implementerer Figur 2’s Composite-mønster, behøver du aldrig at skelne mellem komponenter og kompositter, og du behøver ikke at kaste en Component-reference til en Composite-instans. Så det ovenfor anførte kodefragment reduceres til en enkelt linje:

...component.addComponent(someComponentThatCouldBeAComposite);...

Men hvis Component-referencen i det foregående kodefragment ikke henviser til en Composite, hvad skal addComponent() så gøre? Det er et vigtigt stridspunkt i forbindelse med implementeringen af figur 2’s Composite-mønster. Da primitive komponenter ikke indeholder andre komponenter, giver det ingen mening at tilføje en komponent til en anden komponent, så Component.addComponent()-metoden kan enten fejle lydløst eller kaste en undtagelse. Typisk betragtes det som en fejl at føje en komponent til en anden primitiv komponent, så det er måske bedst at kaste en undtagelse.

Så hvilken implementering af Composite-mønstret – den i figur 1 eller den i figur 2 – fungerer bedst? Det er altid et emne, der er genstand for stor debat blandt dem, der implementerer Composite-mønstret; Design Patterns foretrækker implementeringen i figur 2, fordi du aldrig behøver at skelne mellem komponenter og containere, og fordi du aldrig behøver at udføre en cast. Personligt foretrækker jeg Figur 1’s implementering, fordi jeg har en stærk modvilje mod at implementere metoder i en klasse, som ikke giver mening for den pågældende objekttype.

Nu da du forstår Composite-mønstret, og hvordan du kan implementere det, skal vi undersøge et eksempel på Composite-mønstret med Apache Struts JavaServer Pages (JSP)-rammen (Apache Struts JavaServer Pages (JSP)).

Det sammensatte mønster og Struts Tiles

Apache Struts-rammen indeholder et JSP-tagbibliotek, kaldet Tiles, som giver dig mulighed for at sammensætte en webside fra flere JSP’er. Tiles er faktisk en implementering af J2EE-mønsteret (Java 2 Platform, Enterprise Edition) CompositeView-mønsteret, der selv er baseret på Design Patterns Composite-mønsteret. Inden vi diskuterer Composite-mønsterets relevans for Tiles-tagbiblioteket, skal vi først gennemgå grunden til Tiles, og hvordan man bruger det. Hvis du allerede er bekendt med Struts Tiles, kan du skimme de følgende afsnit og begynde at læse ved “Use the Composite Pattern with Struts Tiles.”

Note: Du kan læse mere om J2EE CompositeView-mønstret i min artikel “Web Application Components Made Easy with Composite View” (JavaWorld, december 2001).

Designere konstruerer ofte websider med et sæt af diskrete regioner; f.eks. består websiden i figur 3 af en sidebar, en header, en indholdsregion og en sidefod.

Figur 3. Det sammensatte mønster og Struts Tiles. Klik på miniaturebillede for at få vist billedet i fuld størrelse.

Websites omfatter ofte flere websider med identiske layouts, som f.eks. figur 3’s sidebar/overskrift/indhold/footer-layout. Med Struts Tiles kan du genbruge både indhold og layout mellem flere websider. Før vi diskuterer dette genbrug, skal vi se, hvordan Figur 3’s layout traditionelt implementeres med HTML alene.

Implementer komplekse layouts i hånden

Eksempel 1 viser, hvordan du kan implementere Figur 3’s webside med HTML:

Eksempel 1. Et komplekst layout implementeret i hånden

Den foregående JSP har to store ulemper: For det første er sidens indhold indlejret i JSP’en, så du kan ikke genbruge noget af det, selv om sidebjælken, overskriften og sidefoden sandsynligvis vil være den samme på tværs af mange websider. For det andet er sidens layout også indlejret i JSP’en, så du kan heller ikke genbruge det, selv om mange andre websider på det samme websted bruger det samme layout. Vi kan bruge <jsp:include>-aktionen til at afhjælpe den første ulempe, som jeg diskuterer i det følgende.

Implementer komplekse layouts med JSP includes

Eksempel 2 viser en implementering af webside i figur 3, der bruger <jsp:include>:

Eksempel 2. Et komplekst layout implementeret med JSP indeholder

Den foregående JSP indeholder indholdet af andre JSP’er med <jsp:include>. Fordi jeg har indkapslet dette indhold i separate JSP’er, kan du genbruge det til andre websider:

Eksempel 3. sidebar.jsp

For fuldstændighedens skyld har jeg nedenfor anført de JSP’er, der er inkluderet af den foregående JSP:

Eksempel 4. header.jsp

<font size='6'>Welcome to Sabreware, Inc.</font><hr>

Eksempel 5. content.jsp

<font size='4'>Page-specific content goes here</font>

Eksempel 6. footer.jsp

<hr>Thanks for stopping by!

Selv om JSP’en i eksempel 2 bruger <jsp:include> til at genbruge indhold, kan du ikke genbruge sidens layout, fordi det er hardcodet i denne JSP. Med Struts Tiles kan du genbruge både indholdet og layoutet, som illustreret i næste afsnit.

Implementer komplekse layouts med Struts Tiles

Eksempel 7 viser websiden i figur 3 implementeret med Struts Tiles:

Eksempel 7. Brug Struts Tiles til at indkapsle layout

Den foregående JSP bruger <tiles:insert>-tagget til at oprette JSP’en i figur 3. Denne JSP er defineret af en tiles-definition ved navn sidebar-header-footer-definition. Denne definition findes i Tiles-konfigurationsfilen, som i dette tilfælde er WEB-INF/tiles-defs.xml, der er anført i eksempel 8:

Eksempel 8. WEB-INF/tiles-defs.xml

Den foregående Tiles-definition angiver sidens layout, indkapslet i header-footer-sidebar-layout.jsp, og sidens indhold, indkapslet i sidebar.jsp, header.jsp, content.jsp og footer.jsp, som anført i Eksempel 3-6. Eksempel 9 viser den JSP, der definerer layoutet-header-footer-sidebar-layout.jsp:

Eksempel 9. header-footer-sidebar-layout.jsp

Den foregående JSP indkapsler layoutet og indsætter indhold i overensstemmelse med de værdier, der er angivet for sidebar, editor, indhold og footer-regioner i Tiles-definitionsfilen, hvilket letter genbrug for både indhold og layout. Du kan f.eks. definere en anden Tiles-definition med samme layout, samme sidebar, editor og sidefod, men med forskelligt indhold:

For at oprette den JSP, der er defineret af Tiles-definitionen a-different-sidebar-header-footer-definition, bruger du <tiles:insert>-tagget som her:

Takket være Struts Tiles kan du genbruge både indhold og layout, hvilket viser sig at være uvurderligt for websteder med mange JSP’er, der deler layout og noget indhold. Men hvis du ser nærmere på koden fra Eksempel 7-9, vil du bemærke, at layoutet for sidebarområdet er hardcodet i sidebar.jsp, som er angivet i Eksempel 3. Det betyder, at du ikke kan genbruge dette layout. Heldigvis implementerer Tiles-tagbiblioteket Composite-mønstret, som giver os mulighed for at angive en tiles-definition – i stedet for en JSP – for en region. I næste afsnit forklarer jeg, hvordan du bruger denne implementering af Composite-mønsteret.

Brug Composite-mønsteret med Struts Tiles

Struts Tiles implementerer Composite-mønsteret, hvor Component-klassen repræsenteres af JSP’er og Composite-klassen repræsenteres af en Tiles-definition. Med denne implementering kan du angive enten en JSP (en komponent) eller en Tiles-definition (en sammensat) som indhold for en JSP-region. Eksempel 10 illustrerer denne funktion:

Eksempel 10. WEB-INF/tiles-defs.xml: Brug det sammensatte mønster

Den foregående Tiles-konfigurationsfil definerer to Tiles-definitioner: sidebar-definition og sidebar-header-footer-definition. sidebar-definition er angivet som værdien for sidebarområdet i sidebar-header-footer-definition. Du kan angive den som sådan, fordi Tiles implementerer det sammensatte mønster ved at lade Tiles angive en definition (en Composite, der er en JSP-samling), hvor du normalt ville angive en enkelt JSP (som er en Component).

Sidebarens layout er indkapslet i eksempel 11’s sidebar-layout.jsp:

Eksempel 11. sidebar-layout.jsp

Eksempel 12 viser flags.jsp, der er angivet som indholdet for sidebarens top-region, og eksempel 13 viser sidebar-links.jsp, der er angivet som sidebarens bottom-region:

Eksempel 12. flags.jsp

Eksempel 13. sidebar-links.jsp

Nu kan sidebar-definition definere andre regioner med en øverste og nederste komponent, selv om du nok bør omdøbe denne definition til noget mere generisk som top-bottom-definition.

Det hele er kompositter for tiden

Kompositmønsteret er populært blandt præsentationsrammer som Swing og Struts, fordi det giver dig mulighed for at indlejre containere ved at behandle komponenter og deres containere nøjagtigt på samme måde. Struts Tiles bruger Composite-mønsteret til at angive en simpel JSP eller en Tiles-definition – som er en samling af JSP’er – som en flises indhold. Det er en effektiv mulighed, der letter administrationen af store websteder med forskellige layouts.

Det nedenstående afsnit “Hjemmeopgave fra sidst” udvider diskussionen i denne artikel ved at internationalisere den foregående applikation med en Struts-aktion og JSP Standard Tag Library (JSTL).

Hjemmeopgave

Diskuter, hvordan Swing implementerer det sammensatte mønster med Component– og Container-klasserne.

Hjemmeopgave fra sidst

I din sidste opgave blev du bedt om at downloade Struts fra http://jakarta.apache.org/struts/index.html og implementere din egen Struts-actionklasse.

I denne opgave besluttede jeg mig for at implementere en Struts-action sammen med JSP Standard Tag Library (JSTL) for at internationalisere eksempel 1’s webapplikation. Selv om Struts leverer den nødvendige infrastruktur til internationalisering af dine webapplikationer, bør du bruge JSTL til denne opgave, fordi JSTL er en standard. På et tidspunkt vil Struts’ internationaliseringsmuligheder sandsynligvis blive forældet eller integreret med JSTL.

Når jeg internationaliserede eksempel 1’s webapplikation med en Struts-aktion og JSTL, lokaliserede jeg denne applikation til kinesisk. Figur H1 illustrerer resultatet.

Bemærk: Jeg kender ikke et eneste ord kinesisk, og slet ikke hvordan man skriver sproget, så figur H1’s kinesisk er fremstillet ud fra vilkårlige Unicode-strenge. Men det ser alligevel cool ud.

Figur H1. Internationalisering med en Struts-aktion. Klik på miniaturebilledet for at få vist billedet i fuld størrelse.

Bemærk, at jeg specificerede href-attributterne i eksempel 12’s flags.jsp som tomme strenge, så når du klikker på flagene, genindlæser servletcontaineren den aktuelle JSP. Derfor er det første skridt i retning af internationalisering af webapplikationen at angive en URL for disse attributter som anført i eksempel H1:

Eksempel H1. flags.jsp

URL’en for hver href-attribut er den samme: flags.do. Der er to anmodningsparametre hæftet på denne URL: en for det sprogområde, der svarer til flaget, og en anden, der repræsenterer den aktuelle JSP’s sti. Sidstnævnte anmodningsparameter fås ved at påkalde Http.ServletRequest.getServletPath()-metoden.

I applikationens implementeringsbeskrivelse har jeg kortlagt alle URL’er, der slutter på .do, til Struts-handlingsservletten på følgende måde:

Eksempel H2. WEB-INF/web.xml (uddrag)

Notat: Se min artikel “Take Command of Your Software” (JavaWorld, juni 2002) for at få flere oplysninger om Struts og Struts-handlingsservletten.

Næst mappede jeg stien /flags til Struts-handlingen actions.FlagAction i Struts-konfigurationsfilen, som er anført i eksempel H3:

Eksempel H3. WEB-INF/struts-config.xml

På grund af denne tilknytning får URL-adressen flags.do Struts-handlingsservletten til at påkalde actions.FlagAction.execute()-metoden; derfor vil et klik på et flag påkalde actions.FlagAction.execute()-metoden. Eksempel H4 viser actions.FlagAction-klassen:

Eksempel H4. WEB-INF/classes/actions/FlagAction.java

Metoden actions.FlagAction.execute() indhenter en reference til locale-anmodningsparameteren og videregiver denne værdi til JSTL Config-klassens set() metode. Denne metode gemmer strengen, der repræsenterer lokalområdet, i FMT_LOCALE-konfigurationsindstillingen, som JSTL bruger til at lokalisere tekst og formatere tal, valutaer, procenter og datoer.

Nu, hvor jeg har angivet et lokalområde for JSTL-internationaliseringshandlinger, angiver jeg som det næste et ressourcepakke i programmets implementeringsbeskrivelse, hvoraf et uddrag er anført i eksempel H5:

Eksempel H5. WEB-INF/web.xml (Uddrag)

Ressourcebundle-basisnavnet resources er angivet for parameteren javax.servlet.jsp.jstl.fmt.localizationContext kontekst-initialisering. Det betyder, at JSTL vil søge efter en fil, der hedder resources_en.properties, når lokaliteten er indstillet til britisk engelsk (en-GB), og resources_zh.properties, når lokaliteten er indstillet til kinesisk (zh-ZH).

Nu da jeg har angivet et ressourcebundle og en lokalitet for JSTL’s internationaliseringshandlinger, ændrer jeg som det næste JSP-filerne, så de bruger disse handlinger, hvilket eksemplerne H6-H9 viser:

Eksempel H6. sidebar-links.jsp

Eksempel H7. header.jsp

Eksempel H8. content.jsp

Eksempel H9. footer.jsp

Eksempler H6-H9’s JSP’er bruger JSTL <fmt:message>-aktionen til at udtrække Unicode-strenge fra det ressourcepakke, der er angivet i implementeringsbeskrivelsen. Eksempel H10 og H11 viser ressourcebundlerne for henholdsvis engelsk og kinesisk:

Eksempel H10. WEB-INF/classes/resources_en.properties

Eksempel H11. WEB-INF/classes/resources_zh.properties

E-mail

I en e-mail til mig skrev Joseph Friedman:

I “Decorate Your Java Code” (JavaWorld, december 2001) skriver du:

“Det omsluttende objekt – kendt som en dekorator – er i overensstemmelse med grænsefladen for det objekt, det omslutter, hvilket gør det muligt at bruge dekoratoren, som om den var en instans af det objekt, den omslutter.”

Derimod dekorerer kodeeksemplet FileReader med en LineNumberReader og kalder readLine(), som ikke er i FileReaders grænseflade; du bruger således ikke LineNumberReader på gennemsigtig vis.”

Både FileReader og LineNumberReader er i overensstemmelse med den grænseflade, der er defineret af Reader-klassen, som de begge udvider. Ideen er, at du kan overdrage dekoratoren til enhver metode, der forventer en reference til en Reader. Disse metoder forbliver lykkeligt uvidende om denne decorators særlige evner – i dette tilfælde evnen til at læse filer og producere linjenumre. Men hvis du kender til disse særlige evner, kan du drage fordel af dem.

Den omstændighed, at dekoratoren besidder (en eller flere) metoder, som det dekorerede objekt mangler, er på ingen måde i strid med Decorator-mønsterets hensigt; det er faktisk det centrale træk ved dette mønster: at tilføje funktionalitet på køretid til et objekt ved at dekorere objekter rekursivt.

Hvis du kigger på Design Patterns side 173, vil du se dette:

Decorator-underklasser kan frit tilføje operationer for specifik funktionalitet. F.eks. kan ScrollDecorators ScrollTo-operation lade andre objekter rulle i grænsefladen *hvis* de ved, at der tilfældigvis er et ScrollDecorator-objekt i grænsefladen.

DavidGeary er forfatter til Core JSTL Mastering the JSP Standard TagLibrary, som udkommer til efteråret hos Prentice-Hall og Sun Microsystems Press; Advanced JavaServer Pages (PrenticeHall, 2001; ISBN: 013030307041); og Graphic Java-serien (Sun MicrosystemsPress). David har i 18 år udviklet objektorienteret software med en lang række objektorienterede sprog. Siden GOFDesign Patterns-bogen blev udgivet i 1994, har David været en aktiv fortaler for designmønstre og har brugt og implementeret designmønstre i Smalltalk, C++ og Java. I 1997 begyndte David at arbejde på fuld tid som forfatter og lejlighedsvis som taler og konsulent. David er medlem af de ekspertgrupper, der definerer JSP-standardbiblioteket for brugerdefinerede tags og JavaServer Faces, og han bidrager til Apache Struts JSP-rammen.

Lær mere om dette emne

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.