Innego dnia słuchałem National Public Radio’s Car Talk, popularnej cotygodniowej audycji, podczas której rozmówcy zadają pytania dotyczące ich pojazdów. Przed każdą przerwą w programie prowadzący proszą dzwoniących o wybranie numeru 1-800-CAR-TALK, który odpowiada 1-800-227-8255. Oczywiście, ten pierwszy okazuje się znacznie łatwiejszy do zapamiętania niż ten drugi, częściowo dlatego, że słowa „CAR TALK” są złożone: dwa słowa, które reprezentują siedem cyfr. Ludziom zazwyczaj łatwiej jest radzić sobie ze złożeniami, niż z ich poszczególnymi komponentami. Podobnie, gdy tworzysz oprogramowanie zorientowane obiektowo, często wygodnie jest manipulować kompozytami, tak jak manipuluje się poszczególnymi komponentami. To założenie reprezentuje podstawową zasadę wzorca projektowego Composite, który jest tematem tej części Java Design Patterns.

Wzorzec Composite

Zanim zagłębimy się we wzorzec Composite, muszę najpierw zdefiniować obiekty złożone: obiekty, które zawierają inne obiekty; na przykład rysunek może składać się z prymitywów graficznych, takich jak linie, koła, prostokąty, tekst i tak dalej.

Programiści Javy potrzebują wzorca Composite, ponieważ często musimy manipulować kompozytami dokładnie w ten sam sposób, w jaki manipulujemy obiektami prymitywnymi. Na przykład prymitywy graficzne, takie jak linie lub tekst, muszą być rysowane, przesuwane i zmieniane pod względem rozmiaru. Ale chcemy również wykonać te same operacje na kompozytach, takich jak rysunki, które składają się z tych prymitywów. Idealnie byłoby wykonywać operacje na obiektach prymitywnych i kompozytach w dokładnie ten sam sposób, bez rozróżniania między nimi. Gdybyśmy musieli rozróżniać obiekty prymitywne i kompozyty, aby wykonywać te same operacje na tych dwóch typach obiektów, nasz kod stałby się bardziej złożony i trudniejszy do wdrożenia, utrzymania i rozszerzenia.

W Design Patterns autorzy opisują wzorzec Composite w następujący sposób:

Komponuj obiekty w struktury drzewiaste, aby reprezentować hierarchie część-całość. Composite pozwala klientom traktować pojedyncze obiekty i kompozycje obiektów jednolicie.

Implementacja wzorca Composite jest łatwa. Klasy kompozytowe rozszerzają klasę bazową, która reprezentuje obiekty prymitywne. Na rysunku 1 przedstawiono diagram klas, który ilustruje strukturę wzorca Composite.

Rysunek 1. Diagram klas wzorca Composite

W diagramie klas na rysunku 1 użyłem nazw klas z omówienia wzorca Composite w Design Pattern: Component reprezentuje klasę bazową (lub ewentualnie interfejs) dla obiektów prymitywnych, a Composite reprezentuje klasę złożoną. Na przykład, klasa Component może reprezentować klasę bazową dla prymitywów graficznych, podczas gdy klasa Composite może reprezentować klasę Drawing. Klasa Leaf na rysunku 1 reprezentuje konkretny obiekt prymitywny; na przykład klasę Line lub klasę Text. Metody Operation1() i Operation2() reprezentują metody specyficzne dla domeny implementowane zarówno przez klasy Component, jak i Composite.

Klasa Composite utrzymuje kolekcję komponentów. Zazwyczaj metody Composite są implementowane przez iterację po tej kolekcji i wywołanie odpowiedniej metody dla każdego Component w kolekcji. Na przykład, klasa Drawing może zaimplementować swoją metodę draw() w następujący sposób:

Dla każdej metody zaimplementowanej w klasie Component, klasa Composite implementuje metodę o tym samym podpisie, która iteruje po składnikach kompozytu, jak zilustrowano przez metodę draw() wymienioną powyżej.

