Jednou jsem poslouchal populární týdenní vysílání National Public Radio Car Talk, během kterého se volající ptají na svá vozidla. Před každou přestávkou pořadu žádají moderátoři volající, aby vytočili číslo 1-800-CAR-TALK, což odpovídá číslu 1-800-227-8255. Samozřejmě se ukazuje, že první z těchto čísel se pamatuje mnohem lépe než druhé, částečně proto, že slova „CAR TALK“ jsou složená: dvě slova, která představují sedm číslic. Lidé se obecně snáze vyrovnávají se složenými slovy než s jejich jednotlivými složkami. Stejně tak při vývoji objektově orientovaného softwaru je často výhodné manipulovat s kompozity stejně jako s jednotlivými komponentami. Tento předpoklad představuje základní princip návrhového vzoru Composite, který je tématem tohoto dílu seriálu Návrhové vzory Java.

Vzor Composite

Než se ponoříme do vzoru Composite, musím nejprve definovat složené objekty: objekty, které obsahují jiné objekty; například výkres se může skládat z grafických primitiv, jako jsou čáry, kružnice, obdélníky, text atd.

Vývojáři jazyka Java potřebují vzor Composite, protože s kompozitními objekty musíme často manipulovat úplně stejně jako s primitivními objekty. Například grafické primitivy, jako jsou čáry nebo text, je třeba kreslit, přesouvat a měnit jejich velikost. Stejné operace však chceme provádět i s kompozity, jako jsou výkresy, které se z těchto primitiv skládají. V ideálním případě bychom chtěli provádět operace s primitivními objekty i kompozity naprosto stejným způsobem, aniž bychom mezi nimi rozlišovali. Pokud bychom museli rozlišovat mezi primitivními objekty a kompozity, abychom mohli provádět stejné operace na těchto dvou typech objektů, náš kód by se stal složitějším a obtížněji implementovatelným, udržovatelným a rozšiřitelným.

V knize Design Patterns autoři popisují vzor Composite takto:

Skládání objektů do stromových struktur, které představují hierarchie částí a celků. Vzor Composite umožňuje klientům zacházet s jednotlivými objekty a kompozicemi objektů jednotně.

Implementace vzoru Composite je snadná. Kompozitní třídy rozšiřují základní třídu, která reprezentuje primitivní objekty. Na obrázku 1 je znázorněn diagram tříd, který ilustruje strukturu vzoru Composite.

Obrázek 1. Diagram tříd vzoru Composite

V diagramu tříd na obrázku 1 jsem použil názvy tříd z diskuse o vzoru Design Pattern Composite: Component představuje základní třídu (případně rozhraní) pro primitivní objekty a Composite představuje kompozitní třídu. Například třída Component může představovat základní třídu pro grafické primitivy, zatímco třída Composite může představovat třídu Drawing. Třída Leaf na obrázku 1 představuje konkrétní primitivní objekt; například třídu Line nebo třídu Text. Metody Operation1() a Operation2() představují doménově specifické metody implementované třídami Component i Composite.

Třída Composite udržuje kolekci komponent. Typicky jsou metody Composite implementovány iterací nad touto kolekcí a voláním příslušné metody pro každou Component v kolekci. Například třída Drawing může implementovat svou metodu draw() takto:

Pro každou metodu implementovanou ve třídě Component implementuje třída Composite metodu se stejnou signaturou, která iteruje přes komponenty kompozice, jak ilustruje výše uvedená metoda draw().

Třída Composite rozšiřuje třídu Component, takže metodě, která očekává komponentu, můžete předat kompozit; uvažujme například následující metodu:

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

Předchozí metodě je předána komponenta – buď jednoduchá komponenta, nebo kompozit – a ta pak vyvolá metodu draw() této komponenty. Protože třída Composite rozšiřuje třídu Component, metoda repaint() nemusí rozlišovat mezi komponentami a kompozity – jednoduše volá metodu draw() pro danou komponentu (nebo kompozit).

Diagram třídy vzoru Composite na obrázku 1 ilustruje jeden problém tohoto vzoru: při odkazování na třídu Component musíte rozlišovat mezi komponentami a kompozity a musíte volat metodu specifickou pro kompozit, například addComponent(). Tento požadavek obvykle splníte přidáním metody, například isComposite(), do třídy Component. Tato metoda vrací false pro komponenty a je přepsána ve třídě Composite tak, aby vracela true. Kromě toho musíte také provést cast reference Component na instanci třídy Composite, například takto:

Všimněte si, že metodě addComponent() je předána reference Component, což může být buď primitivní komponenta, nebo kompozit. Protože tato komponenta může být kompozitní, můžete komponenty skládat do stromové struktury, jak naznačuje výše uvedená citace z Design Patterns.

Obrázek 2 ukazuje alternativní implementaci vzoru Composite.

Obr. 2. Alternativní diagram tříd vzoru Composite

Pokud implementujete vzor Composite na obrázku 2, nemusíte nikdy rozlišovat mezi komponentami a kompozity a nemusíte obsazovat odkaz Component na instanci Composite. Výše uvedený fragment kódu se tedy redukuje na jediný řádek:

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

Ale pokud odkaz Component v předchozím fragmentu kódu neodkazuje na Composite, co má dělat addComponent()? To je hlavní sporný bod implementace kompozitního vzoru na obrázku 2. Protože primitivní komponenty neobsahují jiné komponenty, nemá přidání komponenty k jiné komponentě smysl, takže metoda Component.addComponent() může buď tiše selhat, nebo vyhodit výjimku. Obvykle je přidání komponenty k jiné primitivní komponentě považováno za chybu, takže vyhození výjimky je asi nejlepší postup.

Takže která implementace vzoru Composite – ta na obrázku 1 nebo ta na obrázku 2 – funguje nejlépe? To je vždy předmětem velkých debat mezi implementátory vzoru Composite; Design Patterns dává přednost implementaci na obrázku 2, protože nikdy není třeba rozlišovat mezi komponentami a kontejnery a nikdy není třeba provádět obsazení. Osobně dávám přednost implementaci podle obrázku 1, protože mám silnou averzi vůči implementaci metod ve třídě, které pro daný typ objektu nedávají smysl.

Teď, když jste pochopili vzor Composite a způsob jeho implementace, prozkoumejme příklad vzoru Composite s frameworkem Apache Struts JavaServer Pages (JSP).

Vzor Composite a dlaždice Struts

Rámec Apache Struts obsahuje knihovnu značek JSP, známou jako dlaždice, která umožňuje sestavit webovou stránku z více JSP. Dlaždice jsou vlastně implementací vzoru J2EE (Java 2 Platform, Enterprise Edition) CompositeView, který sám vychází ze vzoru Design Patterns Composite. Než se budeme zabývat významem vzoru Composite pro knihovnu značek Tiles, zopakujme si nejprve důvody vzniku vzoru Tiles a způsob jeho použití. Pokud jste již se Struts Tiles obeznámeni, můžete následující části přeskočit a začít číst u „Use the Composite Pattern with Struts Tiles.“

Poznámka: Více o vzoru J2EE CompositeView si můžete přečíst v mém článku „Web Application Components Made Easy with Composite View“ (JavaWorld, prosinec 2001).

Designéři často konstruují webové stránky se sadou oddělených oblastí; například webová stránka na obrázku 3 se skládá z postranního panelu, záhlaví, oblasti obsahu a zápatí.

Obrázek 3. Vzor Composite a dlaždice Struts. Kliknutím na miniaturu zobrazíte obrázek v plné velikosti.

Webové stránky často obsahují více webových stránek s identickým rozvržením, jako je například rozvržení bočního panelu/záhlaví/obsahu/zápatí na obrázku 3. Dlaždice Struts umožňují opakované použití obsahu i rozvržení mezi více webovými stránkami. Než toto opakované použití probereme, podívejme se, jak se rozvržení obrázku 3 tradičně implementuje pouze pomocí jazyka HTML.

Ruční implementace složitých rozvržení

Příklad 1 ukazuje, jak lze webovou stránku obrázku 3 implementovat pomocí jazyka HTML:

Příklad 1. Složité rozvržení implementované ručně

Předchozí JSP má dvě hlavní nevýhody: Za prvé, obsah stránky je vložen do JSP, takže nic z něj nemůžete znovu použít, přestože postranní panel, záhlaví a zápatí budou pravděpodobně stejné na mnoha webových stránkách. Za druhé, rozvržení stránky je také vloženo do tohoto JSP, takže jej rovněž nemůžete znovu použít, i když mnoho dalších webových stránek na stejné webové stránce používá stejné rozvržení. První nevýhodu můžeme odstranit pomocí akce <jsp:include>, o čemž pojednávám dále.

