Wzorzec composite

Jak widać, wszystkie jednostki naszego modelu rozszerzają klasę Unit. Użytkownik może więc być pewien, że każdy z obiektów hierarchii Unit będzie obsługiwał metodę bombardStrength(). Klasa Unit może być traktowana identycznie jak Archer. Klasy Army i TroopCarrier są kompozytami (ang. composite) — obiektami składającymi się z innych obiektów. Klasy Archer i LaserCannonUnit to liście bądź końcówki (ang. leaves), klasy reprezentujące końcowe węzły struktury drzewiastej przystosowane do obsługi podstawowych operacji hierarchii, ale nienadające się do przechowywania innych jej obiektów. Pojawia się wątpliwość, czy liście powinny implementować identyczny interfejs, co kompozyty (jak na rysunku 10.1). Na diagramie widać, że TroopCarrier i Army agregują inne jednostki, ale klasy liści również implementują wywołanie addUnit() — wrócimy do tego wkrótce. Na razie przyjrzyjmy się abstrakcyjnej klasie Unit:

<?php
abstract class Unit
{
    abstract function addUnit(Unit $unit);
    abstract function removeUnit(Unit $unit);
    abstract function bombardStrength();
}
//Mamy tu zarys podstawowej funkcjonalności wszystkich obiektów hierarchii Unit. Spójrzmy teraz,
//jak wymienione metody abstrakcyjne mogłyby być implementowane w obiektach-kompozytach:
class Army extends Unit
{
    private $units = array();
    function addUnit(Unit $unit)
    {
        if (in_array($unit, $this->units, true))
        {
            return;
        }
        $this->units[] = $unit;
    }
    function removeUnit(Unit $unit)
    {
        $this->units = array_udiff($this->units, array(
            $unit
        ) , function ($a, $b)
        {
            return ($a === $b) ? 0 : 1;
        });
    }
    function bombardStrength()
    {
        $ret = 0;
        foreach ($this->units as $unit)
        {
            $ret += $unit->bombardStrength();
        }
        return $ret;
    }
}

Metoda addUnit() klasy Army przed włączeniem do oddziału przekazywanej w wywołaniu jednostki sprawdza, czy nie posiada jej już w prywatnej tablicy jednostek. Metoda removeUnit() sprawdza w pętli (podobnie jak metoda addUnit()), czy usunąć dany obiekt Unit.

Obiekty klasy Army mogą przechowywać dowolnego rodzaju obiekty hierarchii Unit, w tym inne obiekty klasy Army lub końcówki takie jak Archer czy LaserCannonUnit. Ponieważ wszystkie jednostki mają implementować metodę bombardStrength(), implementacja tej metody w klasie Army sprowadza się do przejrzenia wszystkich obiektów zawieranych, przechowywanych w składowej $units, i sumowania wartości zwracanych z inicjowanych na ich rzecz wywołań bombardStrength(). Problematycznym aspektem wzorca pozostaje implementacja operacji wcielania i usuwania jednostek. Klasyczny wzorzec zakłada definicję metod add...() i remove...() w abstrakcyjnej klasie bazowej. Dzięki temu wszystkie klasy objęte szablonem udostępniają wspólny interfejs. Ale przez to implementacje tych m

<?php
class UnitException extends Exception
{
}
class Archer extends Unit
{
    function addUnit(Unit $unit)
    {
        throw new UnitException(get_class($this) . " to liść");
    }
    function removeUnit(Unit $unit)
    {
        throw new UnitException(get_class($this) . " to liść");
    }
    function bombardStrength()
    {
        return 4;
    }
}

Z definicji klasa Archer nie jest przewidziana do przechowywania obiektów hierarchii Unit, więc na wywołanie na rzecz obiektu Archer metody addUnit() albo removeUnit() reagujemy zgłoszeniem wyjątku. Ponieważ musielibyśmy podobną implementację przewidzieć dla wszystkich klas końcówek (liści), możemy zdecydować się na jej przeniesienie do abstrakcyjnej klasy bazowej

