A minap a National Public Radio Car Talk című műsorát hallgattam, egy népszerű heti adást, amelyben a telefonálók a járműveikkel kapcsolatos kérdéseket tesznek fel. Minden műsorszünet előtt a műsorvezetők arra kérik a hívókat, hogy tárcsázzák az 1-800-CAR-TALK-ot, ami az 1-800-227-8255-nek felel meg. Természetesen az előbbit sokkal könnyebb megjegyezni, mint az utóbbit, részben azért, mert a “CAR TALK” egy összetett szó: két szó, amely hét számjegyet jelent. Az emberek általában könnyebben kezelik az összetett szavakat, mint azok egyes összetevőit. Hasonlóképpen, amikor objektumorientált szoftvert fejlesztünk, gyakran kényelmes úgy manipulálni a kompozitokat, mint az egyes komponenseket. Ez az előfeltevés jelenti a kompozit tervezési minta alapelvét, amely ennek a Java Design Patterns részletnek a témája.

A kompozit minta

Mielőtt belemerülnénk a kompozit mintába, először meg kell határoznom az összetett objektumokat: olyan objektumok, amelyek más objektumokat tartalmaznak; egy rajz például állhat grafikus primitívekből, például vonalakból, körökből, téglalapokból, szövegből és így tovább.

A Java fejlesztőknek azért van szükségük a Composite mintára, mert az összetett objektumokat gyakran pontosan ugyanúgy kell manipulálnunk, mint a primitív objektumokat. Például az olyan grafikus primitíveket, mint a vonalak vagy a szöveg, rajzolni, mozgatni és átméretezni kell. De ugyanezt a műveletet el akarjuk végezni olyan kompozitokon is, például rajzokon, amelyek ezekből a primitívekből állnak. Ideális esetben mind a primitív objektumokon, mind az összetett objektumokon pontosan ugyanúgy szeretnénk műveleteket végezni, a kettő közötti különbségtétel nélkül. Ha különbséget kell tennünk primitív objektumok és kompozitok között ahhoz, hogy ugyanazt a műveletet végezzük el e két objektumtípuson, a kódunk bonyolultabbá válna, és nehezebb lenne implementálni, karbantartani és bővíteni.

A Design Patterns-ben a szerzők így írják le a kompozit mintát:

Az objektumokat fa struktúrákba komponáljuk, hogy rész-egész hierarchiákat ábrázoljunk. A Composite lehetővé teszi, hogy az ügyfelek egységesen kezeljék az egyes objektumokat és az objektumok összetételeit.

A Composite minta megvalósítása egyszerű. Az összetett osztályok kiterjesztik a primitív objektumokat reprezentáló alaposztályt. Az 1. ábra egy osztálydiagramot mutat, amely a Composite minta felépítését szemlélteti.

1. ábra. A Composite minta osztálydiagramja

Az 1. ábra osztálydiagramján a Design Pattern Composite minta tárgyalása során használtam az osztályneveket: A Component a primitív objektumok alaposztályát (vagy esetleg interfészét), a Composite pedig egy összetett osztályt képvisel. Például a Component osztály a grafikus primitívek alaposztályát, míg a Composite osztály egy Drawing osztályt képviselhet. Az 1. ábra Leaf osztálya egy konkrét primitív objektumot képvisel; például egy Line osztályt vagy egy Text osztályt. A Operation1() és Operation2() metódusok mind a Component, mind a Composite osztályok által megvalósított tartományspecifikus metódusokat képviselik.

A Composite osztály egy komponensgyűjteményt tart fenn. Általában a Composite metódusokat úgy valósítjuk meg, hogy iteráljuk ezt a gyűjteményt, és a gyűjteményben lévő minden egyes Component metódushoz meghívjuk a megfelelő metódust. Például egy Drawing osztály a draw() metódusát a következőképpen implementálhatja:

A Component osztályban implementált minden egyes metódushoz a Composite osztály ugyanolyan aláírású metódust implementál, amely az összetett komponensek felett iterál, ahogyan azt a fenti draw() metódus mutatja.