Klasa Composite rozszerza klasę Component, więc możesz przekazać kompozyt do metody, która oczekuje komponentu; na przykład, rozważ następującą 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();}

Poprzednia metoda otrzymuje komponent – albo prosty komponent, albo kompozyt – a następnie wywołuje metodę draw() tego komponentu. Ponieważ klasa Composite rozszerza Component, metoda repaint() nie musi rozróżniać między komponentami i kompozytami – po prostu wywołuje metodę draw() dla komponentu (lub kompozytu).

Schemat klas wzorca Composite na rysunku 1 ilustruje jeden problem z tym wzorcem: musisz rozróżniać między komponentami i kompozytami, gdy odwołujesz się do Component i musisz wywołać metodę specyficzną dla kompozytu, taką jak addComponent(). Zazwyczaj spełniasz to wymaganie poprzez dodanie metody, takiej jak isComposite(), do klasy Component. Ta metoda zwraca false dla komponentów i jest nadpisana w klasie Composite, aby zwracać true. Dodatkowo, musisz również rzutować referencję Component na instancję Composite, jak poniżej:

Zauważ, że metoda addComponent() przekazuje referencję Component, która może być albo prymitywnym komponentem albo kompozytem. Ponieważ ten komponent może być kompozytem, można skomponować komponenty w strukturę drzewiastą, jak wskazuje wspomniany cytat z Design Patterns.

Rysunek 2 przedstawia alternatywną implementację wzorca Composite.

Rysunek 2. Alternatywny diagram klas wzorca Composite

Jeżeli zaimplementujesz wzorzec Composite z rysunku 2, nie musisz nigdy rozróżniać komponentów i kompozytów, a także nie musisz rzutować referencji Component na instancję Composite. Tak więc powyższy fragment kodu redukuje się do jednej linii:

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

Ale, jeśli referencja Component w poprzednim fragmencie kodu nie odnosi się do instancji Composite, co powinien zrobić addComponent()? Jest to główny punkt sporny z implementacją wzorca Composite na rysunku 2. Ponieważ prymitywne komponenty nie zawierają innych komponentów, dodawanie komponentu do innego komponentu nie ma sensu, więc metoda Component.addComponent() może albo zakończyć się cichym niepowodzeniem, albo rzucić wyjątek. Zazwyczaj dodawanie komponentu do innego prymitywnego komponentu jest uważane za błąd, więc rzucenie wyjątku jest prawdopodobnie najlepszym sposobem działania.

Więc, która implementacja wzorca Composite – ta z rysunku 1 czy ta z rysunku 2 – działa najlepiej? To jest zawsze temat wielkiej debaty wśród implementatorów wzorca Composite; Design Patterns preferuje implementację z rysunku 2, ponieważ nigdy nie trzeba rozróżniać komponentów i kontenerów, i nigdy nie trzeba wykonywać rzutowania. Osobiście wolę implementację z rysunku 1, ponieważ mam silną awersję do implementowania metod w klasie, które nie mają sensu dla tego typu obiektu.

Teraz, gdy rozumiesz wzorzec Composite i jak możesz go zaimplementować, przeanalizujmy przykład wzorca Composite z frameworkiem Apache Struts JavaServer Pages (JSP).

Wzorzec Composite i Struts Tiles

Szkielet Apache Struts zawiera bibliotekę znaczników JSP, znaną jako Tiles, która pozwala skomponować stronę WWW z wielu JSP. Tiles jest właściwie implementacją wzorca J2EE (Java 2 Platform, Enterprise Edition) CompositeView, opartego na wzorcu Design Patterns Composite. Zanim omówimy znaczenie wzorca Composite dla biblioteki znaczników Tiles, przyjrzyjmy się najpierw zasadności istnienia wzorca Tiles oraz temu, jak się go używa. Jeśli jesteś już zaznajomiony ze Struts Tiles, możesz pominąć następujące sekcje i rozpocząć czytanie od „Use the Composite Pattern with Struts Tiles.”

