Häromdagen lyssnade jag på National Public Radios Car Talk, en populär veckosändning där uppringare ställer frågor om sina fordon. Före varje programpaus ber programledarna uppringarna att ringa 1-800-CAR-TALK, vilket motsvarar 1-800-227-8255. Det förstnämnda numret är naturligtvis mycket lättare att komma ihåg än det sistnämnda, delvis på grund av att orden ”CAR TALK” är en sammansättning: två ord som representerar sju siffror. Människor tycker i allmänhet att det är lättare att hantera sammansättningar än de enskilda komponenterna. När man utvecklar objektorienterad programvara är det ofta bekvämt att manipulera kompositer på samma sätt som man manipulerar enskilda komponenter. Den förutsättningen utgör den grundläggande principen för designmönstret Composite, som är ämnet för den här delen av Java Design Patterns.

Mönstret Composite

För att vi ska kunna dyka ner i mönstret Composite måste jag först definiera kompositobjekt: objekt som innehåller andra objekt; en ritning kan t.ex. vara sammansatt av grafiska primitiva objekt, t.ex. linjer, cirklar, rektanglar, text, och så vidare.

Javautvecklare behöver Composite-mönstret eftersom vi ofta måste manipulera sammansatta objekt på exakt samma sätt som vi manipulerar primitiva objekt. Exempelvis måste grafiska primitiva objekt som linjer eller text ritas, flyttas och ändra storlek. Men vi vill också utföra samma operation på kompositer, t.ex. ritningar, som består av dessa primitiva objekt. Helst vill vi utföra operationer på både primitiva objekt och kompositer på exakt samma sätt, utan att skilja mellan de två. Om vi måste skilja mellan primitiva objekt och kompositer för att utföra samma operationer på dessa två typer av objekt skulle vår kod bli mer komplex och svårare att implementera, underhålla och utöka.

I Design Patterns beskriver författarna kompositmönstret så här:

Sammansätt objekt i trädstrukturer för att representera del-hel-hierarkier. Med Composite kan klienter behandla enskilda objekt och sammansättningar av objekt på ett enhetligt sätt.

Det är enkelt att implementera Composite-mönstret. Kompositklasser utökar en basklass som representerar primitiva objekt. Figur 1 visar ett klassdiagram som illustrerar Composite-mönstrets struktur.

Figur 1. Ett klassdiagram för Composite-mönstret

I figur 1:s klassdiagram har jag använt klassnamn från Design Patterns diskussion om Composite-mönstret: Component representerar en basklass (eller möjligen ett gränssnitt) för primitiva objekt, och Composite representerar en sammansatt klass. Exempelvis kan klassen Component representera en basklass för grafiska primitiva objekt, medan klassen Composite kan representera en Drawing-klass. Figur 1:s Leaf-klass representerar ett konkret primitivt objekt, till exempel en Line-klass eller en Text-klass. Metoderna Operation1() och Operation2() representerar domänspecifika metoder som implementeras av både Component– och Composite-klasserna.

Klassen Composite upprätthåller en samling komponenter. Typiskt sett implementeras Composite-metoder genom att iterera över denna samling och anropa lämplig metod för varje Component i samlingen. Till exempel kan en Drawing-klass implementera sin draw()-metod så här:

För varje metod som implementeras i Component-klassen implementerar Composite-klassen en metod med samma signatur som itererar över kompositens komponenter, vilket illustreras av draw()-metoden som anges ovan.

Klassen Composite utökar klassen Component, så du kan överlämna en komposit till en metod som förväntar sig en komponent; tänk till exempel på följande metod:

// 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 föregående metoden överlämnas en komponent – antingen en enkel komponent eller en komposit – och anropar sedan den komponentens draw() metod. Eftersom klassen Composite förlänger Component behöver repaint()-metoden inte skilja mellan komponenter och kompositer – den åberopar helt enkelt draw()-metoden för komponenten (eller kompositen).