Implementace složitých rozvržení pomocí JSP includes

Příklad 2 ukazuje implementaci Webové stránky na obrázku 3, která používá akci <jsp:include>:

Příklad 2. Na obrázku 3 je zobrazena implementace Webové stránky, která používá akci <jsp:include>. Komplexní rozvržení implementované pomocí JSP obsahuje

Předchozí JSP obsahuje obsah jiných JSP s <jsp:include>. Protože jsem tento obsah zapouzdřil do samostatných JSP, můžete jej znovu použít pro další webové stránky:

Příklad 3. Sidebar.jsp

Pro úplnost uvádím níže seznam JSP, které předchozí JSP obsahuje:

Příklad 4. JSP s obsahem, který je součástí předchozího JSP. header.jsp

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

Příklad 5. content.jsp

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

Příklad 6. footer.jsp

<hr>Thanks for stopping by!

Přestože JSP příkladu 2 používá <jsp:include> pro opakované použití obsahu, nelze opakovaně použít rozvržení stránky, protože je v tomto JSP pevně zakódováno. Funkce Struts Tiles umožňuje opakovaně použít jak obsah, tak rozvržení, jak je znázorněno v následující části.

Implementace složitých rozvržení pomocí funkce Struts Tiles

Příklad 7 ukazuje webovou stránku na obrázku 3 implementovanou pomocí funkce Struts Tiles:

Příklad 7. Funkce Struts Tiles umožňuje opakovaně použít obsah i rozvržení. Použití dlaždic Struts k zapouzdření rozvržení

Předchozí JSP využívá značku <tiles:insert> k vytvoření JSP na obrázku 3. Tento JSP je definován definicí dlaždic s názvem sidebar-header-footer-definition. Tato definice se nachází v konfiguračním souboru dlaždic, který je v tomto případě WEB-INF/tiles-defs.xml, uvedeném v příkladu 8:

Příklad 8. WEB-INF/tiles-defs.xml

Předchozí definice dlaždic určuje rozložení stránky, zapouzdřené v header-footer-sidebar-layout.jsp, a obsah stránky, zapouzdřený v sidebar.jsp, header.jsp, content.jsp a footer.jsp, jak je uvedeno v příkladech 3-6. Příklad 9 uvádí JSP, který definuje rozvržení-header-footer-sidebar-layout.jsp:

Příklad 9. header-footer-sidebar-layout.jsp

Předchozí JSP zapouzdřuje rozvržení a vkládá obsah podle hodnot zadaných pro oblasti postranního panelu, editoru, obsahu a zápatí v souboru definice dlaždic, čímž usnadňuje opakované použití obsahu i rozvržení. Můžete například definovat jinou definici dlaždic se stejným rozvržením, stejným postranním panelem, editorem a zápatím, ale jiným obsahem:

Pro vytvoření JSP definovaného definicí dlaždic a-different-sidebar-header-footer-definition použijete značku <tiles:insert>, například takto:

Díky dlaždicím Struts můžete opakovaně použít obsah i rozvržení, což se ukazuje jako neocenitelné pro webové stránky s mnoha JSP, které sdílejí rozvržení a část obsahu. Pokud se však pozorně podíváte na kód z příkladů 7-9, všimnete si, že rozložení pro oblast postranního panelu je natvrdo zakódováno v sidebar.jsp, který je uveden v příkladu 3. To znamená, že toto rozvržení nelze znovu použít. Naštěstí knihovna značek dlaždic implementuje vzor Composite, který nám umožňuje zadat definici dlaždic – namísto JSP – pro region. V následující části vysvětlím, jak tuto implementaci vzoru Composite použít.

Použití vzoru Composite s knihovnou Struts Tiles

Struts Tiles implementuje vzor Composite, kde je třída Component reprezentována JSP a třída Composite je reprezentována definicí dlaždic. Tato implementace umožňuje zadat jako obsah oblasti JSP buď JSP (komponentu), nebo definici dlaždic (kompozit). Příklad 10 ilustruje tuto funkci:

Příklad 10. WEB-INF/tiles-defs.xml: Předchozí konfigurační soubor dlaždic definuje dvě definice dlaždic: sidebar-definition a sidebar-header-footer-definition. V souboru sidebar-header-footer-definition je jako hodnota pro oblast postranního panelu zadána hodnota sidebar-definition. Můžete ji takto zadat, protože Tiles implementuje vzor Composite tím, že umožňuje Tiles zadat definici (Composite, která je kolekcí JSP) tam, kde byste normálně zadali jeden JSP (což je Component).