Uwaga: Możesz przeczytać więcej o wzorcu J2EE CompositeView w moim artykule „Web Application Components Made Easy with Composite View” (JavaWorld, grudzień 2001).

Projektanci często konstruują strony WWW z zestawem dyskretnych regionów; na przykład strona WWW z rysunku 3 składa się z paska bocznego, nagłówka, regionu treści i stopki.

Rysunek 3. Wzorzec Composite i Struts Tiles. Kliknij na miniaturkę, aby zobaczyć obraz w pełnym rozmiarze.

Strony internetowe często zawierają wiele stron o identycznych układach, takich jak układ paska bocznego/nagłówka/content/stopka z rysunku 3. Struts Tiles pozwala na ponowne użycie zarówno treści jak i układu pomiędzy wieloma stronami. Zanim omówimy to ponowne użycie, zobaczmy, jak układ Rysunku 3 jest tradycyjnie implementowany za pomocą samego HTML-a.

Implementuj złożone układy ręcznie

Przykład 1 pokazuje, jak można zaimplementować stronę Rysunku 3 za pomocą HTML-a:

Przykład 1. Złożony układ zaimplementowany ręcznie

Poprzedni JSP ma dwie poważne wady: Po pierwsze, zawartość strony jest osadzona w JSP, więc nie można jej ponownie wykorzystać, nawet jeśli pasek boczny, nagłówek i stopka będą prawdopodobnie takie same na wielu stronach WWW. Po drugie, układ strony jest również osadzony w JSP, więc również nie można go ponownie użyć, nawet jeśli wiele innych stron w tej samej witrynie używa tego samego układu. Możemy użyć akcji <jsp:include>, aby zaradzić pierwszej wadzie, co omówię dalej.

Implementacja złożonych układów za pomocą JSP includes

Przykład 2 pokazuje implementację strony WWW z rysunku 3, która używa akcji <jsp:include>:

Przykład 2. Złożony układ zaimplementowany za pomocą JSP zawiera

Poprzedni JSP zawiera zawartość innych JSP z <jsp:include>. Ponieważ zamknąłem tę zawartość w oddzielnych JSP, możesz ją ponownie wykorzystać na innych stronach WWW:

Przykład 3. sidebar.jsp

Dla kompletności, poniżej wymieniłem JSP dołączone przez poprzedni JSP:

Przykład 4. header.jsp

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

Przykład 5. content.jsp

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

Przykład 6. footer.jsp

<hr>Thanks for stopping by!

Mimo że JSP z przykładu 2 używa <jsp:include> do ponownego użycia zawartości, nie można ponownie użyć układu strony, ponieważ jest on zakodowany w tym JSP. Struts Tiles pozwala na ponowne wykorzystanie zarówno treści, jak i układu, jak pokazano w następnej sekcji.

Implementuj złożone układy za pomocą Struts Tiles

Przykład 7 pokazuje stronę z rysunku 3 zaimplementowaną za pomocą Struts Tiles:

Przykład 7. Use Struts Tiles to encapsulate layout

Poprzedni JSP używa znacznika <tiles:insert> do utworzenia JSP z rysunku 3. Ten JSP jest zdefiniowany przez definicję kafelków o nazwie sidebar-header-footer-definition. Definicja ta znajduje się w pliku konfiguracyjnym Tiles, którym w tym przypadku jest plik WEB-INF/tiles-defs.xml, wymieniony w przykładzie 8:

Przykład 8. WEB-INF/tiles-defs.xml

Poprzednia definicja Tiles określa układ strony, zamknięty w pliku header-footer-sidebar-layout.jsp, oraz zawartość strony, zamkniętą w plikach sidebar.jsp, header.jsp, content.jsp i footer.jsp, jak wymieniono w przykładach 3-6. Przykład 9 zawiera JSP, który definiuje layout-header-footer-sidebar-layout.jsp:

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

