Decorator (Dekorator)

decorator uml

Opis

Wzorzec projektowy Decorator (Dekorator) należy do grupy wzorców strukturalnych. Umożliwia rozszerzenie obiektu o nowe zachowania opakowując go specjalną klasą. Każdy dekorator zawiera konkretną funkcjonalność, która może być aplikowana do innego obiektu dynamicznie i najlepiej niezależnie.

Problem i rozwiązanie

Wszędzie tam, gdzie komponować da się wszystko z wszystkim, a możliwych konfiguracji jest dużo, świetnie sprawdzi się wzorzec dekoratora. Gdyby oprzeć się na dziedziczeniu, niestety hierarchia klas okaże się niesamowicie rozbudowana i w wielu przypadkach skomplikowana. Czym więcej opcji tym większa złożoność. Poza tym dziedziczenie nie rozwiązuje problemu, bo dekorator potrafi dokładać zachowania dynamicznie w locie.

Za przykład posłuży udział w konferencji programistycznej. Skupię się na konkretnym evencie, chociaż warto mieć na uwadze, że mając system do ich organizowania, każde wydarzenie może zawierać inne świadczenia do wyboru. To dodatkowa komplikacja, a zaznaczam to tylko dlatego, że dekorator nie będzie miał z tym problemu.

Na konferencję można kupić bilet. Dodatkowo do zakupienia są też: maskotka słonik i pakiet naklejek web development. Jako, że wydarzenie jest dwudniowe to dla przyjezdnych w ofercie są: nocleg i pakiet posiłków. Przy tak niewielkiej liczbie świadczeń robi się spora hierarchia klas, jeżeli miałaby powstać dla każdej kombinacji. Poza tym, takie zamówienie może być rozłożone w czasie. Nic nie stoi na przeszkodzie, żeby dokupić nocleg trochę później. Gotowa klasa, nie jest więc rozwiązaniem.

Całkiem fajny przypadek dla decorator pattern. Na pierwszy rzut oka wydawać się może, że rozwiązuje bardzo podobny problem jak wzorzec Builder. Założeniem budowniczego jest jednak skonstruować i dostarczyć obiekt gotowy do użytku – w pełni skonfigurowany. Wzorzec projektowy, któremu poświęcony został ten wpis, jak sama nazwa wskazuje, dekoruje istniejący już obiekt o nowe zachowania. Implementacja wspomnianego przykładu w dalszej części wpisu.

Plusy i minusy

Dużą wadą dekoratora jest wymaganie ścisłej kolejności dekoracji w procesie. Przyznam, że według mnie jeśli tak wygląda implementacja to wzorzec traci całą swoją esencję. Taka sytuacja pachnie mi bardziej polem do popisu dla innych wzorców albo nawet zwykłego serwisu. Jasne, nie jest kłopotem zaszyć stałą kolejność, ale później przy rozwoju czy modyfikacji klas prawdopodobnie wystąpi problem. Dla mnie dekoratory powinny być tak niezależne jak tylko się da, dlatego tylko w takich sytuacjach staram się ich używać.

Kolejny minus to nieczytelny wynik operacji. Dekorator dodaje zachowania często bez modyfikacji oryginalnego obiektu. Czasem okaże się, że nie wiadomo skąd taki rezultat operacji albo co dzieje się pod spodem, ponieważ logika wynikająca z dekoracji w zasadzie jest ukryta.

Duża liczba klas, które w dodatku wyglądają podobnie – minus, który często okaże się zaletą. Małe klasy równa się jedna odpowiedzialność (single responsibility). Proces dynamicznego dokładania czy odejmowania zachowań zapewnia dużą elastyczność i łatwą rozszerzalność (open-closed). Dynamiczna kompozycja zamiast statycznego dziedziczenia.

Przykładowa implementacja w PHP

Jak zwykle poniżej szkielet wzorca projektowego. Może się okazać, że dekorator przyda się w cięższych logicznie przypadkach. Wtedy często niestety trudno o niezależność dekoracji i dowolną kolejność ich wykonywania.

Dekoracja kojarzy się z wyglądem stąd też normalne, że będzie przychodzić na myśl w miejscach, gdzie zmiana dotyczy tego typu cech obiektu. Zaznaczam jednak, że jest to uzupełnienie o zachowanie, więc również operacje logiczne jak chociażby wyliczenie ceny. A i to nie jest idealny przykład, bo esencją tego wzorca jest dodanie kolejnego zachowania.

W przykładzie widać jedną wadę o której wspomniałem. Konkretne dekoratory wyglądają niemal identycznie, a kod wygląda jakby był zduplikowany. Oczywiście metodę do wyliczania ceny można wynieść powyżej do klasy abstrakcyjnej. Często będą one jednak nieznacznie się różniły przez co nie będzie to możliwe. Mimo wszystko i tak będą wyglądały bliźniaczo.

