Composite (Kompozyt)

composite kompozyt uml

Opis

Wzorzec projektowy Composite (Kompozyt) należy do grupy wzorców strukturalnych. Umożliwia łączenie obiektów w struktury drzewiaste w taki sposób, by zarówno pojedynczy obiekt jak i ich kolekcja mogły być traktowane jednakowo.

Występuje kilka nazewnictw dla relacji obiektów rodzic-potomek. Spotkać można na przykład takie jak, odpowiednio: kompozyt-liść, komponent-składnik. Nierzadko używane są na przemian, a nawet pomieszane.

Problem i rozwiązanie

Reprezentacje drzewiaste w programowaniu można spotkać bardzo często. Mówiąc o OOP, na myśl przychodzi oczywiście hierarchia obiektów. Obsłużyć ją można co najmniej na kilka sposobów, a niektóre z nich prawie zawsze prowadzą do tych samych problemów. Szczególnie w przypadkach, gdzie klasa potomka jest bardzo podobna do rodzica, a implementacja zakłada, że są to dwa różne byty.

Zacznie się ciągłe sprawdzanie, z obiektem jakiego typu ma się do czynienia w konkretnej akcji albo na jakie operacje pozwala. A co, gdyby traktować je w ten sam sposób? Uregulować je wspólnym interfejsem, dzięki czemu klient realizujący akcję nie musi się martwić, czy operacje wykonuje na obiekcie potomka, czy rodzica. Właśnie takie podejście, dzięki rekurencji, może usprawnić obsługę hierarchicznych modeli. Oczywiście rekurencja dla bardzo złożonych struktur bywa kosztowna, ale odpowiednim buforowaniem da się pewne operacje zoptymalizować.

Plusy i minusy

Wzorzec projektowy Kompozyt ma wiele zalet. Przede wszystkim bardzo łatwo rozszerza się jego strukturę o kolejne klasy należące do hierarchii. Wystarczy odnieść się do właściwego interfejsu (bądź klasy abstrakcyjnej) i zapewnić odpowiednie implementacje. Co ważne, takie zmiany nie wiążą się też z modyfikacjami istniejącego już kodu, co przekłada się na pełne wsparcie dla reguły Otwarte/Zamknięte.

Klient realizujący daną akcję nie ma pojęcia jaką pozycję przyjmuje obiekt w strukturze (enkapsulacja). Takie rozwiązanie gwarantuje sporą elastyczność. Obiekty mające relację do innych obiektów to właściwie codzienność, dlatego warto w swojej skrzynce narzędziowej mieć Composite.

W miarę jak model klas się rozrasta, interfejs staje się coraz trudniejszy do utrzymania – to niewątpliwie spora wada. Struktura drzewiasta sama w sobie jest już dość skomplikowana, więc nie ma się co dziwić. Wszystko komplikuje się jednak w momencie, w którym obiekty zaczynają się coraz bardziej różnić. W takiej sytuacji wspólny interfejs okazuje się tylko dlatego „wspólny”, że wszystkie klasy go implementują. Nie ma to jednak przełożenia na ich faktyczne zachowania.

Przykładowa implementacja w PHP

W poniższej implementacji występują dwa obiekty złączone przez wspólny interfejs i jedna klasa testująca podstawowe funkcjonalności. Przykład ten ilustruje strukturę drzewiastą jaką jest relacja obiektów katalog i plik. Folder może posiadać wewnątrz inne foldery, bądź pliki. Oba obiekty nie są jednak bardzo różne w założeniach i operacje, które można na nich wykonać najprawdopodobniej będą tożsame (czasem realizowane w inny sposób). Jest to świetna okazja do użycia wzorca Kompozyt.

Warto dodać, że nierzadko obiektów tych będzie więcej niż dwa, a ich wzajemne relacje mogą być na tyle skomplikowane, że każdy może posiadać każdego. W takich sytuacjach to narzędzie jeszcze bardziej pozwala uprościć kod. Żeby niepotrzebnie nie zaciemniać, przykład poniżej ma najprostszą z możliwych form.

Każdy z elementów systemu plików ma swój unikalny identyfikator (normalnie możliwe, że byłaby to nazwa, która musi być unikalna w ramach konkretnej przestrzeni) i metodę pozwalająca odczytać rozmiar. Oczywiście na rozmiar składają się wszystkie elementy zawarte w danym komponencie. Analogicznie jak waga pudełka zawierającego w sobie inne pudełka. I tak dla liścia (plik) jest to po prostu jego rozmiar, a dla kompozytu (katalog) jest to rozmiar wszystkich elementów podrzędnych. Wartość dla pustego folderu wynosi zero, chociaż niekoniecznie pusty folder może istnieć – zależy od implementacji.