Poprzedni JSP enkapsuluje layout i wstawia zawartość zgodnie z wartościami określonymi dla regionów sidebar, editor, content i footer w pliku definicji Tiles, ułatwiając w ten sposób ponowne użycie zarówno zawartości, jak i layoutu. Na przykład, można zdefiniować inną definicję Tiles z tym samym układem, tym samym paskiem bocznym, edytorem i stopką, ale inną zawartością:

Aby utworzyć JSP zdefiniowany przez definicję Tiles a-different-sidebar-header-footer-definition, używamy znacznika <tiles:insert>, jak poniżej:

Dzięki Struts Tiles, można ponownie użyć zarówno zawartości, jak i układu, co okazuje się nieocenione dla witryn z wieloma JSP, które współdzielą układ i część zawartości. Jeśli jednak przyjrzymy się dokładnie kodowi z przykładów 7-9, zauważymy, że layout dla regionu sidebar jest zakodowany w sidebar.jsp, który jest wymieniony w przykładzie 3. Oznacza to, że nie można ponownie użyć tego układu. Na szczęście, biblioteka znaczników Tiles implementuje wzorzec Composite, który pozwala nam określić definicję kafelków – zamiast JSP – dla regionu. W następnej sekcji wyjaśniam, jak używać tej implementacji wzorca Composite.

Używaj wzorca Composite ze Struts Tiles

Struts Tiles implementuje wzorzec Composite, gdzie klasa Component jest reprezentowana przez JSP, a klasa Composite jest reprezentowana przez definicję Tiles. Ta implementacja pozwala określić albo JSP (komponent) albo definicję Tiles (kompozyt) jako zawartość regionu JSP. Przykład 10 ilustruje tę właściwość:

Przykład 10. WEB-INF/tiles-defs.xml: Użyj wzorca Composite

Poprzedni plik konfiguracyjny Tiles definiuje dwie definicje Tiles: sidebar-definition i sidebar-header-footer-definition. Wartość sidebar-definition jest określona jako wartość dla regionu paska bocznego w sidebar-header-footer-definition. Można je tak określić, ponieważ Tiles implementuje wzorzec Composite, pozwalając Tiles określić definicję (Composite, która jest kolekcją JSP) tam, gdzie normalnie określilibyśmy pojedynczy JSP (który jest Component).

Układ paska bocznego jest zamknięty w sidebar-layout.jsp Przykładu 11:

Przykład 11. sidebar-layout.jsp

W przykładzie 12 wymieniono flags.jsp, określony jako zawartość regionu top paska bocznego, a w przykładzie 13 wymieniono sidebar-links.jsp, określony jako region bottom paska bocznego:

Przykład 12. flags.jsp

Przykład 13. sidebar-links.jsp

Teraz sidebar-definition może definiować inne regiony z górnym i dolnym komponentem, chociaż prawdopodobnie powinieneś zmienić nazwę tej definicji na coś bardziej ogólnego, jak top-bottom-definition.

Wszystko to kompozyty w tych dniach

Wzorzec Composite jest popularny wśród frameworków prezentacji, takich jak Swing i Struts, ponieważ pozwala zagnieżdżać kontenery poprzez traktowanie komponentów i ich kontenerów dokładnie tak samo. Struts Tiles używa wzorca Composite do określenia prostego JSP lub definicji Tiles – która jest kolekcją JSP – jako zawartości kafelka. Jest to potężna możliwość, która ułatwia zarządzanie dużymi witrynami o różnych układach.

Poniższa sekcja „Zadanie domowe z ostatniego czasu” rozszerza dyskusję tego artykułu poprzez internacjonalizację poprzedniej aplikacji za pomocą akcji Struts i biblioteki JSP Standard Tag Library (JSTL).

Zadanie domowe

Przedyskutuj, w jaki sposób Swing implementuje wzorzec kompozytowy za pomocą klas Component i Container.

Praca domowa z ostatniego czasu