A Composite osztály kiterjeszti a Component osztályt, így átadhatunk egy kompozitot egy olyan metódusnak, amely egy komponenst vár; tekintsük például a következő metódust:

// 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();}

Az előző metódusnak átadunk egy komponenst – akár egy egyszerű komponenst, akár egy kompozitot -, majd meghívja a komponens draw() metódusát. Mivel a Composite osztály kiterjeszti a Component osztályt, a repaint() metódusnak nem kell különbséget tennie komponensek és kompozitok között – egyszerűen meghívja a komponens (vagy kompozit) draw() metódusát.

Az 1. ábra kompozit minta osztálydiagramja szemlélteti a minta egy problémáját: meg kell különböztetni a komponenseket és kompozitokat, amikor egy Component-re hivatkozunk, és meg kell hívnunk egy kompozit-specifikus metódust, például a addComponent() metódust. Ezt a követelményt általában úgy teljesítjük, hogy a Component osztályhoz hozzáadunk egy metódust, például a isComposite() metódust. Ez a metódus a komponensek esetében false értéket ad vissza, és a Composite osztályban felülírásra kerül, hogy true értéket adjon vissza. Ezenkívül a Component hivatkozást egy Composite példányra is át kell öntenünk, például így:

Megjegyezzük, hogy a addComponent() metódusnak egy Component hivatkozást adunk át, amely lehet primitív komponens vagy összetett. Mivel ez a komponens lehet kompozit, a komponenseket egy fa struktúrába állíthatjuk össze, ahogy azt a fent említett idézet is jelzi a Design Patternsből.

A 2. ábra egy alternatív Composite minta megvalósítását mutatja.

2. ábra. Egy alternatív Composite minta osztálydiagram

Ha a 2. ábrán látható Composite mintát valósítjuk meg, akkor soha nem kell különbséget tennünk komponensek és kompozitok között, és nem kell egy Component hivatkozást egy Composite példányhoz castolni. Így a fenti kódrészlet egyetlen sorra redukálódik:

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

De ha az előző kódrészletben szereplő Component hivatkozás nem egy Composite-re hivatkozik, akkor mit csináljon a addComponent()? Ez az egyik fő vitapont a 2. ábra összetett minta megvalósításával kapcsolatban. Mivel a primitív komponensek nem tartalmaznak más komponenseket, egy komponens hozzáadásának egy másik komponenshez nincs értelme, ezért a Component.addComponent() metódus vagy némán hibázik, vagy kivételt dob. Általában egy komponens hozzáadása egy másik primitív komponenshez hibának minősül, így a kivétel dobása talán a legjobb megoldás.

Melyik Composite minta megvalósítása működik tehát jobban – az 1. ábrán vagy a 2. ábrán látható? Ez mindig nagy vita tárgya a Composite minta megvalósítói között; a Design Patterns a 2. ábrán látható megvalósítást részesíti előnyben, mert itt soha nem kell különbséget tenni komponensek és konténerek között, és soha nem kell castot végrehajtani. Én személy szerint az 1. ábra implementációját részesítem előnyben, mert erősen idegenkedem attól, hogy olyan metódusokat implementáljak egy osztályban, amelyeknek nincs értelme az adott objektumtípushoz.

Most, hogy megértettük a Composite mintát és azt, hogyan lehet implementálni, vizsgáljunk meg egy Composite minta példát az Apache Struts JavaServer Pages (JSP) keretrendszerrel.

A Composite minta és a Struts Tiles

Az Apache Struts keretrendszer tartalmaz egy JSP-tagkönyvtárat, az úgynevezett Tiles-t, amelynek segítségével több JSP-ből állíthatunk össze egy weboldalt. A Tiles tulajdonképpen a J2EE (Java 2 Platform, Enterprise Edition) CompositeView minta implementációja, amely maga is a Design Patterns Composite mintán alapul. Mielőtt megvitatnánk a Composite minta relevanciáját a Tiles címke könyvtárral kapcsolatban, először tekintsük át a Tiles létjogosultságát, és azt, hogyan használjuk. Ha már ismeri a Struts Tiles-t, átfuthatja a következő részeket, és az olvasást a “Use the Composite Pattern with Struts Tiles.”

