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)
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ć.
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.
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.
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ł