[Wzorce projektowe] Wzorzec projektowy Dekorator

Cześć. Dziś post bardziej techniczny. No i pierwszy z serii, nieregularnej zresztą. Będę opisywał wzorce projektowe oraz ich przykładowe zastosowanie w praktyce. Na pierwszy ogień idzie wzorzec projektowy dekorator bądź z angielska decorator. Nie przedłużając dłużej, zaczynamy!

Jest to pierwszy post z serii, więc zacznę od podstawowego pytania:

Co to jest wzorzec projektowy?

Wyobraź sobie, że wiele problemów, które napotykasz tworząc swoje własne oprogramowanie, jest powtarzalna, spotyka nie tylko ciebie, ale też została już rozwiązana i ładnie opisana. Tym właśnie jest wzorzec projektowy – praktycznie gotowym rozwiązaniem na twój problem. Warto zaznaczyć, że wzorzec jest opisem, implementacja należy do ciebie jako programisty. Wzorców projektowych mamy kilka rodzajów, a taki podstawowy podział to:

  • Wzorce kreacyjne – takie, które opisują tworzenie obiektów
  • Strukturalne – takie, które opisują struktury obiektów
  • Behawioralne – takie, które opisują zachowanie obiektów i relacje między sobą

Wzorzec projektowy dekorator – teoria

Dekorator jest strukturalnym wzorcem i chyba jednym z najprostszych. Co rozwiązuje ten wzorzec? Wyobraź sobie klasę, której brakuje jakiejś funkcjonalności. Za pomocą tego wzorca możemy rozszerzyć funkcjonalność klasy o jakąś nową funkcjonalność.  Łatwo zapamiętać – dekorujemy klasę nową funkcjonalnością. Wzorzec projektowy dekorator jest opakowaniem na istniejącą już klasę, a co więcej, może być opakowaniem na opakowanie!

Wzorzec projektowy dekorator – praktyka

Przygotowanie

Wiemy w teorii, do czego służy wzorzec projektowy dekorator i być może świeci ci się już lampka jak by to można zastosować. Jeśli jeszcze nie wiesz, to już za chwilę się dowiesz. Zacznijmy od sytuacji – mamy serwis, który wypluwa (ok, renderuje) nam stringa jako dane. Chcielibyśmy móc renderować je np do JSON’a albo XML’a bądź jakiegokolwiek innego. Zacznijmy od interfejsu dającego metodę renderowania:

<?php

interface RendereableInterface
{
    public function render(): string;
}

Nic nadzwyczajnego, ale przyda nam się potem.

Dobra, teraz stwórzmy klasę konkretnego serwisu:

<?php
class SomeService implements RenderableInterface
{
    protected $data;
    
    public function render(): string
    {
        return $this->data;
    }
}

Jak widzimy klasa jest banalnie prosta, pominąłem już jakiekolwiek konstruktory itp, bo nie o to chodzi. Mamy chronione pole (może być prywatne) $data, które jest stringiem zwracanym metodą getData();

Wzorzec strukturalny dekorator w akcji

Naszym dekoratorem będzie klasa abstrakcyjna, bo tak wygodnie. Oczywiście implementacja tego wzorca może być różna, ja wybrałem taką, potem pokażę jeszcze inną. W każdym razie tworzymy klasę abstrakcyjną, która będzie implementowała ten sam interfejs co nasz serwis:

<?php
abstract class RendererDecorator implements RenderableInterface
{
    protected $service;

    public function __construct(RenderableInterface $service)
    {
        $this->service = $service;
    }

    public function render(): string;
}

Okej, co tu się dzieje? Mamy abstrakcyjną klasę, dzięki której nie będziemy duplikować kodu, gdy będziemy chcieli stworzyć więcej dekoratorów. W konstruktorze przyjmuje obiekt implementujący wcześniej stworzony przez nas Interfejs.

Teraz stwórzmy już konkretny dekorator:

<?php
class JsonRenderer extends RendererDecorator
{
    public function render(): string
    {
        return json_encode($this->service->getData());
    }   
}

Myślę, że tu nie ma wiele do tłumaczenia. Konkretny dekorator jest rozszerzeniem klasy RendererDecorator i na swój sposób renderuje dane, w tym wypadku jest to JSON. Zauważ, że wszędzie jest ta sama metoda – render. A wszystkie klasy implementują RenderableInterface. To bardzo ważne, bo:

<?php

$service = new SomeService();
$service = new JsonRenderer($service);

$service->render();

Co by nie było pod zmienną $service, mamy metodę render. Końcowego użytkownika będzie właśnie to obchodziło.

Podsumowanie

Jak pewnie zauważyłeś, ten wzorzec projektowy jest bardzo prosty i nie powinien przysporzyć problemu, jego implementacja może być naprawdę różna, a to powyżej jest tylko jeden z przykładów. Pomoże nam rozbudować istniejące klasy w sposób dynamiczny.

Znałeś ten wzorzec wcześniej? Kiedy go stosujesz? Podziel się w komentarzu!