Megjegyzés: A J2EE CompositeView mintáról bővebben a “Web Application Components Made Easy with Composite View” (JavaWorld, 2001. december) című cikkemben olvashat.

A tervezők gyakran különálló régiókból álló weboldalakat építenek fel; például a 3. ábra weboldala oldalsávból, fejlécből, tartalmi régióból és láblécből áll.

3. ábra. Az összetett minta és a Struts csempék. Kattintson a miniatűrre a teljes méretű kép megtekintéséhez.

A weboldalak gyakran több azonos elrendezésű weboldalt tartalmaznak, mint például a 3. ábra oldalsáv/fejléc/tartalom/talp elrendezése. A Struts Tiles lehetővé teszi mind a tartalom, mind az elrendezés újrafelhasználását több Weboldal között. Mielőtt ezt az újrafelhasználást tárgyalnánk, nézzük meg, hogy a 3. ábra elrendezését hagyományosan csak HTML segítségével valósítjuk meg.

A komplex elrendezések kézzel történő megvalósítása

Az 1. példa azt mutatja, hogyan valósíthatjuk meg a 3. ábra weboldalát HTML segítségével:

1. példa. Kézzel megvalósított összetett elrendezés

Az előző JSP-nek két nagy hátránya van: Először is, az oldal tartalma be van ágyazva a JSP-be, így nem lehet újrafelhasználni belőle semmit, még akkor sem, ha az oldalsáv, a fejléc és a lábléc valószínűleg sok weboldalon ugyanaz lesz. Másodszor, az oldal elrendezése szintén a JSP-be van beágyazva, így azt sem tudja újrafelhasználni, még akkor sem, ha ugyanazon a webhelyen számos más weboldal ugyanazt az elrendezést használja. Az első hátrányt a <jsp:include> művelettel orvosolhatjuk, amint azt a következőkben tárgyalom.

Komplex elrendezések megvalósítása JSP-bevonásokkal

A 2. példa a 3. ábra weblapjának egy olyan megvalósítását mutatja, amely a <jsp:include>-t használja:

2. példa. A JSP-vel megvalósított összetett elrendezés tartalmazza

Az előző JSP más JSP-k tartalmát tartalmazza <jsp:include>. Mivel ezt a tartalmat külön JSP-kbe foglaltam, így más weboldalakhoz újra felhasználható:

Példa 3. sidebar.jsp

A teljesség kedvéért alább felsorolom az előző JSP által bevont JSP-ket:

Példa 4. Példa. header.jsp

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

Példa 5. content.jsp

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

Példa 6. footer.jsp

<hr>Thanks for stopping by!

Még ha a 2. példa JSP-je <jsp:include>-t használ is a tartalom újrafelhasználására, az oldal elrendezését nem tudja újrafelhasználni, mert az abban a JSP-ben keményen kódolva van. A Struts Tiles lehetővé teszi mind a tartalom, mind az elrendezés újrafelhasználását, amint azt a következő szakasz szemlélteti.

Komplex elrendezések megvalósítása a Struts Tiles segítségével

A 7. példa a 3. ábrán látható, Struts Tiles segítségével megvalósított weboldalt mutatja:

7. példa. Struts Tiles használata az elrendezés kapszulázására

Az előző JSP a <tiles:insert> taget használja a 3. ábra JSP-jének létrehozásához. Ezt a JSP-t egy sidebar-header-footer-definition nevű csempe definíció határozza meg. Ez a definíció a Tiles konfigurációs fájlban található, ami ebben az esetben a WEB-INF/tiles-defs.xml, amely a 8. példában szerepel:

8. példa. WEB-INF/tiles-defs.xml

Az előző Tiles definíció meghatározza az oldal elrendezését, amelyet a header-footer-sidebar-layout.jsp kódol, és az oldal tartalmát, amelyet a sidebar.jsp, header.jsp, content.jsp és footer.jsp kódol, a 3-6. példában felsoroltak szerint. A 9. példa felsorolja az elrendezést definiáló JSP-t-header-footer-sidebar-layout.jsp:

Példa 9. header-footer-sidebar-layout.jsp