Figur 1:s klassdiagram för kompositmönstret illustrerar ett problem med mönstret: du måste skilja mellan komponenter och kompositer när du refererar till en Component och du måste åberopa en kompositspecifik metod, till exempel addComponent(). Du uppfyller vanligtvis detta krav genom att lägga till en metod, till exempel isComposite(), till klassen Component. Den metoden returnerar false för komponenter och överordnas i Composite-klassen för att returnera true. Dessutom måste du också casta Component-referensen till en Composite-instans, så här:

Bemärk att addComponent()-metoden får en Component-referens, som kan vara antingen en primitiv komponent eller en komposit. Eftersom komponenten kan vara en komposit kan du komponera komponenter till en trädstruktur, vilket indikeras av det tidigare nämnda citatet från Design Patterns.

Figur 2 visar en alternativ implementering av kompositmönstret.

Figur 2. Ett alternativt klassdiagram för Composite-mönstret

Om du implementerar Composite-mönstret i figur 2 behöver du aldrig skilja mellan komponenter och kompositer, och du behöver inte casta en Component-referens till en Composite-instans. Så kodfragmentet ovan reduceras till en enda rad:

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

Men om Component-referensen i det föregående kodfragmentet inte hänvisar till en Composite, vad ska då addComponent() göra? Det är en viktig punkt som är oklar med genomförandet av figur 2:s kompositmönster i figur 2. Eftersom primitiva komponenter inte innehåller andra komponenter är det meningslöst att lägga till en komponent till en annan komponent, så Component.addComponent()-metoden kan antingen misslyckas tyst eller kasta ett undantag. Att lägga till en komponent till en annan primitiv komponent betraktas vanligtvis som ett fel, så att kasta ett undantag är kanske det bästa tillvägagångssättet.

Så vilken implementering av Composite-mönstret – den i figur 1 eller den i figur 2 – fungerar bäst? Det är alltid ett ämne som debatteras flitigt bland dem som implementerar Composite-mönstret; Design Patterns föredrar implementationen i figur 2 eftersom du aldrig behöver skilja mellan komponenter och behållare, och du behöver aldrig utföra en kastning. Personligen föredrar jag Figur 1:s implementering, eftersom jag har en stark motvilja mot att implementera metoder i en klass som inte är meningsfulla för den objekttypen.

Nu när du förstår Composite-mönstret och hur du kan implementera det ska vi undersöka ett exempel på Composite-mönstret med ramverket Apache Struts JavaServer Pages (JSP).

Mönstret Composite och Struts Tiles

Apache Struts ramverk innehåller ett JSP-taggbibliotek, kallat Tiles, som gör att du kan komponera en webbsida från flera JSP:er. Tiles är egentligen en implementering av J2EE-mönstret (Java 2 Platform, Enterprise Edition) CompositeView, som i sin tur bygger på Design Patterns Composite-mönstret. Innan vi diskuterar Composite-mönstrets relevans för Tiles-taggbiblioteket ska vi först gå igenom varför Tiles finns och hur man använder det. Om du redan är bekant med Struts Tiles kan du skippa de följande avsnitten och börja läsa vid ”Use the Composite Pattern with Struts Tiles”

Note: Du kan läsa mer om J2EE CompositeView-mönstret i min artikel ”Web Application Components Made Easy with Composite View” (JavaWorld, december 2001).

Designers konstruerar ofta webbsidor med en uppsättning diskreta regioner; till exempel består webbsidan i figur 3 av en sidebar, en rubrik, en innehållsregion och en sidfot.

Figur 3. Mönstret Composite och Struts Tiles. Klicka på miniatyrbilden för att se bilden i full storlek.

Websidor innehåller ofta flera webbsidor med identiska layouter, t.ex. figur 3:s sidebar/header/content/footer-layout. Med Struts Tiles kan du återanvända både innehåll och layout på flera webbsidor. Innan vi diskuterar återanvändning ska vi se hur figur 3:s layout traditionellt implementeras med enbart HTML.

Implementera komplexa layouter för hand

Exempel 1 visar hur du kan implementera figur 3:s webbsida med HTML:

Exempel 1. En komplex layout som implementeras för hand

