Wzorce projektowe w PHP: Decorator (Dekorator)

Autor: Bartosz Maciaszek
Data publikacji: 02.11.2007, 11:57 | Ostatnia modyfikacja: 02.11.2007, 11:57

Drugi z serii artykuł omawiający implementację bardzo ciekawego wzorca projektowego w PHP - Dekoratora (ang. Decorator)



Teoria

Nazwy wzorców projektowych nie biorÄ… siÄ™ z nieba. TeoriÄ™ tÄ™ potwierdza wzorzec dekorator - jak sama nazwa wskazuje sÅ‚uży do dekorowania obiektów, a także innych, nawet elementarnych typów danych. Dekorator w konstruktorze otrzymuje obiekt, który ma udekorować i, co najważniejsze, ze wspólnym interfejsem. DziÄ™ki temu obiekt może być dekorowany kilkukrotnie bez zmiany typu bazowego. Przyjrzymy siÄ™ kodowi, aby Å‚atwiej byÅ‚o zrozumieć to, co chciaÅ‚bym zaprezentować.

Do góry

Abstrakcyjny przykład

Wczujmy siÄ™ w nastrój i omówmy najprostrzy przykÅ‚ad. ZaÅ‚óżmy, że mamy do dyspozycji trzy dekoratory, które w specyficzny sposób zmieniajÄ… obiekt wejÅ›ciowy, który jest najprostrzym stringiem, a raczej obiektem klasy UndecoratedString która posiada jedna skÅ‚adowÄ… typu string i implementuje taki oto interfejs:

interface String
{
    public function getString();
}
 
class UndecoratedString implements String
{
    public function __construct($string)
    {
        $this->string = $string;
    }
 
    public function getString()
    {
        return $this->string;
    }
}

KlasÄ… bazowÄ… naszych dekoratorów bÄ™dzie taka oto klasa:

abstract class StringDecorator implements String
{
    protected $decorate = null;
 
    public function __construct(String $string)
    {
        $this->decorate = $string;
    }
 
    public function getString()
    {
        return $this->decorate->getString();
    }
}

Jako argument konstruktora przyjmuje ona obiekt klasy implementującej interfejs String, czyli na przykład naszej UndecoratedString.

Oto nasze trzy dekoratory, wszystkie dziedziczą ze StringDecorator i przysłaniają metodę 'getString' zmieniając jej zachowanie. Przeanalizuj ich kod. Zauważ, że przysłaniając wywołują metodę getString() z klasy przekazanej w konstruktorze. Oczywiście zamiast $this->decorate->getString() moglibyśmy użyć konstrukcji parent::getString(). Efekt byłby identyczny.

class BoldDecorator extends StringDecorator
{
    public function getString()
    {
        return '<b>' . $this->decorate->getString() . '</b>';
    }
}
 
class ItalicDecorator extends StringDecorator
{
    public function getString()
    {
        return '<i>' . $this->decorate->getString() . '</i>';
    }
}
 
class UnderlineDecorator extends StringDecorator
{
    public function getString()
    {
        return '<u>' . $this->decorate->getString() . '</u>';
    }
}

Przejdźmy do sedna, czyli do końcowego kodu wywołującego wszystkie dekoratory.

Stwórzmy obiekt klasy UndecoratedString:

$string = new UndecoratedString('ala ma kota');

NastÄ™pnie dekorujemy go w taki sposób:

$string = new BoldDecorator($string);
$string = new ItalicDecorator($string);
$string = new UnderlineDecorator($string);
 
echo $string->getString();

Wynikiem działania tego kodu będzie:

<u><i><b>ala ma kota</b></i></u>

Co zyskaliÅ›my? Uniwersalny i bardzo przeźroczysty mechanizm do zmieniania zachowania lub wyglÄ…du obiektów, który jest zupeÅ‚nie odseparowany od modelu biznesowego. Każdy dekorator ma okreÅ›lony zakres zmian, które wykonuje i może być z powodzeniem wywoÅ‚ywany bez interferencji z innymi dekoratorami. Dużą zaletÄ… jest to, że w każdym przypadku możemy skorzystać z dowolnej iloÅ›ci dekoratorów, a dziÄ™ki temu, że abstrakcyjny dekorator implementuje ten sam interfejs, co obiekt bazowy (UndecoratedString) nie obserwujemy zmian klas - w każdym miejscu '$string instanceof String' zwróci true.

Do góry

Zastosowanie przy tworzeniu aplikacji WWW

W jaki sposób ten wzorzec projektowy możemy wykorzystać w praktyce? Dobrym przykÅ‚adem bÄ™dÄ… wszelkiego rodzaju listingi. ZaÅ‚óżmy, że wykonaliÅ›my zapytanie do bazy danych, które zwróciÅ‚o nam takÄ… oto strukturÄ™:

$rows = array
(
    array
    (
        'userId'=> 1,
        'login' => 'mike',
        'email' => 'mike@foobar.com',
        'deletable' => false
    ),
 
    array
    (
        'userId' => 2,
        'login' => 'frank',
        'email' => 'frank@foo.com',
        'deletable' => true
    ),
 
    array
    (
        'userId' => 5,
        'login' => 'lucas',
        'email' => 'lucas@bar.com',
        'deletable' => true
    )
);

Na podstawie tych danych mamy wyÅ›wietlić tabelÄ™ użytkowników skÅ‚adajÄ…cÄ… siÄ™ z trzech kolumn. W pierwszej pojawić siÄ™ ma login użytkownika podlinkowany do jego szczegóÅ‚ów, w drugiej jego email w postaci linka 'mailto:', a w trzeciej link 'usuÅ„', o ile flaga 'deletable' jest równa true. Na poczÄ…tek musimy okreÅ›lić wspólny interfejs dla niesformatowanego obiektu i dekoratorów:

