Kompozyt
Cel
Kompozyt to strukturalny wzorzec projektowy pozwalający komponować obiekty w struktury drzewiaste, a następnie traktować te struktury jakby były osobnymi obiektami.
Problem
Stosowanie wzorca Kompozyt ma sens tylko w przypadku, gdy główny model twojej aplikacji można przedstawić w formie drzewa.
Na przykład, wyobraź sobie, że masz dwa typy obiektów: Produkty
i Opakowania
. Opakowanie
może zawierać wiele Produktów
, a także pewną liczbę mniejszych Opakowań
. Te małe Opakowania
także mogą przechowywać zarówno Produkty
, jak i jeszcze mniejsze Opakowania
i tak dalej.
Załóżmy, że postanowisz stworzyć system zamawiania, który wykorzystuje jakieś klasy. Zamówienia mogą obejmować proste produkty nie posiadające opakowań, a także pudła wypełnione produktami i… innymi pudełkami. Jak wówczas określić całkowitą wartość pieniężną takiego zamówienia?
Możesz spróbować bezpośredniego podejścia: rozpakuj wszystkie pudełka, przejrzyj ich zawartość i oblicz sumę. W prawdziwym życiu jest to wykonalne, ale w programie nie jest to niestety tak proste, jak działanie w pętli. Musisz z góry znać klasy Produktów
i Opakowań
jakie przeglądasz, poziom zagnieżdżenia opakowań i inne kłopotliwe szczegóły. Wszystko to sprawia, że bezpośrednie podejście byłoby problematyczne, albo wręcz niemożliwe.
Rozwiązanie
Wzorzec projektowy Kompozyt zakłada rozwiązanie w którym zarówno z Produktami
, jak i Opakowaniami
pracujemy poprzez wspólny interfejs, deklarujący metodę obliczania całej sumy.
Jak by to działało? W przypadku produktu, metoda zwracałaby jego cenę. W przypadku pudła, przejrzałaby zawartość przedmiot po przedmiocie, pytając o cenę każdego z nich, a na końcu zwróciła całkowitą wartość opakowania. Jeśli któryś z przedmiotów w pudle okazałby się mniejszym pudełkiem, również przejrzałaby jego zawartość i tak aż do obliczenia wartości wszystkich obiektów wewnątrz. Samo opakowanie mogłoby nawet dodawać swoją wartość do ostatecznej ceny.
Największą zaletą tego podejścia jest to, że nie musimy się przejmować konkretną klasą obiektów składających się na drzewo. Nie musimy wiedzieć, czy obiekt jest prostym produktem, czy też złożonym kontenerem. Traktujemy wszystko w taki sam sposób, za pomocą takiego samego interfejsu. Gdy wywołasz metodę, same obiekty przekażą ją sobie dalej.
Analogia do prawdziwego życia
Armie większości państw mają strukturę hierarchiczną. Armia składa się z wielu dywizji; dywizje dzielą się na brygady, a brygady składają się z drużyn. Rozkazy wydaje się odgórnie, po czym są one przekazywane w dół, po każdym z poziomów, aż każdy żołnierz będzie wiedział co jest do zrobienia.
Struktura
-
Interfejs Komponentu opisuje operacje wspólne zarówno dla prostych, jak i złożonych elementów drzewa.
-
Liść jest podstawowym elementem drzewa i nie posiada elementów podrzędnych.
Zazwyczaj, to właśnie komponenty-liście wykonują większość faktycznej pracy, ponieważ nie mają komu jej zlecić.
-
Kontener (zwany też kompozytem) jest elementem posiadającym elementy podrzędne: liście, lub inne kontenery. Kontener nie zna konkretnych klas swojej zawartości. Komunikuje się ze wszystkimi elementami podrzędnymi tylko poprzez interfejs komponentu.
Otrzymawszy żądanie, kontener deleguje pracę do swoich podelementów, przetwarza wyniki pośrednie i zwraca ostateczny wynik klientowi.
-
Klient współpracuje ze wszystkimi elementami za pośrednictwem interfejsu komponentu. W wyniku tego klient może działać w taki sam sposób zarówno na prostych, jak i złożonych elementach drzewa.
Pseudokod
W tym przykładzie, wzorzec Kompozyt pozwala zaimplementować układanie figur geometrycznych w stos w programie graficznym.
Klasa ZłożonaGrafika
jest kontenerem, na który może składać się każda liczba podrzędnych figur, w tym inne figury złożone. Figura złożona ma takie same metody jak pojedyncza figura. Jednakże, zamiast robić coś samodzielnie, figura złożona przekazuje żądanie rekursywnie wszystkim swoim elementom, a sama “zsumowuje” wynik.
Kod kliencki współpracuje ze wszystkimi figurami za pomocą interfejsu który jest jednakowy dla wszystkich klas figur. Tym samym klient nie jest świadomy, czy ma do czynienia z prostym kształtem, czy złożonym. Klient może pracować z bardzo złożonymi strukturami bez wiązania się z konkretnymi klasami tworzącymi strukturę.
Zastosowanie
Stosuj wzorzec Kompozyt gdy musisz zaimplementować drzewiastą strukturę obiektów.
Wzorzec Kompozyt określa dwa podstawowe typy elementów współdzielących jednakowy interfejs: proste liście oraz złożone kontenery. Kontener może być złożony zarówno z liści, jak i z innych kontenerów. Pozwala to skonstruować zagnieżdżoną, rekurencyjną strukturę obiektów przypominającą drzewo.
Stosuj ten wzorzec gdy chcesz, aby kod kliencki traktował zarówno proste, jak i złożone elementy jednakowo.
Wszystkie elementy zdefiniowane przez wzorzec Kompozyt współdzielą jeden interfejs. Dzięki temu, klient nie musi martwić się konkretną klasą obiektów z jakimi ma do czynienia.
Jak zaimplementować
-
Upewnij się, że główny model twojej aplikacji można przedstawić w formie struktury drzewiastej. Spróbuj rozdzielić go na proste elementy i kontenery. Pamiętaj, że kontenery muszą móc zawierać w sobie zarówno proste elementy, jak i inne kontenery.
-
Zadeklaruj interfejs komponentu z listą metod które mają sens zarówno w przypadku prostych, jak i złożonych komponentów.
-
Stwórz klasę-liść reprezentującą proste elementy. Program może posiadać wiele różnych klas-liści.
-
Stwórz klasę-kontener, reprezentującą złożone elementy. W tej klasie umieść pole tablicowe, które przechowywać będzie odniesienia do elementów podrzędnych. Tablica musi być w stanie przechowywać zarówno liście, jak i kontenery, więc zadeklaruj jej typ jako interfejs komponentu.
Implementując metody interfejsu komponentu, pamiętaj, że kontener ma delegować większość swych obowiązków elementom podrzędnym.
-
Na koniec zdefiniuj metody pozwalające dodawać i usuwać elementy podrzędne w kontenerze.
Pamiętaj, że powyższe operacje można zadeklarować w interfejsie komponentu. Łamie to Zasadę segregacji interfejsów, ponieważ metody będą puste w klasie-liść. W zamian, klient będzie w stanie traktować wszystkie elementy jednakowo, nawet komponując drzewo.
Zalety i wady
- Można pracować ze skomplikowanymi strukturami drzewiastymi w wygodny sposób: wykorzystaj na swoją korzyść polimorfizm i rekursję.
- Zasada otwarte/zamknięte. Możesz wprowadzać do programu obsługę nowych typów elementów bez psucia istniejącego kodu, gdyż pracuje on teraz z drzewem różnych obiektów.
- Ustalenie wspólnego interfejsu dla klas o diametralnie różnych funkcjonalnościach może okazać się trudne. W pewnych przypadkach trzeba przesadnie uogólnić interfejs komponentu, co uczyni go trudniejszym do zrozumienia.
Powiązania z innymi wzorcami
-
Możesz zastosować wzorzec Budowniczy by tworzyć złożone drzewa Kompozytowe dzięki możliwości zaprogramowania ich etapów konstrukcji tak, aby odbywały się rekurencyjnie.
-
Łańcuch zobowiązań często stosuje się w połączeniu z Kompozytem. W takim przypadku, gdy komponent-liść otrzymuje żądanie, może je przekazać poprzez łańcuch nadrzędnych komponentów aż do korzenia drzewa obiektów.
-
Iteratory służą do sekwencyjnego przemieszczania się po drzewie Kompozytowym element po elemencie.
-
Odwiedzający może wykonać działanie na całym drzewie Kompozytowym.
-
Węzły będące liśćmi drzewa Kompozytowego można zaimplementować jako Pyłki by zaoszczędzić nieco pamięci RAM.
-
Kompozyt i Dekorator mają podobne diagramy struktur ponieważ oba bazują na rekursywnej kompozycji w celu zorganizowania nieokreślonej liczby obiektów.
Dekorator przypomina Kompozyt, ale posiada tylko jeden element podrzędny. Ponadto, kolejną różnicą jest to, że Dekorator przypisuje dodatkowe obowiązki opakowywanemu obiektowi, zaś Kompozyt jedynie “sumuje” wyniki otrzymane od elementów podrzędnych.
Wzorce mogą też współpracować: Dekorator może służyć rozszerzeniu zachowania określonego obiektu w drzewie Kompozytowym
-
Projekty intensywnie korzystające ze wzorców Kompozyt i Dekorator mogą skorzystać również na zastosowaniu Prototypu. Zastosowanie tego wzorca pozwala klonować złożone struktury zamiast konstruować je ponownie od zera.