Jako, że oba obiekty traktowane są w ten sam sposób, to dla kompozytu wyliczenie rozmiaru kończy się na jednym foreachu – chociaż występuje tutaj mechanizm rekurencji, więc dla dużych struktur wewnątrz wykona się wiele pętli. Operacja ta może okazać się kosztowna – jeśli jest taka obawa to warto pomyśleć o buforowaniu. Wówczas nie tylko klasa File będzie miała pole $size. Przy każdym dodaniu elementu za pomocą metody addElement() jej wartość, dla wszystkich elementów mieszczących się wyżej w hierarchii, będzie rosła. Poniższa implementacja nie zakłada usuwania elementów, ale oczywiście należałoby to uwzględnić w przypadku utrwalania rozmiaru. Przy wyliczaniu dynamicznym nie trzeba się tym martwić, chociaż operacja może być bardziej zasobochłonna.

<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Composite;

interface FileSystemElementInterface
{
    public function getId(): string;

    public function getSize(): int;
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Composite;

class File implements FileSystemElementInterface
{
    private string $id;

    private int $size;

    public function __construct(int $size)
    {
        $this->id = uniqid();
        $this->size = $size;
    }

    public function getId(): string
    {
        return $this->id;
    }

    public function getSize(): int
    {
        return $this->size;
    }
}

<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Composite;

class Directory implements FileSystemElementInterface
{
    private string $id;

    private array $elements;

    public function __construct()
    {
        $this->id = uniqid();
        $this->elements = [];
    }

    public function getId(): string
    {
        return $this->id;
    }

    public function getSize(): int
    {
        $size = 0;
        /** @var FileSystemElementInterface $element */
        foreach ($this->elements as $element) {
            $size += $element->getSize();
        }

        return $size;
    }

    public function addElement(FileSystemElementInterface $element): void
    {
        $this->elements[$element->getId()] = $element;
    }
}

<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Composite\Test;

use DesignPatterns\Structural\Composite\Directory;
use DesignPatterns\Structural\Composite\File;
use PHPUnit\Framework\TestCase;

final class DirectoryTest extends TestCase
{
    public function testCanGetSizeOfEmptyDirectory(): void
    {
        $this->assertSame(0, (new Directory())->getSize());
    }

    public function testCanGetSizeOfDirectoryThatContainsOnlyFiles(): void
    {
        $directory = new Directory();

        $directory->addElement(new File(12));
        $directory->addElement(new File(8));

        $this->assertSame(20, $directory->getSize());
    }

    public function testCanGetSizeOfDirectoryThatContainsDirectoriesAndFiles(): void
    {
        $firstChildDirectory = new Directory();
        $firstChildDirectory->addElement(new File(12));

        $secondChildDirectory = new Directory();
        $secondChildDirectory->addElement(new File(12));
        $secondChildDirectory->addElement(new File(24));
        $secondChildDirectory->addElement(new File(12));

        $parentDirectory = new Directory();
        $parentDirectory->addElement(new File(16));
        $parentDirectory->addElement(new File(24));
        $parentDirectory->addElement($firstChildDirectory);
        $parentDirectory->addElement($secondChildDirectory);

        $this->assertSame(100, $parentDirectory->getSize());
    }
}

Composite – podsumowanie

Zastosowanie wzorca projektowego Composite na pewno wymaga głębszego przemyślenia. Kiedy powstaje model, bardzo często ma w sobie kilka pól i metod. Na tym etapie widać więcej podobieństw, niż różnic między klasami. W przyszłości może jednak okazać się, że klasa rodzica jest całkowicie różna od potomka. Wówczas wspólny interfejs staje się uciążliwy.

Kompozyt użyty we właściwym miejscu bezapelacyjnie upraszcza nawet skomplikowane struktury drzewiaste. Warto korzystać z niego w aplikacjach, ale należy rozważnie podchodzić do jego użycia.

Krystian Żądło
Programista PHP i właściciel marki Koddlo. Pasjonat czystego kodu i dobrych praktyk programowania obiektowego. Prywatnie fan angielskiej piłki nożnej, dobrego humoru oraz podcastów.