Den föregående JSP:n har två stora nackdelar: För det första är sidans innehåll inbäddat i JSP:n, så du kan inte återanvända något av det, även om sidofältet, rubriken och sidfoten sannolikt är desamma på många webbsidor. För det andra är sidans layout också inbäddad i JSP, så du kan inte heller återanvända den, även om många andra webbsidor på samma webbplats använder samma layout. Vi kan använda åtgärden <jsp:include> för att avhjälpa den första nackdelen, vilket jag diskuterar härnäst.

Implementera komplexa layouter med JSP includes

Exempel 2 visar en implementering av webbsidan i figur 3 som använder <jsp:include>:

Exempel 2. En komplex layout som genomförs med JSP innehåller

Den föregående JSP innehåller innehållet i andra JSP:er med <jsp:include>. Eftersom jag har kapslat in detta innehåll i separata JSP:er kan du återanvända det för andra webbsidor:

Exempel 3. sidebar.jsp

För fullständighetens skull har jag nedan listat de JSP:er som ingår i den föregående JSP:

Exempel 4. header.jsp

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

Exempel 5. content.jsp

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

Exempel 6. footer.jsp

<hr>Thanks for stopping by!

Även om exempel 2:s JSP använder <jsp:include> för att återanvända innehåll, kan du inte återanvända sidans layout, eftersom den är hårdkodad i det JSP:et. Med Struts Tiles kan du återanvända både innehåll och layout, vilket illustreras i nästa avsnitt.

Implementera komplexa layouter med Struts Tiles

Exempel 7 visar webbsidan i Figur 3 som implementerats med Struts Tiles:

Exempel 7. Använd Struts Tiles för att kapsla in layout

I föregående JSP används taggen <tiles:insert> för att skapa JSP:n för figur 3. Den JSP:n definieras av en tiles-definition som heter sidebar-header-footer-definition. Den definitionen finns i konfigurationsfilen Tiles, som i det här fallet är WEB-INF/tiles-defs.xml och som anges i exempel 8:

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

Den föregående Tiles-definitionen anger sidans layout, inkapslad i header-footer-sidebar-layout.jsp, och sidans innehåll, inkapslat i sidebar.jsp, header.jsp, content.jsp och footer.jsp, enligt exempel 3-6. Exempel 9 visar den JSP som definierar layouten-header-footer-sidebar-layout.jsp:

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

Den föregående JSP:n kapslar in layouten och infogar innehåll enligt de värden som anges för sidebar, redaktör, innehåll och sidfotsregioner i Tiles-definitionsfilen, vilket underlättar återanvändning för både innehåll och layout. Du kan till exempel definiera en annan Tiles-definition med samma layout, samma sidebar, redaktör och sidfot, men med ett annat innehåll:

För att skapa den JSP som definieras av Tiles-definitionen a-different-sidebar-header-footer-definition använder du taggen <tiles:insert>, så här:

Med hjälp av Struts Tiles kan du återanvända både innehåll och layout, vilket är ovärderligt för webbplatser med många JSP:er som har samma layout och visst innehåll. Men om du tittar närmare på koden från exempel 7-9 kommer du att märka att layouten för sidofältet är hårdkodad i sidebar.jsp, som anges i exempel 3. Det betyder att du inte kan återanvända den layouten. Som tur är implementerar biblioteket Tiles taggbiblioteket Composite-mönstret, vilket gör att vi kan ange en tiles-definition – i stället för en JSP – för en region. I nästa avsnitt förklarar jag hur man använder denna implementering av Composite-mönstret.

Använd Composite-mönstret med Struts Tiles

Struts Tiles implementerar Composite-mönstret, där Component-klassen representeras av JSP:er och Composite-klassen representeras av en Tiles-definition. Med den implementeringen kan du ange antingen en JSP (en komponent) eller en Tiles-definition (en komposit) som innehåll för en JSP-region. Exempel 10 illustrerar denna funktion:

Exempel 10. WEB-INF/tiles-defs.xml: Använd det sammansatta mönstret

Den föregående Tiles-konfigurationsfilen definierar två Tiles-definitioner: sidebar-definition och sidebar-header-footer-definition. sidebar-definition specificeras som värde för sidobarregionen i sidebar-header-footer-definition. Du kan ange den som sådan eftersom Tiles implementerar Composite-mönstret genom att låta Tiles ange en definition (en Composite som är en JSP-samling) där du normalt skulle ange en enskild JSP (som är en Component).