Az előző JSP az elrendezést kapszulázza és a tartalmat az oldalsáv, szerkesztő, tartalom és lábléc régiók számára a Tiles definíciós fájlban megadott értékeknek megfelelően illeszti be, megkönnyítve ezzel mind a tartalom, mind az elrendezés újrafelhasználását. Például definiálhatunk egy másik Tiles definíciót ugyanolyan elrendezéssel, ugyanolyan oldalsávval, szerkesztővel és lábléccel, de eltérő tartalommal:

A a-different-sidebar-header-footer-definition Tiles definíció által meghatározott JSP létrehozásához a <tiles:insert> taget használjuk, így:

A Struts Tilesnek köszönhetően mind a tartalom, mind az elrendezés újrafelhasználható, ami felbecsülhetetlen értékűnek bizonyul a sok JSP-t tartalmazó, elrendezést és bizonyos tartalmakat megosztó webhelyek esetében. Ha azonban alaposan megnézzük a 7-9. példák kódját, észrevehetjük, hogy az oldalsáv régió elrendezése keményen kódolva van a sidebar.jsp-ben, amely a 3. példában szerepel. Ez azt jelenti, hogy ezt az elrendezést nem lehet újrafelhasználni. Szerencsére a Tiles tag könyvtár megvalósítja a Composite mintát, amely lehetővé teszi, hogy egy régióhoz – JSP helyett – egy csempe definíciót adjunk meg. A következő részben elmagyarázom, hogyan használhatjuk ezt a Composite mintaimplementációt.

A Composite minta használata a Struts Tiles segítségével

A Struts Tiles implementálja a Composite mintát, ahol a Component osztályt JSP-k, a Composite osztályt pedig egy Tiles definíció képviseli. Ez az implementáció lehetővé teszi, hogy vagy egy JSP-t (egy komponens), vagy egy Tiles definíciót (egy kompozit) adjunk meg egy JSP régió tartalmaként. A 10. példa szemlélteti ezt a funkciót:

10. példa. WEB-INF/tiles-defs.xml: Az összetett minta használata

Az előző Tiles konfigurációs fájl két Tiles definíciót definiál: sidebar-definition és sidebar-header-footer-definition. A sidebar-definition az sidebar-header-footer-definition-ben az oldalsáv régió értékeként van megadva. Azért adhatjuk meg így, mert a Tiles úgy valósítja meg a Composite mintát, hogy a Tiles egy definíciót (egy Composite, amely egy JSP-gyűjtemény) ad meg ott, ahol normál esetben egyetlen JSP-t (amely egy Component) adnánk meg.

Az oldalsáv elrendezését a 11. példa sidebar-layout.jsp:

A 11. példa sidebar-layout.jsp-ben foglaltuk össze. sidebar-layout.jsp

A 12. példa az oldalsáv top régiójának tartalmaként megadott flags.jsp, a 13. példa pedig az oldalsáv bottom régiójaként megadott sidebar-links.jsp:

12. példa. flags.jsp

13. példa: sidebar-links.jsp

Most a sidebar-definition meghatározhat más régiókat is egy felső és egy alsó komponenssel, bár valószínűleg át kellene nevezni ezt a definíciót valami általánosabbra, például top-bottom-definition-re.

Ezekben a napokban minden kompozit

A kompozit minta népszerű a prezentációs keretrendszerek, például a Swing és a Struts körében, mert lehetővé teszi a konténerek egymásba fészkelését azáltal, hogy a komponenseket és konténereiket pontosan ugyanúgy kezeli. A Struts Tiles a Composite mintát használja egy egyszerű JSP vagy egy Tiles definíció – amely JSP-k gyűjteménye – mint egy lapka tartalmának megadására. Ez egy hatékony képesség, amely megkönnyíti a különböző elrendezésű, nagyméretű webhelyek kezelését.

Az alábbi “Házi feladat a múltkorról” című rész kibővíti a cikk tárgyalását azzal, hogy az előző alkalmazást egy Struts-akcióval és a JSP Standard Tag Library (JSTL) segítségével nemzetközivé teszi.

Házi feladat

