Autumn SALE

Kompozyt

Znany też jako: Drzewo obiektów, Object Tree, Composite

Cel

Kompozyt to strukturalny wzorzec projektowy pozwalający komponować obiekty w struktury drzewiaste, a następnie traktować te struktury jakby były osobnymi obiektami.

Wzorzec projektowy Kompozyt

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?

Struktura złożonego zamówienia

Zamówienie może obejmować różnorakie produkty opakowane w pudełka, które z kolei są zapakowane w większych pudełkach, i tak dalej. Cała struktura przypomina odwrócone drzewo.

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.

Rozwiązanie sugerowane przez wzorzec Kompozyt

Wzorzec Kompozyt pozwala wykonywać działania rekursywnie — po wszystkich komponentach drzewa.

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

Przykład struktury w wojsku

Przykład struktury w wojsku.

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

Struktura wzorca KompozytStruktura wzorca Kompozyt
  1. Interfejs Komponentu opisuje operacje wspólne zarówno dla prostych, jak i złożonych elementów drzewa.

  2. 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ć.

  3. 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.

  4. 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.

Struktura przykładu Kompozytu

Przykład edytora figur geometrycznych.

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ę.

// Interfejs komponentu deklaruje działania wspólne zarówno dla
// prostych jak i skomplikowanych obiektów struktury.
interface Graphic is
    method move(x, y)
    method draw()

// Klasa liść reprezentuje końcowe elementy struktury. Liść nie
// posiada pod-obiektów. Na ogół to właśnie te obiekty wykonują
// faktyczne działania, zaś obiekty złożone jedynie je delegują
// swoim pod-komponentom.
class Dot implements Graphic is
    field x, y

    constructor Dot(x, y) { ... }

    method move(x, y) is
        this.x += x, this.y += y

    method draw() is
        // Narysuj kropkę w miejscu o współrzędnych X i Y.

// Wszystkie klasy komponentów mogą rozszerzać inne komponenty.
class Circle extends Dot is
    field radius

    constructor Circle(x, y, radius) { ... }

    method draw() is
        // Narysuj okrąg w punkcie X i Y o promieniu R.

// Klasa kompozyt reprezentuje komponenty złożone które mogą
// posiadać potomstwo. Obiekty kompozytowe zazwyczaj delegują
// faktyczną pracę swoim dzieciom a następnie "podsumowują"
// otrzymane wyniki.
class CompoundGraphic implements Graphic is
    field children: array of Graphic

    // Obiekt będący kompozytem może dodawać lub usuwać inne
    // komponenty (zarówno proste jak i złożone) do/ze swej
    // listy obiektów-dzieci.
    method add(child: Graphic) is
        // Dodaj obiekt potomny do tablicy potomstwa.

    method remove(child: Graphic) is
        // Usuń obiekt-dziecko z tablicy potomstwa.

    method move(x, y) is
        foreach (child in children) do
            child.move(x, y)

    // Kompozyt wykonuje swoje podstawowe zadania w konkretny
    // sposób. Przechodzi rekursywnie przez wszystkie obiekty
    // podrzędne, zbierając od nich wyniki i je sumując. Skoro
    // obiekty-dzieci kompozytu przekazują te wywołania swoim
    // obiektom-dzieciom i tak dalej, całe drzewo obiektów
    // zostaje przejrzane.
    method draw() is
        // 1. Dla każdego komponentu-dziecka:
        //     - Narysuj komponent.
        //     - Zaktualizuj otaczający prostokąt.
        // 2. Narysuj prostokąt przerywaną linią korzystając ze
        // współrzędnych granicy.


// Kod klienta współpracuje ze wszystkimi komponentami za
// pośrednictwem ich interfejsu bazowego. Dzięki temu kod
// kliencki posiada wsparcie zarówno prostych obiektów-liści,
// jak i złożonych kompozytów.
class ImageEditor is
    field all: CompoundGraphic

    method load() is
        all = new CompoundGraphic()
        all.add(new Dot(1, 2))
        all.add(new Circle(5, 3, 10))
        // ...

    // Połącz wybrane komponenty w jeden złożony komponent
    // kompozytowy.
    method groupSelected(components: array of Graphic) is
        group = new CompoundGraphic()
        foreach (component in components) do
            group.add(component)
            all.remove(component)
        all.add(group)
        // Wszystkie komponenty zostaną narysowane.
        all.draw()

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ć

  1. 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.

  2. Zadeklaruj interfejs komponentu z listą metod które mają sens zarówno w przypadku prostych, jak i złożonych komponentów.

  3. Stwórz klasę-liść reprezentującą proste elementy. Program może posiadać wiele różnych klas-liści.

  4. 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.

  5. 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.

Przykłady kodu

Kompozyt w języku C# Kompozyt w języku C++ Kompozyt w języku Go Kompozyt w języku Java Kompozyt w języku PHP Kompozyt w języku Python Kompozyt w języku Ruby Kompozyt w języku Rust Kompozyt w języku Swift Kompozyt w języku TypeScript