Sidebalkens layout är inkapslad i exempel 11:s sidebar-layout.jsp:

Exempel 11. sidebar-layout.jsp

Exempel 12 listar flags.jsp, specificerat som innehåll för sidofältets top-region, och exempel 13 listar sidebar-links.jsp, specificerat som sidofältets bottom-region:

Exempel 12. flags.jsp

Exempel 13. sidebar-links.jsp

Nu kan sidebar-definition definiera andra regioner med en topp- och en bottenkomponent, även om du antagligen bör döpa om den definitionen till något mer generiskt som top-bottom-definition.

Allt är kompositmaterial nuförtiden

Mönstret för kompositmaterial är populärt bland presentationsramverk, till exempel Swing och Struts, eftersom det gör det möjligt att nästla in behållare genom att behandla komponenter och deras behållare exakt lika. Struts Tiles använder Composite-mönstret för att ange en enkel JSP eller en Tiles-definition – som är en samling JSP – som innehåll i en kakel. Det är en kraftfull funktion som underlättar hanteringen av stora webbplatser med olika layouter.

Det nedanstående avsnittet ”Hemläxa från sist” utökar diskussionen i den här artikeln genom att internationalisera den föregående applikationen med en Struts-åtgärd och JSP Standard Tag Library (JSTL).

Hemläxa

Diskutera hur Swing implementerar kompositmönstret med klasserna Component och Container.

Hemläxa från förra gången

I din förra uppgift ombads du att ladda ner Struts från http://jakarta.apache.org/struts/index.html och implementera en egen Struts-åtgärdsklass.

För den här uppgiften bestämde jag mig för att implementera en Struts-åtgärd tillsammans med JSP Standard Tag Library (JSTL) för att internationalisera exempel 1:s webbprogram. Även om Struts tillhandahåller den nödvändiga infrastrukturen för att internationalisera dina webbapplikationer bör du använda JSTL för den uppgiften, eftersom JSTL är en standard. Vid någon tidpunkt kommer Struts internationaliseringsfunktioner förmodligen att avvecklas eller integreras med JSTL.

När jag internationaliserade exempel 1:s webbapplikation med en Struts-åtgärd och JSTL lokaliserade jag applikationen för kinesiska. Figur H1 illustrerar resultatet.

Notera: Jag kan inte ett enda ord kinesiska, än mindre hur man skriver språket, så figur H1:s kinesiska är tillverkad av godtyckliga Unicode-strängar. Men det ser ändå häftigt ut.

Figur H1. Internationalisering med en Struts-åtgärd. Klicka på miniatyrbilden för att se bilden i full storlek.

Märk att jag specificerade href-attributen i exempel 12:s flags.jsp som tomma strängar, så när du klickar på flaggorna laddar servletcontainern om den aktuella JSP:en. Därför är det första steget mot internationalisering av webbapplikationen att ange en URL för dessa attribut, enligt exempel H1:

Exempel H1. flags.jsp

Urlänsen för varje href-attribut är densamma: flags.do. Två förfrågningsparametrar är hängda på denna URL: en för den lokal som motsvarar flaggan och en annan som representerar den aktuella JSP:s sökväg. Du får den sistnämnda begäransparametern genom att anropa metoden Http.ServletRequest.getServletPath().

I applikationens distributionsbeskrivning mappade jag alla webbadresser som slutar på .do till Struts action-servlet, på följande sätt:

Exempel H2. WEB-INF/web.xml (Utdrag)

Note: Se min artikel ”Take Command of Your Software” (JavaWorld, juni 2002) för mer information om Struts och Struts action-servleten.

Nästan mappade jag sökvägen /flags till Struts-åtgärden actions.FlagAction i Struts-konfigurationsfilen, som visas i exempel H3:

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

På grund av denna mappning får URL flags.do Struts-åtgärdsservleten att åberopa actions.FlagAction.execute()-metoden; därför kommer ett klick på en flagga att åberopa actions.FlagAction.execute()-metoden. Exempel H4 listar klassen actions.FlagAction:

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