<?php
abstract class Unit
{
    abstract function bombardStrength();
    function addUnit(Unit $unit)
    {
        throw new UnitException(get_class($this) . " to liść");
    }
    function removeUnit(Unit $unit)
    {
        throw new UnitException(get_class($this) . " to liść");
    }
}
class Archer extends Unit
{
    function bombardStrength()
    {
        return 4;
    }
}

// utworzenie armii
$main_army = new Army();
// włączenie do niej paru jednostek
$main_army->addUnit(new Archer());
$main_army->addUnit(new LaserCannonUnit() );
// utworzenie nowej armii
$sub_army = new Army();
// zaciąg do nowej armii
$sub_army->addUnit(new Archer());
$sub_army->addUnit(new Archer());
$sub_army->addUnit(new Archer());
// wcielenie drugiej armii do pierwszej
$main_army->addUnit($sub_army);
// obliczenia siły ataku wykonywane automatycznie w tle
print "Atak z siłą: {$main_army->bombardStrength()}\n";

Do utworzonego oddziału głównego dodajemy kilka jednostek podstawowych. Proces ten powtarzamy dla drugiego utworzonego oddziału, który następnie wcielamy do pierwszego. Przy obliczaniu siły rażenia (Unit::bombardStrength()) wynikowego oddziału złożoność struktury hierarchii obiektów jest dla wywołującego zupełnie niewidoczna. Konsekwencje Jeśli Czytelnik myśli podobnie jak ja, powinien na widok kodu klasy Archer nabrać podejrzeń. Po co bowiem do klas końcówek włączamy metody addUnit() i removeUnit(), jeśli nie ma potrzeby obsługiwania operacji wcielania i usuwania jednostek? Odpowiedź tkwi w przezroczystości typu Unit. Jeśli użytkownik otrzymuje obiekt typu Unit, ma pewność, że obiekt ten implementuje metody addUnit() i removeUnit(). Uwidacznia się tu przyjęta we wzorcu Composite zasada, że klasy obiektów niepodzielnych (liści) mają interfejs identyczny z klasami kompozytów. Taka odpowiedź jest jednak mało satysfakcjonująca, ponieważ honorowanie interfejsu nie oznacza w tym przypadku bezpieczeństwa wywołania metod addUnit() czy removeUnit() na rzecz każdego z obiektów hierarchii Unit. Gdybyśmy owe metody przesunęli tak, aby były dostępne jedynie dla klas kompozytów, wtedy z kolei powstałby problem niepewności co do tego, czy otrzymany obiekt hierarchii Unit obsługuje czy nie obsługuje daną metodę. Mimo wszystko pozostawienie metod-pułapek w klasach liści to dla mnie sytuacja mało komfortowa. Nie ma tu wartości dodanej, a jedynie zamieszanie w projekcie systemu, ponieważ interfejs w zasadzie okłamuje użytkowników co do swojej własnej funkcjonalności. Moglibyśmy w prosty sposób wyeliminować tę niedogodność, wydzielając dla kompozytów ich własny podtyp CompositeUnit. Polegałoby to przede wszystkim na usunięciu metod addUnit() i removeUnit() z klasy Unit:

<?php
abstract class Unit
{
    function getComposite()
    {
        return null;
    }
    abstract function bombardStrength();
}

Zwróćmy uwagę na metodę getComposite(). Wrócimy do niej za moment. Teraz potrzebujemy abstrakcyjnej klasy definiującej metody usunięte z klasy Unit. Możemy w niej nawet przewidzieć ich implementacje domyślne:

abstract class CompositeUnit extends Unit
{
    private $units = array();
    function getComposite()
    {
        return $this;
    }
    protected function units()
    {
        return $this->units;
    }
    function removeUnit(Unit $unit)
    {
        $this->units = array_udiff($this->units, array(
            $unit
        ) , function ($a, $b)
        {
            return ($a === $b) ? 0 : 1;
        });
        function addUnit(Unit $unit)
        {
            if (in_array($unit, $this->units, true))
            {
                return;
            }
            $this->units[] = $unit;
        }
    }
}

