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ć można 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ść.

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 dla każdej kombinacji miałaby taka powstać.

Idealne miejsce 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 powyżej 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 jakiegoś serwisu. Jasne nie jest problemem 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ć.

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ść (SRP). Proces dynamicznego dokładania czy odejmowania zachowań zapewnia dużą elastyczność i łatwą rozszerzalność (OCP). 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 naturalnie 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 jakieś zachowanie, więc również operacje logiczne jak wyliczenie ceny.

W poniższym 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, a mimo wszystko wyglądały i tak 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 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 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 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 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ść i ma zdecydowanie więcej zalet, niż wad. Może nieznacznie różnić się w implementacji w zależności od przypadku, ale idea jest jedna – bezinwazyjnie i dynamicznie dołożyć albo odjąć zachowanie do istniejącego obiektu.

Problem jaki ja widzę w tym wzorcu to, że na początku nie widać jego korzyści. Dla niedużych funkcjonalności może wydawać się to armata na muchę. Nic bardziej mylnego, sam dostrzegam wiele miejsc w projektach, gdzie wiele balastu z obsługi niektórych mechanizmów udało by się zdjąć dekoratorami. Dopiero przy wzroście i rozwoju projektu można sobie podziękować za użycie tego patternu. Rzecz jasna – nie pchajcie go w każde miejsce, gdzie się da.

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.