Metoden actions.FlagAction.execute() erhåller en referens till förfrågeparametern locale och skickar det värdet till JSTL Config-klassens set() metod. Den metoden lagrar strängen som representerar språket i konfigurationsinställningen FMT_LOCALE, som JSTL använder för att lokalisera text och formatera siffror, valutor, procent och datum.

När jag nu har angett en lokal för JSTL:s internationaliseringsåtgärder anger jag härnäst ett resurspaket i applikationens deploymentsdeskriptor, varav ett utdrag visas i exempel H5:

Exempel H5. WEB-INF/web.xml (Utdrag)

Resursbuntet basnamn resources är specificerat för parametern javax.servlet.jsp.jstl.fmt.localizationContext context-initialization. Det betyder att JSTL kommer att söka efter en fil som heter resources_en.properties när språket är inställt på brittisk engelska (en-GB) och resources_zh.properties när språket är inställt på kinesiska (zh-ZH).

Nu när jag har angett ett resursbunt och ett språkexempel för JSTL:s internationaliseringsåtgärder modifierar jag JSP-filerna så att de kan använda dessa åtgärder, vilket framgår av exemplen H6-H9:

Exempel H6. Sidebar-links.jsp

Exempel H7. header.jsp

Exempel H8. content.jsp

Exempel H9. footer.jsp

Exemplen H6-H9:s JSP:er använder JSTL <fmt:message>-åtgärden för att extrahera Unicode-strängar från den resursbunt som anges i distributionsbeskrivningen. Exemplen H10 och H11 listar resurspaketet för engelska respektive kinesiska:

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

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

Email

I ett mejl till mig skrev Joseph Friedman:

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

”Det omslutande objektet – känt som en dekorator – följer gränssnittet för det objekt som det omsluter, vilket gör att dekoratorn kan användas som om den vore en instans av det objekt som den omsluter.”

I kodexemplet dekorerar dock FileReader med en LineNumberReader och anropar readLine(), som inte ingår i FileReaders gränssnitt; därmed använder du inte LineNumberReader på ett transparent sätt.”

Både FileReader och LineNumberReader överensstämmer med det gränssnitt som definieras av Reader-klassen, som de båda förlänger. Tanken är att du kan överlämna dekoratorn till alla metoder som förväntar sig en referens till en Reader. Dessa metoder förblir lyckligt okunniga om dekoratorns speciella möjligheter – i det här fallet förmågan att läsa filer och producera linjenummer. Men om du känner till dessa specialfunktioner kan du dra nytta av dem.

Det faktum att dekoratorn har (en eller flera) metoder som det dekorerade objektet saknar bryter inte på något sätt mot Decorator-mönstrets avsikt; det är faktiskt den centrala funktionen i det mönstret: att lägga till funktionalitet vid körning till ett objekt genom att dekorera objekt rekursivt.

Om du tittar på Design Patterns sida 173 ser du detta:

Decorator-underklasser är fria att lägga till operationer för specifik funktionalitet. Till exempel låter ScrollDecorators ScrollTo-operation andra objekt bläddra i gränssnittet *om* de vet att det råkar finnas ett ScrollDecorator-objekt i gränssnittet.

DavidGeary är författare till Core JSTL Mastering the JSP Standard TagLibrary, som kommer att publiceras i höst av Prentice-Hall och Sun Microsystems Press, Advanced JavaServer Pages (PrenticeHall, 2001, ISBN: 013030307041) och Graphic Java-serien (Sun MicrosystemsPress). David har utvecklat objektorienterad programvara med många objektorienterade språk i 18 år. Sedan boken GOFDesign Patterns publicerades 1994 har David varit en aktiv förespråkare av designmönster och har använt och genomfört designmönster i Smalltalk, C++ och Java. År 1997 började David arbeta på heltid som författare och ibland som talare och konsult. David är medlem i de expertgrupper som definierar JSP-standardbiblioteket för anpassade taggar och JavaServer Faces, och han bidrar till Apache Struts JSP-ramverk.

Lär dig mer om det här ämnet

Lämna ett svar

Din e-postadress kommer inte publiceras.