Rozložení postranního panelu je zapouzdřeno v sidebar-layout.jsp příkladu 11:

Příklad 11. V příkladu 11 se nachází sidebar-layout.jsp. sidebar-layout.jsp

Příklad 12 uvádí flags.jsp, který je specifikován jako obsah oblasti top postranního panelu, a Příklad 13 uvádí sidebar-links.jsp, který je specifikován jako oblast bottom postranního panelu:

Příklad 12. flags.jsp

Příklad 13. sidebar-links.jsp

Nyní může sidebar-definition definovat další regiony s horní a dolní komponentou, i když byste pravděpodobně měli tuto definici přejmenovat na něco obecnějšího, například top-bottom-definition.

Dnes je to všechno kompozit

Vzor Composite je oblíbený u prezentačních frameworků, jako jsou Swing a Struts, protože umožňuje vnořit kontejnery tím, že s komponentami a jejich kontejnery zachází úplně stejně. Struts Tiles používá vzor Composite k určení jednoduchého JSP nebo definice Tiles – což je kolekce JSP – jako obsahu dlaždice. To je mocná schopnost, která usnadňuje správu rozsáhlých webových stránek s různými rozvrženími.

Následující část „Domácí úkol z minula“ rozšiřuje diskusi tohoto článku o internacionalizaci předchozí aplikace pomocí akce Struts a knihovny JSP Standard Tag Library (JSTL).

Domácí úkol

Probírejte, jak Swing implementuje kompozitní vzor pomocí tříd Component a Container.

Domácí úkol z minula

V minulém úkolu jste měli za úkol stáhnout Struts z http://jakarta.apache.org/struts/index.html a implementovat vlastní třídu akce Struts.

Pro tento úkol jsem se rozhodl implementovat akci Struts ve spojení s knihovnou JSP Standard Tag Library (JSTL), abych internacionalizoval webovou aplikaci Příklad 1.

Domácí úkol z minula. Přestože Struts poskytuje potřebnou infrastrukturu pro internacionalizaci webových aplikací, měli byste pro tento úkol použít knihovnu JSTL, protože JSTL je standard. V určitém okamžiku budou internacionalizační schopnosti nástroje Struts pravděpodobně zastaralé nebo budou integrovány do JSTL.

Po internacionalizaci webové aplikace Příkladu 1 pomocí akce Struts a JSTL jsem tuto aplikaci lokalizoval pro čínštinu. Výsledek ilustruje obrázek H1.

Poznámka: Neumím ani slovo čínsky, natož jak se tento jazyk píše, takže čínština na obrázku H1 je vytvořena z libovolných řetězců Unicode. Ale bez ohledu na to to vypadá skvěle.

Obrázek H1. Internacionalizace pomocí akce Struts. Kliknutím na miniaturu zobrazíte obrázek v plné velikosti.

Všimněte si, že jsem atributy href v příkladu 12 flags.jsp zadal jako prázdné řetězce, takže po kliknutí na příznaky kontejner servletu znovu načte aktuální JSP. Proto je prvním krokem k internacionalizaci webové aplikace zadání adresy URL pro tyto atributy, jak je uvedeno v příkladu H1:

Příklad H1. flags.jsp

Adresa URL pro každý atribut href je stejná: flags.do. K této adrese URL jsou připojeny dva parametry požadavku: jeden pro locale odpovídající příznaku a druhý, který představuje cestu k aktuálnímu JSP. Druhý parametr požadavku získáte vyvoláním metody Http.ServletRequest.getServletPath().

V popisovači nasazení aplikace jsem namapoval všechna URL končící na .do na servlet akce Struts takto:

Příklad H2. WEB-INF/web.xml (výňatek)

Poznámka: Další informace o serveru Struts a servletu akce Struts najdete v mém článku „Take Command of Your Software“ (JavaWorld, červen 2002).

Dále jsem namapoval cestu /flags na akci Struts actions.FlagAction v konfiguračním souboru Struts, uvedeném v příkladu H3:

Příklad H3. WEB-INF/struts-config.xml

Vzhledem k tomuto mapování způsobí adresa URL flags.do, že servlet akce Struts vyvolá metodu actions.FlagAction.execute(); kliknutí na příznak tedy vyvolá metodu actions.FlagAction.execute(). Příklad H4 uvádí třídu actions.FlagAction:

Příklad H4. WEB-INF/classes/actions/FlagAction.java

Metoda actions.FlagAction.execute() získá odkaz na parametr požadavku locale a předá tuto hodnotu metodě set() třídy JSTL Config. Tato metoda uloží řetězec reprezentující locale do konfiguračního nastavení FMT_LOCALE, které JSTL používá k lokalizaci textu a formátování čísel, měn, procent a dat.

Teď, když jsem určil locale pro internacionalizační akce JSTL, dále určím svazek prostředků v popisovači nasazení aplikace, jehož výňatek je uveden v příkladu H5:

Příklad H5. WEB-INF/web.xml (výňatek)

Základní název svazku prostředků resources je uveden pro parametr javax.servlet.jsp.jstl.fmt.localizationContext inicializace kontextu. To znamená, že JSTL bude hledat soubor s názvem resources_en.properties, když je locale nastaveno na britskou angličtinu (en-GB) a resources_zh.properties, když je locale nastaveno na čínštinu (zh-ZH).

Teď, když jsem určil svazek prostředků a locale pro internacionalizační akce JSTL, dále upravím soubory JSP, aby tyto akce používaly, jak ukazují příklady H6-H9:

Příklad H6. sidebar-links.jsp

Příklad H7. header.jsp

Příklad H8. content.jsp

Příklad H9. footer.jsp

Příklady H6-H9 používají akci JSTL <fmt:message> k extrakci řetězců Unicode ze svazku prostředků uvedeného v deskriptoru nasazení. Příklady H10 a H11 uvádějí svazky prostředků pro angličtinu, resp. čínštinu:

Příklad H10. WEB-INF/classes/resources_en.properties

Příklad H11. WEB-INF/třídy/zdroje_zh.vlastnosti

Email

Josef Friedman mi v e-mailu napsal:

V článku „Decorate Your Java Code“ (JavaWorld, prosinec 2001) píšete:

„Obalující objekt – známý jako dekorátor – odpovídá rozhraní objektu, který obklopuje, což umožňuje používat dekorátor, jako by byl instancí objektu, který obklopuje.“

Ukázka kódu však dekoruje FileReader pomocí LineNumberReader a volá readLine(), který není v rozhraní FileReader; nepoužíváte tedy LineNumberReader transparentně.

Oba FileReader a LineNumberReader odpovídají rozhraní definovanému třídou Reader, kterou obě rozšiřují. Jde o to, že dekorátor můžete předat libovolné metodě, která očekává odkaz na Reader. Tyto metody zůstanou v blažené nevědomosti o speciálních schopnostech tohoto dekorátoru – v tomto případě o schopnosti číst soubory a vytvářet čísla řádků. Pokud však o těchto speciálních schopnostech víte, můžete je využít.

To, že dekorátor disponuje (jednou nebo více) metodami, které dekorovaný objekt postrádá, nijak neporušuje záměr vzoru Dekorátor; ve skutečnosti je to hlavní rys tohoto vzoru: přidat objektu funkčnost za běhu rekurzivním dekorováním objektů.

Pokud se podíváte na stránku 173 Design Patterns, uvidíte toto:

Dekorátorové podtřídy mohou volně přidávat operace pro specifickou funkčnost. Například operace ScrollDecorator ScrollTo umožňuje jiným objektům procházet rozhraním, *pokud* vědí, že v rozhraní náhodou existuje objekt ScrollDecorator.

DavidGeary je autorem knih Core JSTL Mastering the JSP Standard TagLibrary, která vyjde letos na podzim v nakladatelstvíchPrentice-Hall a Sun Microsystems Press; Advanced JavaServer Pages (PrenticeHall, 2001; ISBN: 0130307041); a řady Graphic Java (Sun MicrosystemsPress). David se již 18 let zabývá vývojem objektově orientovaného softwaru v mnoha objektově orientovaných jazycích. Od vydání knihy GOFDesign Patterns v roce 1994 je David aktivním zastáncem návrhových vzorů a používá a implementuje návrhové vzory v jazycích Smalltalk, C++ a Java. V roce 1997 začal David pracovat na plný úvazek jako autor a příležitostný řečník a konzultant. David je členem expertních skupin definujících standardní knihovnu vlastních značek JSP a JavaServer Faces a přispívá do frameworku Apache Struts JSP.

Další informace o tomto tématu

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.