W ostatnim zadaniu poprosiłeś o pobranie Struts z http://jakarta.apache.org/struts/index.html i zaimplementowanie własnej klasy akcji Struts.

Dla tego zadania zdecydowałem się zaimplementować akcję Struts w połączeniu z biblioteką JSP Standard Tag Library (JSTL), aby umiędzynarodowić aplikację internetową Przykładu 1. Chociaż Struts dostarcza niezbędną infrastrukturę do internacjonalizacji Twoich aplikacji internetowych, powinieneś użyć JSTL do tego zadania, ponieważ JSTL jest standardem. W pewnym momencie, możliwości internacjonalizacji Strutsa zostaną prawdopodobnie zdeprecjonowane lub zintegrowane z JSTL.

Po tym jak zinternacjonalizowałem aplikację internetową Przykładu 1 za pomocą akcji Strutsa i JSTL, zlokalizowałem tę aplikację dla języka chińskiego. Rysunek H1 ilustruje rezultat.

Uwaga: Nie znam ani jednego słowa po chińsku, nie mówiąc już o tym jak pisać w tym języku, więc chiński na rysunku H1 jest sfabrykowany z arbitralnych łańcuchów Unicode. Ale wygląda fajnie, niezależnie od tego.

Rysunek H1. Internacjonalizacja za pomocą akcji Struts. Kliknij na miniaturę, aby zobaczyć obraz w pełnym rozmiarze.

Uwaga Określiłem atrybuty href w flags.jsp Przykładu 12 jako puste łańcuchy, więc kiedy klikniesz na flagi, kontener serwletów przeładuje bieżący JSP. Dlatego pierwszym krokiem w kierunku internacjonalizacji aplikacji internetowej jest określenie adresu URL dla tych atrybutów, jak podano w przykładzie H1:

Przykład H1. flags.jsp

Adres URL dla każdego atrybutu href jest taki sam: flags.do. Dwa parametry żądania są dołączone do tego adresu URL: jeden dla locale odpowiadającego fladze i drugi, który reprezentuje bieżącą ścieżkę JSP. Ten ostatni parametr żądania uzyskuje się przez wywołanie metody Http.ServletRequest.getServletPath().

W deskryptorze wdrożenia aplikacji zmapowałem wszystkie adresy URL kończące się na .do do serwletu akcji Struts, w ten sposób:

Przykład H2. WEB-INF/web.xml (Excerpt)

Uwaga: Zobacz mój artykuł „Take Command of Your Software” (JavaWorld, czerwiec 2002) po więcej informacji o Struts i serwlecie akcji Struts.

Następnie, zmapowałem ścieżkę /flags do akcji Struts actions.FlagAction w pliku konfiguracyjnym Struts, wymienionym w Przykładzie H3:

Przykład H3. WEB-INF/struts-config.xml

Z powodu tego mapowania, URL flags.do powoduje, że serwlet akcji Struts wywołuje metodę actions.FlagAction.execute(); dlatego kliknięcie na flagę wywoła metodę actions.FlagAction.execute(). Przykład H4 wymienia klasę actions.FlagAction:

Przykład H4. WEB-INF/classes/actions/FlagAction.java

Metoda actions.FlagAction.execute() uzyskuje referencję do parametru żądania locale i przekazuje tę wartość do metody set() klasy JSTL Config. Metoda ta przechowuje łańcuch reprezentujący locale w ustawieniu konfiguracyjnym FMT_LOCALE, którego JSTL używa do lokalizacji tekstu i formatowania liczb, walut, procentów i dat.

Teraz, gdy określiłem locale dla działań internacjonalizacyjnych JSTL, określam pakiet zasobów w deskryptorze wdrożenia aplikacji, którego fragment znajduje się w przykładzie H5:

Przykład H5. WEB-INF/web.xml (Excerpt)