Klasa CompositeUnit (kompozyt jednostek) choć sama w sobie nie zawiera żadnych metod abstrakcyjnych, jest deklarowana jako abstrakcyjna. Równocześnie rozszerza klasę Unit, nie definiując jej abstrakcyjnej metody bombardStrength(). Klasa Army (i wszystkie inne klasy kompozytów) może teraz rozszerzać klasę CompositeUnit. Organizację klas po tej modyfikacji ilustruje rysunek 10.2

 

Wyeliminowaliśmy irytujące i bezużyteczne implementacje metod dodawania i usuwania jednostek z klas liści, ale teraz klient musi przed wywołaniem tych metod sprawdzać, czy obiekt, na rzecz którego chce zainicjować wywołanie, jest obiektem klasy CompositeUnit. Tutaj do akcji wkracza metoda getComposite(). Domyślnie zwraca ona bowiem wartość pustą. Jedynie w klasach dziedziczących po CompositeUnit wartość zwracana to obiekt klasy CompositeUnit. Jeśli więc wywołanie tej metody zwróci obiekt, można na jego rzecz wywołać metodę addUnit(). Oto zastosowanie tej techniki z punktu widzenia użytkownika: 

<?php
class UnitScript
{
    static function joinExisting(Unit $newUnit, Unit $occupyingUnit)
    {
        $comp;
        if (!isnull($comp = $occupyingUnit->getComposite()))
        {
            $comp->addUnit($newUnit);
        }
        else
        {
            $comp = new Army();
            $comp->addUnit($occupyingUnit);
            $comp->addUnit($newUnit);
        }
        return $comp;
    }
}

Metoda joinExisting() (połącz siły) przyjmuje dwa obiekty hierarchii Unit. Pierwszy z nich reprezentuje jednostkę nowo przybyłą na dane pole, drugi — jednostkę już na tym polu przebywającą (okupującą pole planszy). Jeśli druga z tych jednostek jest kompozytem (obiektem klasy CompositeUnit), wtedy pierwszy z obiektów jest do niej dodawany. W innym przypadku tworzony jest nowy obiekt klasy Army, do którego wcielane są obie jednostki. Określanie przynależności do hierarchii klas kompozytowych odbywa się za pośrednictwem metody getComposite(). Jeśli zwróci ona obiekt, możemy wprost do niego dodawać nowe obiekty klasy Unit. Jeśli wynikiem wywołania getComposite() będzie wartość pusta, musimy utworzyć obiekt kompozytu na własną rękę, tworząc egzemplarz klasy Army i wcielając do niego obie jednostki. Model można uprościć jeszcze bardziej, wymuszając w metodzie Unit::getComposite() zwrócenie obiektu Army wypełnionego początkowo bieżącą jednostką Unit. Moglibyśmy też wrócić do poprzedniego modelu (w którym nie rozróżnialiśmy pomiędzy obiektami kompozytów a liśćmi) i zrealizować to samo w metodzie Unit::addUnit(): możemy tam utworzyć obiekt Army i dodać do niego oba obiekty Unit. To eleganckie rozwiązanie,Metoda joinExisting() (połącz siły) przyjmuje dwa obiekty hierarchii Unit. Pierwszy z nich reprezentuje jednostkę nowo przybyłą na dane pole, drugi — jednostkę już na tym polu przebywającą (okupującą pole planszy). Jeśli druga z tych jednostek jest kompozytem (obiektem klasy CompositeUnit), wtedy pierwszy z obiektów jest do niej dodawany. W innym przypadku tworzony jest nowy obiekt klasy Army, do którego wcielane są obie jednostki. Określanie przynależności do hierarchii klas kompozytowych odbywa się za pośrednictwem metody getComposite(). Jeśli zwróci ona obiekt, możemy wprost do niego dodawać nowe obiekty klasy Unit. Jeśli wynikiem wywołania getComposite() będzie wartość pusta, musimy utworzyć obiekt kompozytu na własną rękę, tworząc egzemplarz klasy Army i wcielając do niego obie jednostki. Model można uprościć jeszcze bardziej, wymuszając w metodzie Unit::getComposite() zwrócenie obiektu Army wypełnionego początkowo bieżącą jednostką Unit. Moglibyśmy też wrócić do poprzedniego modelu (w którym nie rozróżnialiśmy pomiędzy obiektami kompozytów a liśćmi) i zrealizować to samo w metodzie Unit::addUnit(): możemy tam utworzyć obiekt Army i dodać do niego oba obiekty Unit. To eleganckie rozwiązanie,