<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Decorator;

interface OptionInterface
{
    public function calculatePrice(): float;
}

<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Decorator;

final class Ticket implements OptionInterface
{
    public function calculatePrice(): float
    {
        return 100.00;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Decorator;

abstract class AbstractOptionDecorator implements OptionInterface
{
    public function __construct(
        protected OptionInterface $option
    ) {}
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Decorator;

final class Accommodation extends AbstractOptionDecorator
{
    public const float PRICE = 180.00;

    public function calculatePrice(): float
    {
        return $this->option->calculatePrice() + self::PRICE;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Decorator;

final class Catering extends AbstractOptionDecorator
{
    public const float PRICE = 45.00;

    public function calculatePrice(): float
    {
        return $this->option->calculatePrice() + self::PRICE;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Decorator;

final class Mascot extends AbstractOptionDecorator
{
    public const float PRICE = 30.00;

    public function calculatePrice(): float
    {
        return $this->option->calculatePrice() + self::PRICE;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Decorator;

final class Sticker extends AbstractOptionDecorator
{
    public const float PRICE = 12.00;

    public function calculatePrice(): float
    {
        return $this->option->calculatePrice() + self::PRICE;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Structural\Decorator\Test;

use DesignPatterns\Structural\Decorator\Accommodation;
use DesignPatterns\Structural\Decorator\Catering;
use DesignPatterns\Structural\Decorator\Mascot;
use DesignPatterns\Structural\Decorator\Sticker;
use DesignPatterns\Structural\Decorator\Ticket;
use PHPUnit\Framework\TestCase;

final class OptionDecoratorTest extends TestCase
{
    public function testCanDecorateWithMascot(): void
    {
        $ticket = new Ticket();
        $option = new Mascot($ticket);

        self::assertSame($ticket->calculatePrice() + Mascot::PRICE, $option->calculatePrice());
    }

    public function testCanDecorateWithAccommodation(): void
    {
        $ticket = new Ticket();
        $option = new Accommodation($ticket);

        self::assertSame($ticket->calculatePrice() + Accommodation::PRICE, $option->calculatePrice());
    }

    public function testCanDecorateWithSticker(): void
    {
        $ticket = new Ticket();
        $option = new Sticker($ticket);

        self::assertSame($ticket->calculatePrice() + Sticker::PRICE, $option->calculatePrice());
    }

    public function testCanDecorateWithCatering(): void
    {
        $ticket = new Ticket();
        $option = new Catering($ticket);

        self::assertSame($ticket->calculatePrice() + Catering::PRICE, $option->calculatePrice());
    }

    public function testCanDecorateFullOption(): void
    {
        $ticket = new Ticket();
        $option = new Mascot($ticket);
        $option = new Accommodation($option);
        $option = new Sticker($option);
        $option = new Catering($option);
        $fullPrice = $ticket->calculatePrice()
            + Mascot::PRICE
            + Accommodation::PRICE
            + Sticker::PRICE + Catering::PRICE;

        self::assertSame($fullPrice, $option->calculatePrice());
    }
}

Decorator – podsumowanie

Wzorzec projektowy Dekorator w pełni wspiera obiektowość. Może nieznacznie różnić się w implementacji w zależności od przypadku, ale idea jest jedna – bezinwazyjnie i dynamicznie dołożyć lub odjąć zachowanie istniejącego obiektu.

Problem jaki widzę w tym wzorcu to jego błędne rozumienie. Przyznam się, sam długo w tym tkwiłem przeglądając przykłady o konstruowaniu pizzy. Zresztą muszę przyznać, że mój przykład też nie jest idealny. Jego moc byłoby widać dopiero, kiedy różnice w zachowaniach wynikające z różnych konfiguracji byłyby większe. Niestety stałby się dużo mniej czytelny stąd też zdecydowałem się na uproszczenie.

Może to prowadzić do wniosku, że na początku nie widać korzyści z użycia dekoratora. Skąd więc wiedzieć, kiedy po niego sięgnąć. Będąc szczerym, zmieniłem zdanie co do tego wzorca. Naprawdę, według mnie, mało jest miejsc gdzie warto z niego skorzystać. Są lepsze rozwiązania tej kategorii problemów. Nie twierdzę, żeby w ogóle go nie używać. Nie pakujcie go jednak wszędzie, bo taki kod stanie się trudniejszy w debugowaniu i utrzymaniu.

Programista PHP i właściciel marki Koddlo. Pasjonat czystego kodu i dobrych praktyk programowania obiektowego. Prywatnie fan dobrego humoru i podcastów.