Podstawowa nazwa wiązki zasobów resources jest określona dla parametru javax.servlet.jsp.jstl.fmt.localizationContext context-initialization. Oznacza to, że JSTL będzie szukał pliku o nazwie resources_en.properties, gdy locale jest ustawione na brytyjski angielski (en-GB) i resources_zh.properties, gdy locale jest ustawione na chiński (zh-ZH).

Teraz, gdy określiłem resource bundle i locale dla akcji internacjonalizacji JSTL, modyfikuję pliki JSP, aby użyć tych akcji, jak pokazują przykłady H6-H9:

Przykład H6. sidebar-links.jsp

Przykład H7. header.jsp

Przykład H8. content.jsp

Przykład H9. footer.jsp

Przykłady H6-H9 w JSP używają akcji JSTL <fmt:message> do wyodrębnienia łańcuchów Unicode z pakietu zasobów określonego w deskryptorze wdrożenia. Przykłady H10 i H11 zawierają listę pakietów zasobów dla języka angielskiego i chińskiego, odpowiednio:

Przykład H10. WEB-INF/classes/resources_en.properties

Przykład H11. WEB-INF/classes/resources_zh.properties

Email

W mailu do mnie, Joseph Friedman napisał:

W „Decorate Your Java Code” (JavaWorld, grudzień 2001), piszesz:

„Obiekt zamykający – znany jako dekorator – jest zgodny z interfejsem obiektu, który zamyka, pozwalając dekoratorowi być używanym tak, jakby był instancją obiektu, który zamyka.”

Jednakże przykład kodu dekoruje FileReader za pomocą LineNumberReader i wywołuje readLine(), który nie jest w interfejsie FileReader; zatem nie używasz LineNumberReader w sposób przezroczysty.

Zarówno FileReader, jak i LineNumberReader są zgodne z interfejsem zdefiniowanym przez klasę Reader, którą obie rozszerzają. Chodzi o to, że możesz przekazać dekorator do dowolnej metody, która oczekuje odniesienia do Reader. Metody te pozostają w błogiej nieświadomości specjalnych możliwości tego dekoratora – w tym przypadku, zdolności do czytania plików i tworzenia numerów linii. Jednakże, jeśli wiesz o tych specjalnych możliwościach, możesz je wykorzystać.

Fakt, że dekorator posiada (jedną lub więcej) metod, których brakuje dekorowanemu obiektowi, w żaden sposób nie narusza intencji wzorca Dekorator; w rzeczywistości jest to główna cecha tego wzorca: dodawanie funkcjonalności w czasie wykonywania do obiektu poprzez rekurencyjne dekorowanie obiektów.

Jeśli spojrzysz na Design Patterns strona 173, zobaczysz to:

Podklasy dekoratora mogą swobodnie dodawać operacje dla specyficznej funkcjonalności. Na przykład, operacja ScrollTo w ScrollDecorator pozwala innym obiektom przewijać interfejs, jeśli* wiedzą, że w interfejsie znajduje się obiekt ScrollDecorator.

DavidGeary jest autorem książki Core JSTL Mastering the JSP Standard TagLibrary, która ukaże się jesienią tego roku nakłademPrentice-Hall i Sun Microsystems Press; Advanced JavaServer Pages (PrenticeHall, 2001; ISBN: 0130307041); oraz serii Graphic Java (Sun MicrosystemsPress). David od 18 lat zajmuje się tworzeniem oprogramowania zorientowanego obiektowo w wielu językach obiektowych. Od czasu opublikowania książki GOFDesign Patterns w 1994 roku, David jest aktywnym zwolennikiem wzorców projektowych i używa oraz implementuje wzorce projektowe w Smalltalk, C++ i Java. W 1997 roku David rozpoczął pełnoetatową pracę jako autor oraz okazjonalny mówca i konsultant. David jest członkiem grup ekspertów definiujących niestandardową bibliotekę znaczników standardu JSP oraz JavaServer Faces, a także współtworzy framework Apache Struts JSP.

Naucz się więcej na ten temat

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.