<?php
class TroopCarrier
{
    function addUnit(Unit $unit)
    {
        if ($unit instanceof Cavalry)
        {
            throw new UnitException("Transporter nie może przewozić koni");
        }
        parent::addUnit($unit);
    }
    function bombardStrength()
    {
        return 0;
    }
}

Jesteśmy tu zmuszeni do testowania typu obiektu przekazanego w wywołaniu metody addUnit() za pośrednictwem operatora instanceof. Im więcej takich jak ten przypadków specjalnych, tym wady wzorca będą dokuczliwsze. Wzorzec Composite działa najlepiej wtedy, kiedy większość komponentów to obiekty wymienialne, o zbliżonej semantyce. Kolejną kwestią jest koszt niektórych operacji w ramach wzorca. Typowym przykładem jest wywołanie Army::bombardStrength(), prowokujące kaskadę wywołań propagowanych w dół drzewa struktury jednostek zawieranych w oddziale. Przy mocno rozbudowanych drzewach z wieloma pododdziałami owo jedno wywołanie może sprowokować „w tle” istną lawinę wywołań. Co prawda koszt wykonania metody bombardStrength() nie jest obecnie wysoki, łatwo jednak sobie wyobrazić efekty skomplikowania obliczania siły ataku niektórych jednostek. Jednym ze sposobów eliminacji nawału wywołań i delegowania jest buforowanie wyników poprzednich wywołań metod obiektów zawieranych w obiektach-kompozytach, tak aby w przyszłych odwołaniach do tej wartości można było pominąć narzut wywołań. Ale wtedy trzeba pilnować aktualizacji buforowanych wartości, wdrażając strategię opróżniania buforów po operacjach na drzewie obiektów. Może to wymagać wyposażenia obiektów zawieranych w referencje do obiektów kompozytów. Wreszcie słowo o trwałości. Wzorzec Composite jest co prawda wyjątkowo elegancki, ale nie bardzo nadaje się do utrwalania zbudowanej struktury obiektów w bazie danych, a to dlatego, że całe struktury traktowane są jako pojedyncze obiekty. Aby więc skonstruować taką strukturę na podstawie informacji odczytywanych z bazy danych, trzeba posłużyć się serią kosztownych zapytań. Problem można wyeliminować, przypisując do całego drzewa identyfikator, tak aby można było jednym zapytaniem wyodrębnić z bazy danych wszystkie komponenty drzewa. Po wyodrębnieniu wszystkich obiektów trzeba będzie jednak i tak odtworzyć budowę drzewa, z zachowaniem układu obiektów podrzędnych i nadrzędnych, który również trzeba odzwierciedlić w schemacie bazy danych. Nie jest to zadanie bardzo trudne, ale mimo wszystko nieco skomplikowane. Przystosowanie wzorca Composite do baz danych jest wątpliwe, zupełnie inaczej ma się sprawa z językiem XML, a to dlatego, że w XML-u bardzo łatwo tworzyć drzewiaste struktury elementów

Komentarze wyłączone