interface Listing
{
    public function getRow($pos);
    public function getRows();
}

Zaprojektujmy teraz klasÄ™, która "przekonwertuje" naszÄ… strukturÄ™ na coÅ› zrozumiaÅ‚ego dla dekoratorów. Na poczet tego tworzymy klasÄ™ implementujÄ…cÄ… powyższy interfejs:

class UnformattedListing implements Listing
{
    private $rows = array();
 
    public function __construct(array $rows)
    {
        foreach($rows as $row)
        {
            // tworzymy obiekt klasy stdClass z tablicy
            $this->rows[] = (object) $row;
        }
    }
 
    public function getRow($pos)
    {
        return $this->rows[$pos];
    }
 
    public function getRows()
    {
        return $this->rows;
    }
}

Jako argument konstruktora dostaje ona dane z bazy, po czym tworzy z kolejnych iteracji obiekty klasy stdClass i umieszcza na stosie. Implementuje oczywiÅ›cie dwie metody z interfejsu: getRows i getRow, za pomocÄ… których zwraca stworzonÄ… w konstruktorze tablicÄ™ lub jej okreÅ›lony element.

MajÄ…c dane opakowane w pożądany przez nas interfejs możemy zaplanować abstrakcyjny dekorator, który bÄ™dzie klasÄ… bazowÄ… do wszystkich dekoratorów:

abstract class ListingDecorator implements Listing
{
    protected $decorate = null;
 
    public function __construct(Listing $listing)
    {
        $this->decorate = $listing;
    }
 
    public function getRow($pos)
    {
        return $this->decorate->getRow($pos);
    }
 
    public function getRows()
    {
        $rows = array();
 
        foreach($this->decorate->getRows() as $pos => $row)
        {
            $rows[] = $this->getRow($pos);
        }
 
        return $rows;
    }
}

W klasie tej znajdziemy podstawowe wywołania metod 'getRows' i 'getRow'. Będziemy je przysłaniać w razie potrzeby w końcowych dekoratorach.

Zdefiniujmy teraz nasze dekoratory: AddDeleteLinkDecorator, który w nowym polu o nazwie 'deteleLink' umieÅ›ci link do usuniÄ™cia użytkownika jeżeli pole 'deletable' bÄ™dzie true, AddEmailLinkDecorator podlinkowujÄ…cy adres email i AddUserDetailsLinkDecorator dodajÄ…cy link do szczegóÅ‚ów. Przeanalizuj ich kod.

class AddDeleteLinkDecorator extends ListingDecorator
{
    public function getRow($pos)
    {
        $row = $this->decorate->getRow($pos);
 
        $row->deleteLink = $row->deletable ? '<a href="delete.php?userId=' . $row->userId . '">Usuń</a>' : '';
 
        return $row;
    }
}
class AddEmailLinkDecorator extends ListingDecorator
{
    public function getRow($pos)
    {
        $row = $this->decorate->getRow($pos);
 
        $row->email = '<a href="mailto:' . $row->email . '">' . $row->email . '</a>';
 
        return $row;
    }
}
class AddUserDetailsLinkDecorator extends ListingDecorator
{
    public function getRow($pos)
    {
        $row = $this->decorate->getRow($pos);
 
        $row->login = '<a href="details.php?id=' . $row->userId . '">' . $row->login . '</a>';
 
        return $row;
    }
}

Jeżeli wszystko jest jasne przejdźmy do sedna sprawy, czyli do wywoÅ‚ania dekoratorów:

$listing = new UnformattedListing($rows);
$listing = new AddDeleteLinkDecorator($listing);
$listing = new AddUserDetailsLinkDecorator($listing);
$listing = new AddEmailLinkDecorator($listing);
 
print_r($listing->getRows());

W wyniku dostajemy piÄ™knie udekorowane szczegóÅ‚y, gotowe abyÅ› wsadziÅ‚ je do szablonu HTML. Spróbuj pobawić siÄ™ usuwajÄ…c wywoÅ‚ania poszczególnych dekoratorów.

Co zyskujemy implementujÄ…c wzorzec projektowy Decorator w naszych aplikacjach? Odpowiedź na to pytanie jest prosta: uniwersalność kodu, podziaÅ‚ obowiÄ…zków pomiÄ™dzy odpowiednimi częściami aplikacji, a w koÅ„cu możliwość użycia różnych dekoratorów do struktur różnego typu, o ile ich implementacja bÄ™dzie wystarczajÄ…co abstrakcyjna. KoÅ„czÄ…c ten artykuÅ‚ mam nadziejÄ™, że zainspirowaÅ‚em CiÄ™ do korzystania z rozwiÄ…zaÅ„ tego typu.

Do góry

Waszym zdaniem:

guci0 :: 05.11.2007, 10:26 :: #95

Bardzo ciekawe,
czekam na więcej tego typu artykułów.
Myślę, że warto by również podać więcej użytecznych przykładów.

Pozdrawiam serdecznie

Paweł

guci0 :: 05.11.2007, 10:27 :: #96

Bardzo ciekawe,
czekam na więcej tego typu artykułów.
Myślę, że warto by również podać więcej użytecznych przykładów.

Pozdrawiam serdecznie

Paweł

Twoim zdaniem:

Reklama

banner

Partnerzy

CityDesign.pl
phpSolutions