Tárgyaljuk meg, hogyan valósítja meg a Swing a kompozit mintát a Component és Container osztályokkal.

Hazafeladat a múltkorról

A legutóbbi feladatban azt kértük, hogy töltse le a Struts-ot a http://jakarta.apache.org/struts/index.html-ból, és implementálja saját Struts action osztályát.

Ezzel a feladattal úgy döntöttem, hogy a JSP Standard Tag Library-vel (JSTL) együtt implementálok egy Struts action-t a Példa 1 webalkalmazás nemzetközivé tételéhez. Bár a Struts biztosítja a webalkalmazások nemzetközivé tételéhez szükséges infrastruktúrát, ehhez a feladathoz a JSTL-t kell használni, mivel a JSTL egy szabvány. Valamikor a Struts nemzetköziesítési képességeit valószínűleg elavulttá teszik vagy integrálják a JSTL-be.

Azt követően, hogy az 1. példa webalkalmazását egy Struts-akcióval és a JSTL-lel nemzetköziesítettem, az alkalmazást kínai nyelvre lokalizáltam. Az eredményt a H1. ábra szemlélteti.

Megjegyzem: Egy szót sem tudok kínaiul, nemhogy azt, hogyan kell leírni a nyelvet, ezért a H1. ábra kínaija tetszőleges Unicode karakterláncokból készült. De ettől függetlenül jól néz ki.

H1. ábra. Nemzetköziesítés egy Struts akcióval. Kattintson a miniatűrre a teljes méretű kép megtekintéséhez.

Figyeljük meg, hogy a 12. példa flags.jsp href attribútumait üres karakterláncokként adtam meg, így a zászlókra kattintva a szervletkonténer újratölti az aktuális JSP-t. Ha a zászlókra kattintunk, a szervletkonténer újratölti az aktuális JSP-t. Ezért a webalkalmazás nemzetközivé tételének első lépése az, hogy megadjuk az attribútumok URL-címét a H1. példában felsoroltak szerint:

Példa H1. flags.jsp

Az egyes href attribútumok URL-címe ugyanaz: flags.do. Két kérési paramétert csatolunk ehhez az URL-hez: az egyik a flagnek megfelelő locale-t, a másik pedig az aktuális JSP elérési útvonalát jelöli. Ez utóbbi kérési paramétert a Http.ServletRequest.getServletPath() metódus meghívásával kapjuk meg.

Az alkalmazás telepítési leírójában minden .do végződésű URL-t a Struts action szervlethez képeztem le, így:

Példa H2. WEB-INF/web.xml (Kivonat)

Megjegyzés: A Strutsról és a Struts action szervletről bővebben a “Take Command of Your Software” (JavaWorld, 2002. június) című cikkemben olvashat.

Ezután a Struts konfigurációs fájlban a /flags elérési utat a actions.FlagAction Struts actionre képeztem le, amely a H3 példában szerepel:

Példa H3. WEB-INF/struts-config.xml

Ez a hozzárendelés miatt az URL flags.do a Struts action szervletet a actions.FlagAction.execute() metódus meghívására készteti; ezért egy zászlóra kattintva a actions.FlagAction.execute() metódust fogja meghívni. A H4 példa a actions.FlagAction osztályt sorolja fel:

A H4 példa. WEB-INF/classes/actions/FlagAction.java

A actions.FlagAction.execute() metódus megkapja a locale kérési paraméterre való hivatkozást, és ezt az értéket átadja a JSTL Config osztály set() metódusának. Ez a metódus a FMT_LOCALE konfigurációs beállításban tárolja a nyelvterületet reprezentáló karakterláncot, amelyet a JSTL a szöveg lokalizálására és a számok, pénznemek, százalékok és dátumok formázására használ.

Most, miután megadtam egy nyelvterületet a JSTL nemzetköziesítési műveleteihez, a következőkben megadom az alkalmazás telepítési leírójában az erőforrásköteget, amelynek egy kivonata a H5. példában található:

H5. példa. WEB-INF/web.xml (részlet)

Az erőforrásköteg alapneve resources van megadva a javax.servlet.jsp.jstl.fmt.localizationContext kontextus-inicializációs paraméterhez. Ez azt jelenti, hogy a JSTL egy resources_en.properties nevű fájlt fog keresni, ha a nyelvjárás brit angolra (en-GB), és resources_zh.properties, ha a nyelvjárás kínaira (zh-ZH) van beállítva.

Most, hogy megadtam egy erőforrásköteget és egy nyelvjárást a JSTL nemzetköziesítési műveleteihez, a következőkben módosítom a JSP fájlokat, hogy használják ezeket a műveleteket, ahogy a H6-H9 példák mutatják:

Példa H6. sidebar-links.jsp

Példa H7. header.jsp

Példa H8. content.jsp

Példa H9. footer.jsp

A H6-H9 példák JSP-i a JSTL <fmt:message> műveletet használják a telepítési leíróban megadott forráskötegből származó Unicode karakterláncok kinyerésére. A H10-es és a H11-es példa az angol és a kínai nyelvű erőforráskötegeket sorolja fel:

H10-es példa. WEB-INF/classes/resources_en.properties

Példa H11. WEB-INF/classes/resources_zh.properties

Email

Joseph Friedman egy nekem küldött e-mailben azt írta:

A “Decorate Your Java Code” (JavaWorld, 2001. december) című cikkében azt írja:

“A burkoló objektum – amelyet dekorátornak neveznek – megfelel az általa körülzárt objektum interfészének, így a dekorátor úgy használható, mintha az általa körülzárt objektum példánya lenne.”

A kódpélda azonban a FileReader-t egy LineNumberReader-val díszíti, és meghívja a readLine()-t, amely nem szerepel a FileReader interfészében; így nem átláthatóan használja a LineNumberReader-t.

Mind a FileReader, mind a LineNumberReader megfelel a Reader osztály által meghatározott interfésznek, amelyet mindkettő kiterjeszt. Az ötlet az, hogy a dekorátort átadhatjuk bármelyik metódusnak, amely egy Reader-ra való hivatkozást vár. Ezek a metódusok boldog tudatlanságban maradnak a dekorátor különleges képességeiről – ebben az esetben a fájlok olvasásának és a sorszámok előállításának képességéről. Ha azonban tudunk ezekről a speciális képességekről, akkor kihasználhatjuk azokat.

A tény, hogy a dekorátor rendelkezik (egy vagy több) olyan metódussal, amelyek a dekorált objektumnak nincsenek, semmilyen módon nem sérti a Decorator minta szándékát; valójában ez a minta központi jellemzője: futásidőben hozzáadni egy objektumhoz funkcionalitást az objektumok rekurzív dekorálásával.

Ha megnézzük a Design Patterns 173. oldalát, akkor ezt láthatjuk:

A Decorator alosztályok szabadon hozzáadhatnak műveleteket meghatározott funkciókhoz. Például a ScrollDecorator ScrollTo műveletével más objektumok görgethetik a felületet, *ha* tudják, hogy történetesen van egy ScrollDecorator objektum a felületen.

DavidGeary a szerzője a Core JSTL Mastering the JSP Standard TagLibrary című könyvnek, amely idén ősszel jelenik meg aPrentice-Hall és a Sun Microsystems Press kiadásában; Advanced JavaServer Pages (PrenticeHall, 2001; ISBN: 0130307041); és a Graphic Java sorozatnak (Sun MicrosystemsPress). David 18 éve fejleszt objektumorientált szoftvereket számos objektumorientált nyelvvel. A GOFDesign Patterns könyv 1994-es megjelenése óta David a tervezési minták aktív támogatója, és a Smalltalk, a C++ és a Java nyelveken használta és valósította meg a tervezési mintákat. 1997-ben David főállású szerzőként és alkalmi előadóként és tanácsadóként kezdett dolgozni. David tagja a JSP szabványos egyéni tagkönyvtárat és a JavaServer Faces-t meghatározó szakértői csoportoknak, valamint az Apache Struts JSP keretrendszerhez is hozzájárult.

Tudjon meg többet erről a témáról

Vélemény, hozzászólás?

Az e-mail-címet nem tesszük közzé.