Dekorator wzorzec projektowy na kazda boaczke

Transkrypt

Dekorator wzorzec projektowy na kazda boaczke
Techniki
Dekorator: wzorzec projektowy
na każdą bolączkę
Stopień trudności: lll
Paweł Kozłowski
Nazwa wzorca projektowego dekorator jest
nieco myląca, ponieważ sugeruje, że będziemy
coś wzbogacać, dekorować czy upiększać. Nic
bardziej błędnego! Omawiany wzorzec znajduje
szerokie zastosowanie, niezależnie od tego, czy
projektujemy warstwę dostępu do bazy danych,
logikę biznesową lub kontroler MVC.
J
W SIECI
• http://flexi.sourceforge.net/
– budowany przez nas framework
• http://www.picocontainer.org/
Ports – Pico
• http://www.martinfowler.com/
articles/injection.html – ciekawe artykuły
• http://www.phppatterns.com/
– ciekawe artykuły
2
ednym z najczęściej omawianych i
używanych wzorców projektowych
jest dekorator. Z punktu widzenia
programisty ma same zalety: jest bardzo
użyteczny, nieskomplikowany w implementacji i łatwy do przyswojenia. Obok
singletona jest to prawdopodobnie jeden
z pierwszych wzorców, z którymi stykamy
się, gdy zaczynamy studiować przykłady
i literaturę poświęconą zasadom projektowania. O ile jednak singleton jest obwiniany (często słusznie!) o promowanie fatalnych rozwiązań, to dokładne poznanie dekoratora przyczyniło się do powstania wielu ciekawych rozwiązań architektonicznych.
Nazwa wzorca projektowego "dekorator" jest nieco myląca, ponieważ sugeruje, że będziemy coś wzbogacać, dekorować czy upiększać. Stąd może powstać błędne przeświadczenie, że mamy do czynienia z tworem przydatnym
jedynie w warstwie prezentacji. Nic bardziej błędnego! Omawiany wzorzec znaj-
www.phpsolmag.org
duje szerokie zastosowanie, niezależnie
od tego, czy projektujemy warstwę dostępu do bazy danych, logikę biznesową lub kontroler MVC. Jego istotą jest
możliwość wzbogacenia funkcjonalności
obiektów danych klas w sposób dynamiczny, bez konieczności modyfikowania
oryginalnych obiektów.
Definicje jak zwykle brzmią zbyt sucho, spójrzmy więc na Listing 1, gdzie
Co należy wiedzieć...
Przydatna będzie znajomość dwóch poprzednich artykułów z naszej serii Wzorce projektowe. Czytelnik powinien mieć
wiedzę z obiektowych technik projektowania aplikacji.
Co obiecujemy...
Z artykułu dowiesz się, kiedy i dlaczego
warto stosować wzorzec projektowy dekorator. Na przykładach pokażemy, jak
stosując dekorator, bardzo łatwo i w elegancki sposób dodać nową funkcjonalność do aplikacji bez zbędnej modyfikacji kodu.
PHP Solutions Nr 4/2006
Wzorce projektowe: dekorator
znajduje się elementarny przykład użycia dekoratora. W przedstawionym przypadku użyliśmy dekoratora do wzbogacenia istniejącej funkcjonalności. Nie
musimy jednak zbyt ściśle sugerować
się nazwą wzorca projektowego i spokojnie możemy pozwolić sobie na całkowitą zmianę funkcjonalności dekorowanego obiektu.
Dwa proste przykłady, przedstawione na wspomnianych listingach, obrazują
praktycznie wszystkie istotne cechy wzorca, z którym się zaznajamiamy. Szczególne istotny do odnotowania jest jeden fakt:
dla klienta korzystającego z udekorowanego (często mówimy również - "opakowanego") obiektu obecność dekoratora
jest zupełnie przezroczysta! Klient nie jest
w stanie stwierdzić, czy korzysta z usług
oryginalnego obiektu, czy też z jego udekorowanej wersji. Jest to możliwe dzięki zachowaniu przez dekorator interfejsu
oryginalnego obiektu. Jeśli chcielibyśmy
zapamiętać jakąś jedną zasadę czy zdanie opisujące dekoratory, mogłoby to być:
Dokorator zachowuje interfejs oryginalnego obiektu i najczęściej przyjmuje dekorowany obiekt jako parametr w swoim konstruktorze.
Kolejną bardzo miłą cechą dekoratora
jest możliwość użycia więcej niż jednego
"opakowania" dla podstawowego obiektu. Oznacza to, iż możemy składać nową funkcjonalność z wielu już istniejących
elementów, bez konieczności ingerencji w
raz stworzony kod. Jest to dokładnie ta
sama filozofia, jaką spotykamy przy pracy z powłoką w systemach uniksowych.
Doskonale wiemy, jak wiele pożytecznych
operacji można wykonać, zaprzęgając do
wspólnej pracy elementarne narzędzia. To
samo odnosi się do dekoratorów: sprytnie
łącząc wiele prostych dodatków możemy
uzyskać funkcjonalność, o której nie śniło
się nawet twórcom piszącym klasy czy też
dekoratory (Listing 2).
Zanim przejdziemy do omówienia
bardziej skomplikowanych przykładów,
zatrzymajmy się jeszcze na chwilę nad
własnością dekoratorów, dzięki której
klient nie jest w stanie rozróżnić, czy ma
do czynienia z obiektem podstawowym,
czy też z opakowaną jego wersją. Uważny Czytelnik zauważy, że ta nierozróżnialność jest możliwa, ponieważ pomiędzy dekoratorem i obiektem dekorowanym zachodzi taka sama relacja ("jest",
PHP Solutions Nr 4/2006
ang. IS-A), jak przy dziedziczeniu. Istotnie, przykład z Listingu 2 można by zapisać tylko i wyłącznie przy pomocy tworzenia nowych podklas (Listing 3). Jaki jest więc sens wprowadzania zupełnie nowej konstrukcji programistycznej i
opisywania jej jako wzorca projektowego? Przecież to samo można uzyskać
przy użyciu podstawowych konstrukcji
programowania obiektowego. Jest jedna,
dosyć istotna różnica pomiędzy dwoma
wspomnianymi podejściami. Aby ją łatwo pokazać, przemodelujmy przykład z
Listingu 2, zmieniając kolejność zastosowanych dekoratorów (Listing 4). Proszę
bardzo: cała operacja była wyjątkowo
prosta, odbyła się bez konieczności modyfikowania istniejącego kodu i dopro-
Techniki
wadziła do powstania nowej funkcjonalności. Niestety, w przypadku dziedziczenia mamy dużo trudniejsze zadanie: nie
obędzie się bez dosyć istotnego przemeblowania napisanego kodu. Powodem
jest fakt, iż formując hierarchię dziedziczenia tworzymy dość sztywne, statyczne powiązanie pomiędzy poszczególnymi klasami.
W przypadku dekoratorów nie mamy takiego ograniczenia: możemy dowolnie przestawiać kolejność, dodawać nowe elementy do łańcucha i usuwać już istniejące. Takie zamiany są możliwe nie tylko w momencie ustalania struktury kodu,
ale w dowolnym momencie, nawet w czasie wykonywania skryptu. Dzięki dekoratorom można uzyskać wszystkie dobrodziej-
Listing 1. Prosty przykład użycia dekoratora
<?php
/**
*interfejs, ktory może być implementowany przez wiele klas
*ponieważ dekoratory również implementują ten interfejs
*klient korzystający z udekorawanej klasy nie jest w stanie
*rozróżnić, czy ma do czynienia z pierwotną, czy udekorowaną wersją
*/
interface UserDAO {
public function save(User $user);
public function findByLoginNamePassword($loginName, $password);
}
class UserDAOImpl implements UserDAO {
public function save(User $user) {
// standardowa funkcjonalność zapisywania
// danych użytkowników do DB
}
public function findByLoginNamePassword($loginName, $password){
// standardowa funkcjonalność odnajdywania
// danych użytkowników po nazwie
}
}
class UserDAOUpperCaseLoginDecorator implements UserDAO {
private $_decoratedDao;
public function __construct(UserDAO $decoratedDao){
$this->_decoratedDao = $decoratedDao;
}
public function save(User $user) {
//zadaniem dekoratora jest zapisywanie loginów
//tylko wielkimi literami
$user->setLoginName(strtoupper($user->getLoginName()));
//wywołanie oryginalnej funkcjonalności
$this->_decoratedDao->save($user);
}
public function findByLoginNamePassword($loginName, $password) {
//implementacja wyszukiwania pozostaje bez zmian
return $this->_decoratedDao->findByLoginNamePassword(
$loginName, $password);
}
}
//konstruowanie udekorowanej wersji obiektu
$dao = new UserDAOUpperCaseLoginDecorator(new UserDAOImpl());
?>
www.phpsolmag.org
3
Techniki
Wzorce projektowe: dekorator
stwa dziedziczenia, ale w sposób bardziej
elastyczny.
Nie oznacza to oczywiście, iż namawiamy Was, by od dzisiaj nie używać dziedziczenia i pisać tylko dekoratory. Jak każdy wzorzec projektowy, dekorator nie powinien być stosowany tylko w celu otrzymania bardziej elastycznej architektury, ale w przypadkach, w których będzie
naprawdę pomocny. Uzbrojeni w solidne
podstawy teoretyczne możemy przyjrzeć
się wreszcie przykładom pokazującym, jak
w łatwy i efektywny sposób rozbudować
nasze aplikacje bez konieczności modyfikacji ich podstawowego kodu.
Wróćmy zatem do przykładów. Jedną z dobrych praktyk architektonicznych
jest dzielenie kodu aplikacji na warstwy.
W typowej aplikacji WWW, najniższą
warstwę stanowi baza danych (najczęściej w postaci relacyjnej bazy danych
lub zwykłych plików). Powyżej umieszczamy wartwę dostępu do danych w postaci obiektów DAO (ang. Data Access
Objects). Już ta jedna wspomniana warstwa jest okazją do zaimplementowaniu wielu funkcjonalności w postaci dekoratorów. Wyobraźmy sobie, że chcemy zapisywać czas wyszukiwania danych w DB. Nic prostszego: zamiast modyfikować istniejący kod, otoczmy go dodatkową klasą (Listing 5). A może mamy
DAO służące do zapisu danych użytkowników i w pewnych przypadkach chcemy
szyfrować hasła. Proszę bardzo: zamiast
utrzymywać dwie zbliżone wersje tego
samego DAO, dużo efektywniej rozwiążemy problem stosując prosty dekorator
– bez modyfikowania i duplikowania istniejącego kodu.
Przykłady można mnożyć również w
innych warstwach sytemu. Jeśli pomyślimy o dowolnej logice biznesowej zapisującej dane, to dekoratory znajdą zastosowanie przy wersjonowaniu danych,
zapisywaniu logów zmian, kontroli dostępu itd. Przesuwając się jeszcze jedną warstwę wyżej, do kontrolerów MVC,
szybko odkryjemy kolejne zastosowania: śledzenie zachowań użytkowników
(np. ścieżki poruszania się po serwisie
WWW) czy sprawdzanie uprawnień. Zastosowania i przykłady można by mnożyć w nieskończoność. Poprzestańmy
więc na stwierdzeniu, że wzorzec dekoratora jest niezwykle użyteczny i podpowiada eleganckie rozwiązania dla często spotykanych problemów związanych
z dodawaniem specyficznej funkcjonal-
4
ności w różnych wersjach tej samej, podstawowej aplikacji. Ta mnogość i wygoda zastosowań powoduje, że w złożonej aplikacji możemy mieć wiele dekoratorów dla jednego obiektu. Pisanie takiej
ilości dekoratorów i zarządzanie ich konfiguracją stanowi wyzwanie samo w sobie. W dalszej części artykułu zastanowimy się więc, jak szybko pisać i konfigurować dekoratory.
Zatrzymajmy się na dłużej nad przy-
wołanym przykładem zastosowania dekoratorów do dodania obsługi uprawnień na
poziomie warstwy kontrolerów MVC. Listing 6 pokazuje dwa przykładowe kontrolery oraz opakowanie z dekoratorów
sprawdzających uprawnienia. Przedstawiony schemat dodawania obsługi uprawnień do aplikacji jest niezwykle efektywny.
Po pierwsze, pozwala uruchamiać tą samą aplikację ze sprawdzaniem uprawnień
i bez, w zależności od wymagań klienta.
Listing 2. Łączymy dekoratory
<?php
class UserDAOPasswordHashingDecorator implements UserDAO {
private $_decoratedDao;
public function __construct(UserDAO $decoratedDao){
$this->_decoratedDao = $decoratedDao;
}
public function save(User $user) {
//zadaniem dekoratora jest zapisywanie loginów
//tylko wielkimi literami
$user->setPassword(md5($user->getPassword()));
//wywołanie oryginalnej funkcjonalności
$this->_decoratedDao->save($user);
}
public function findByLoginNamePassword($loginName, $password) {
//przy wyszukiwaniu również szyfrujemy hasła
return $this->_decoratedDao->findByLoginNamePassword(
$loginName, md5($password));
}
}
//konstruowanie wielokrotnie udekorowanej wersji obiektu
$dao = new UserDAOUpperCaseLoginDecorator(new UserDAOPasswordHashingDecorator(
new UserDAOImpl()));
?>
Listing 3. Przykład z Listingu 2 zapisany przy pomocy nowych podklas
<?php
class UserDAOUpperCaseLogin extends UserDAOImpl {
public function save(User $user) {
$user->setLoginName(strtoupper($user->getLoginName()));
parent::save($user);
}
}
class UserDAOPasswordHashing extends UserDAOUpperCaseLogin {
public function save(User $user) {
$user->setPassword(md5($user->getPassword()));
parent::save($user);
}
public function findByLoginNamePassword($loginName, $password) {
return parent::findByLoginNamePassword($loginName, md5($password));
}
}
//konstruowanie wersji obiektu z dziedziczeniem
$dao = new UserDAOPasswordHashing();
?>
Listing 4. Zmieniona kolejność zastosowanych dekoratorów w stosunku do
Listingu 2. Tworzymy nową funkcjonalność bez modyfikacji istniejącego kodu.
<?php
$dao = new UserDAOPasswordHashingDecorator(new UserDAOUpperCaseLoginDecorator(
new UserDAOImpl()));
?>
www.phpsolmag.org
PHP Solutions Nr 4/2006
Wzorce projektowe: dekorator
�������������������
����������������
�����������
Rysunek 1. Uproszczony graf obiektów w naszej aplikacji
Z drugiej strony, taka architektura umożliwia niezależne pisania kodu biznesowego i odpowiedzialnego za bezpieczeństwo. Wreszcie, możemy sobie wyobrazić
zastosowanie różnych silników do sprawdzania uprawnień (np. własny lub GACL).
Niestety, nasze wszystkie zachwyty trochę przybledną, jeśli uświadomimy sobie,
że dla każdego kontrolera trzeba stworzyć osobny dekorator. Nie jest to może
wielkim problemem przy pięciu czy dziesięciu kontrolerach, ale staje się koszmarem przy dużych aplikacjach, gdzie często spotkamy setki klas typu kontroler. Nasze miny staną się jeszcze bardziej nietęgie, jeśli uświadomimy sobie, że każdy z
dekoratorów będzie do siebie bardzo podobny. Ponowny rzut oka na Listing 6 faktycznie ujawnia, że w kolejnych dekoratorach różnią się tylko nazwy klas i metod
– zasadnicza logika pozostaje praktycznie bez zmian.
Bezmyślne pisanie niemal identycznych dekoratorów nie jest zajęciem szczególnie ciekawym czy produktywnym, a
dodatkowo – to oczywista duplikacja kodu. Musimy więc znaleźć jakiś sposób
na zautomatyzowanie żmudnego zajęcia.
PHP5, jako język niezwykle dynamiczny
daje nam szerokie możliwości generowania szablonowego kodu w locie. Spośród
wielu opcji dwie wydają się najciekawsze:
użycie magicznej metody __call lub generowanie całego kodu dekoratora w czasie działania skryptu. Obie metody mają
swoje wady i zalety, które za chwilę omówimy, ale najpierw spójrzmy na Listing 7,
gdzie znajdują się przykładowe dekoratory (z Listingu 6), zaimplementowane przy
użyciu obu wspomnianych metod.
Dekorator stworzony przy pomocy
__call jest dosyć prosty i pozwala prezycyjnie regulować, które metody w dekoratorze są generowane automatyczne,
a które ręcznie. Po prostu dla tych, które chcemy napisać sami, tworzymy kon-
PHP Solutions Nr 4/2006
kretną metodą, a wprzypadku pozostałych
metod polegamy na __call. Omawiana
właśnie metoda ma w zasadzie tylko jedną wadę – w ten sposób wygenerowane
dekoratory nie mogą być użyte w wywołaniach funkcji i metod, dla których określono typ argumentu (a więc dla konstrukcji $obiekt->nazwaMetody(TypArgumentu
$agrument);). Jest to dość poważna wada, ponieważ na początku podkreślaliśmy,
iż jedną z cech dekoratora jest zachowanie interfejsu oryginalnego obiektu. Zastosowanie __call do generowania dekoratorów wyklucza silniejszą kontrolę typów,
a tym samym – interfejsów implementowanych przez podstawową i udekorowaną klasę.
Techniki
Aby obejść zidentyfikowany problem,
posłużymy się prostą koncepcyjnie metodą – generowaniem kodu w locie.
Spójrzmy na Listing 8, gdzie pokazujemy
użycie specjalnie przygotowanej biblioteki (PHPProxy, dostępnej na sourceforge.net) do generowania kodu. Jak widać,
samo jej użycie jest niezwykle proste
– sprowadza się do zaimplementowania
tylko tej logiki, która ma się znaleźć w dekoratorze – nie musimy powielać ani jednego znaku kodu. Tak prosta implementacja jest możliwa dzięki istnieniu interfejsu ProxyInvocationHandler. W interfejsie tym zdefiniowana jest tylko jedna
metoda – invoke($method, $args). Nie
trzeba być potomkiem Sherlocka Holmesa by wydedukować, że metoda ta jest
uruchamiana w momencie wywoływania
metod na dekoratorze, a w argumetach
otrzymujemy wszystkie niezbędne informacje o wywołaniu – tj. nazwę metody i
jej argumenty. Korzystając z implementacji interfejsu ProxyInvocationHandler
z opisywanej biblioteki (Listing 9), możemy łatwo zdecydować, czy chcemy tylko
udekorować oryginalną funkcjonalność
(a więc wywołać ją podczas uruchamiania dekoratora), czy też stworzyć zupełnie nowy kod: cały sekret tkwi w meto-
Listing 5. Dodajemy nową funkcjonalność do zapisywania czasu wyszukiwania
danych w DB
<?php
class PerformanceLoggingUserDAO implements UserDAO {
private $_decoratedDao;
private $_startTime;
public function __construct(UserDAO $decoratedDao){
$this->_decoratedDao = $decoratedDao;
}
public function save(User $user) {
$this->startMethodExecution();
$this->_decoratedDao->save($user);
$this->stopMethodExecution('save');
}
public function findByLoginNamePassword($loginName, $password) {
$this->startMethodExecution();
return $this->_decoratedDao->findByLoginNamePassword(
$loginName, $password);
$this->stopMethodExecution('findByLoginNamePassword');
}
private function startMethodExecution(){
$this->_startTime = microtime();
}
private function stopMethodExecution($methodName){
$executionTime = microtime() - $this->_startTime;
echo "Wykonanie metody '$methodName' zajęło $executionTime <br>";
}
}
//konstruowanie udekorowanej wersji obiektu
$dao = new PerformanceLoggingUserDAO(new UserDAOImpl());
?>
www.phpsolmag.org
5
Techniki
Wzorce projektowe: dekorator
dzie getDelegate(), dzieki której możemy pobrać dekorowany obiekt i wywołać
na nim dowolną metodę.
Użycie biblioteki PHPProxy pozwala generować dekoratory przy minimalnym nakładzie pracy ze strony programisty i przy eliminacji powtórzeń w kodzie.
Niestety, nie jest to metoda pozbawiona wad. Po pierwsze, generowanie kodu
w czasie działania programu może być
zbyt czasochłonne w przypadku mocno
obciążonych serwisów internetowych,
gdzie wydajność jest rzeczą krytyczną.
Drugi, trochę mniej uciążliwy problem,
związany jest z przypadłością nękająca wszystkie rozwiązania oparte o generowanie kodu. Otóż podczas wykonania programu uruchamiany jest również kod, który... nie jest nigdzie zapisany na stałe. Oznacza to, że w pewnych
warunkach śledzenie wykonania programu (np. podczas wyszukiwania błędów)
może być nieco utrudnione. Tak czy inaczej, mimo przedstawionych wad, jest to
zdecydowanie najłatwiejsza metoda tworzenia wielu dekoratorów.
Podsumujmy naszą dotychczasową wiedzę o dekoratorach. Na wstępie stwierdziliśmy, że jest to bardzo pożyteczny wzorzec, który może być stosowany praktycznie w każdej warstwie
systemu. Jest to alternatywa do dziedziczenia obiektów, umożliwiająca wzbogacanie lub zmianę istniejącej funkcjonalności, bez konieczności modyfikacji
raz napisanego kodu. Dekorator pada
jednak trochę ofiarą własnego sukcesu
– mnogość jego zastosowań powoduje, że w bardziej rozbudowanej aplikacji możemy wykorzystać setki obiektów
implementujących wzorzec dekoratora.
Aby nie zginąć w tym gąszczu podobnych obiektów, znaleźliśmy dwie metody na dynamiczne generowanie dekoratorów. Niestety, to nie koniec problemów, które musimy rozwiązać, aby
efektywnie wykorzystać dekoratory jako integralną część rozwiązania architektonicznego.
Załóżmy, że Rysunek 1 przedstawia uproszczony graf obiektów w naszej
aplikacji. Obiekty te są oczywiście ułożone w warstwy, natomiast obiekty są połączone między sobą siecią zależności.
Już samo poprawne skonstruowanie takiej sieci obiektów może być nie lada
wyzwaniem, o czym Czytelnik mógł się
przekonać, śledząc mój artykuł Obiektowa linia montażowa, czyli przejrzyste
6
�������������������
���
�����������
���
���������������
����������������
�����������
���
��������������������
Rysunek 2. Dodając dekoratory, wprowadzamy do omawianego grafu kolejny stopień
skomplikowania, opakowując wybrane obiekty aplikacji j
Listing 6a. Dwa przykładowe kontrolery oraz opakowanie z dekoratorów
sprawdzających uprawnienia
<?php
class useraction {
private $_userService;
public function __construct(UserService $userService) {
$this->_userService = $userService;
}
public function listall(HttpRequest $request, ModelAndView $mv){
$users = $this->_userService->findAll();
$mv->addToModel('users',$users);
$mv->setView('userslist');
return $mv;
}
public function listwithexpensivequery(
HttpRequest $request, ModelAndView $mv){
$users = $this->_userService->findUserByExpensiveQuery();
$mv->addToModel('users',$users);
$mv->setView('userslist');
return $mv;
}
public function addform(HttpRequest $request, ModelAndView $mv){
$mv->setView('useraddform');
return $mv;
}
public function add(HttpRequest $request, ModelAndView $mv){
$user = new User(
$request->getParam('login'),
$request->getParam('pass'),
$request->getParam('firstname'),
$request->getParam('lastname'));
$mv->addToModel('user',$user);
try {
$this->_userService->addUser($user);
$mv->setView('useraddconfirm');
} catch (UserExistsException $e) {
$mv->addToModel('adduser_error','User exists!');
$mv->setView('useraddform');
}
return $mv;
}
}
class homepage {
public function show(HttpRequest $request, ModelAndView $mv){
$mv->setView('homepage');
return $mv;
}
}
www.phpsolmag.org
PHP Solutions Nr 4/2006
Wzorce projektowe: dekorator
�����������������������������������������������
��������������
�����������������������
�����������������������
�����������������������
�����������������������
�������������
Rysunek 3. W Pico zawarta jest cała konfiguracja związana z połączeniami pomiędzy
obiektami. Aby wprowadzić dodatkowy obiekt pośredniczący, musimy jedynie
przekonfigurować połączenia.
Listing 6b. Dwa przykładowe kontrolery oraz opakowanie z dekoratorów
sprawdzających
// Klasa pomocnicza dla dekoratorów, gdzie odbywa się
// właściwe sprawdzanie uprawnień
abstract class AbstractActionSecurityDecoratorImpl {
private $_decoratedAction;
public function __construct($decoratedAction) {
$this->_decoratedAction = $decoratedAction;
}
public function executeTargetActionWithSecurityCheck(
$targetAction, HttpRequest $request, ModelAndView $mv){
//tutaj tylko proste sprawdzanie, czy zalogowany,
//ale równie łatwo zintegrować np. GACL
session_start();
if ($_SESSION['user'] == null) {
$mv->setView('loginform');
return $mv;
} else {
return $this->_decoratedAction->$targetAction($request, $mv);
}
}
}
//Mimo, iż obie akcje są zupełnie inne,
//to dekoratory są niemal identyczne!
//Oczywista duplikacja kodu!
class UserActionSecurityDecoratorImpl
extends AbstractActionSecurityDecoratorImpl {
public function listall(HttpRequest $request, ModelAndView $mv){
return $this->executeTargetActionWithSecurityCheck(
'listall', $request, $mv);
}
public function listwithexpensivequery(
HttpRequest $request, ModelAndView $mv){
return $this->executeTargetActionWithSecurityCheck(
'listwithexpensivequery', $request, $mv);
}
public function addform(HttpRequest $request, ModelAndView $mv){
return $this->executeTargetActionWithSecurityCheck(
'addform', $request, $mv);
}
public function add(HttpRequest $request, ModelAndView $mv){
return $this->executeTargetActionWithSecurityCheck('add', $request, $mv);
}
}
class HomepageActionSecurityDecoratorImpl
extends AbstractActionSecurityDecoratorImpl {
public function show(HttpRequest $request, ModelAndView $mv){
return $this->executeTargetActionWithSecurityCheck('show', $request, $mv);
}
}
?>
PHP Solutions Nr 4/2006
www.phpsolmag.org
Techniki
i elastyczne aplikacje w PHP5, z numeru 1/2006. Dodając dekoratory, wprowadzamy do omawianego grafu kolejny stopień skomplikowania, opakowując wybrane obiekty aplikacji (Rysunek
2). Warto przy tym zauważyć, że udekorowaniu będą podlegały głównie obiekty
infrastrukturalne (kontrolery, DAO itd.), a
nie domenowe. Musimy więc znaleźć jakiś łatwy sposób na dodanie wielu dekoratorów w całej aplikacji, bez konieczności ręcznego przebudowywania połączeń.
Przy składaniu skomplikowanych
grafów obiektów doskonale sprawdza się
wzorzec architektoniczny IoC (ang. Inversion of Control), o którym również pisaliśmy w wyżej wspomnianym artykule. Mamy szczęście, ponieważ ten sam
wzorzec również znakomicie ułatwia dodawanie dekoratorów. Dlaczego? Przypomnijmy, że w przypadku stosowania
wzorca IoC bardzo pomocne są biblioteki typu kontener IoC. Te lekkie kontenery biorą na siebie cały ciężar tworzenia
skomplikowanych grafów obiektów, dbając przy tym o właściwe rozwiązanie zależności pomiędzy obiektami. Kontener
IoC jest więc jednym, centralnym miejscem, gdzie powoływane są do życia
instancje obiektów infrastrukturalnych.
Doskonale, o to nam właśnie chodziło. To scentralizowane miejsce tworzenia obiektów pozwala nam łatwo dodać
nasze "opakowania". Spójrzmy na Listing 10, gdzie znajdziemy przykład wykorzystania Pico dla PHP – lekkiego kontenera IoC – do dodania dekoratorów do
obiektów aplikacji. Jak widać, cała operacja jest stosunkowo prosta – wystarczą
dwie linijki kodu.
Przeanalizujmy dokładniej przykład z
Listingu 10. Widzimy na nim początkowo
dwa współpracujące ze sobą obiekty: serwisowy i DAO. Są to obiekty infrastrukturalne, podczas dzialania aplikacji potrzebny jest zwykle tylko jeden egzemplarz takiego obiektu. W tym przypadku to Pico
dba o powołanie do życia instancji obiektów oraz ich połączenie. Aby zastosować
dekorator zliczający czas wykonania poszczególnych metod, musimy w jakiś sposób rozerwać ścisłe połączenie pomiędzy
obiektem DAO i obiektem z DAO korzystającym. Przy klasycznym podejściu do
budowy programów, gdybyśmy użyli operatora new, metod statycznych lub fabryk,
mielibyśmy małe szanse na wprowadzenie trzeciego obiektu w łańcuch powią-
7
Techniki
Wzorce projektowe: dekorator
Listing 7. Dekorator (z Listingu 6), zaimplementowany przy użyciu __call oraz
generowanie całego kodu dekoratora w czasie działania skryptu
<?php
// implementacja dekoratora z wykorzystaniem metody __call
class UserActionSecurityDecoratorCallImpl
extends AbstractActionSecurityDecoratorImpl {
public function __call ($methodName, $args ) {
$request = $args[0];
$mv = $args[1];
return $this->executeTargetActionWithSecurityCheck(
$methodName, $request, $mv);
}}
class HomepageActionSecurityDecoratorCallImpl
extends AbstractActionSecurityDecoratorImpl {
public function __call ($methodName, $args ) {
$request = $args[0];
$mv = $args[1];
return $this->executeTargetActionWithSecurityCheck(
$methodName, $request, $mv);
}}
?>
Listing 8. Działanie PHPProxy do dynamicznego generowania kodu
<?php
// biblioteka do dynamicznego generowania klas
require_once(dirname(__FILE__).'/phpproxy/src/proxygenerator.inc.php');
// wprowadzamy interfejs na fragment kodu sprawdzający uprawnienia, aby mieć
// możliwość stosowania różnych sposobów i bibliotek
interface SecurityChecker {
function hasRightsFor(User $user, $actionToCheckRightsFor);
}
class SecurityCheckerIsNotNull implements SecurityChecker {
function hasRightsFor(User $user, $actionToCheckRightsFor){
if ($user != null) {
//tu tylko proste sprawdzanie, ale chcemy pokazać,
//że kod odpowiedzialny za sprawdzanie uprawnień
//jest wydzielony i może być użyty w dekoratorach
return true;
} else {
return false;
}
} }
// Część wspólna dla wszystkich dekoratorów. Nie ma tu duplikacji kodu jak na
// Listingu 7.Cała logika związana ze sprawdzeniem uprawnień i wyowołaniem
// (lub nie) dekoratora zamknięta jest w tej jednej klasie
class SecurityCheckerMethodInvImpl extends DelegatingInvocationHandler {
private $_securityChecker;
private $_decoratedAction;
public function __construct(SecurityChecker $securityChecker) {
$this->_securityChecker = $securityChecker;
}
public function invoke($method, $args) {
$request = $args[0];
$mv = $args[1];
$user = getLoggedInUser();
if ($this->_securityChecker($user, $method)){
return parent::invoke($method, $args);
} else {
$mv->setView('loginform');
return $mv;
}
}
public function getLoggedInUser(){
session_start();
return $_SESSION['user'];
} }
$proxyGenerator = new ProxyClassGenerator();
$secureMethodInvocation = new SecurityCheckerMethodInvImpl(
new SecurityCheckerIsNotNull());
// dynamiczne wygenerowanie dowolnego dekoratora sprowadza się do jednej linii
// kodu!
$useractionDecorated = $proxyGenerator->getProxy(
'useraction', $secureMethodInvocation);
$homepageDecorated = $proxyGenerator->getProxy(
'homepage', $secureMethodInvocation);
?>
8
www.phpsolmag.org
zań. W przypadku kontenera IoC sprawa jest o wiele prostsza, ponieważ w Pico zawarta jest cała konfiguracja związana z połączeniami pomiędzy obiektami.
Jedyne, co musimy zrobić, to przekonfigurować te połączenia w taki sposób, by
wprowadzić dodatkowy obiekt pośredniczący (Rysunek 3). W Pico robimy to przy
pomocy parametrów komponentu, tak jak
pokazaliśmy w drugiej części Listingu 10.
Oczywiście pokazany sposób można wykorzystać do użycia więcej niż jednego
dekoratora (Listing 11).
Kontenery IoC w znaczący sposób
ułatwiają stosowanie dekoratorów w złożonych aplikacjach. Dzieki nim możemy
w jednym, centralnym miejscu, skonfigurować połączenia między obiektami i
tym samym dodać nowy element do takiego połączenia. Sposób jest dość prosty i nie wymaga zmian w kodzie PHP
– jedynie w konfiguracji kontenera IoC.
Listing 9. Korzystając z
implementacji interfejsu
ProxyInvocationHandler z
opisywanej biblioteki możemy
łatwo zdecydować, czy chcemy
tylko udekorować oryginalną
funkcjonalność, czy też stworzyć
zupełnie nowy kod
<?php
interface ProxyInvocationHandler {
public function
invoke($method, $args);
}
class DelegatingInvocationHandler
implements
ProxyInvocationHandler {
private $_delegate = null;
public function __construct(
$delegate){
$this->_delegate =
$delegate;
}
public function invoke(
$method, $args){
$reflectionClass =
new ReflectionClass(
get_class($this->
getDelegate()));
$reflectionMethod =
$reflectionClass->
getMethod($method);
return $reflectionMethod->
invokeArgs(
$this->getDelegate(),$args);
}
public function getDelegate(){
return $this->_delegate;
}
}
?>
PHP Solutions Nr 4/2006
Wzorce projektowe: dekorator
Rozwiązanie jest prawie idealne. Prawie, bo w dalszym ciągu dla każdego
dekoratora trzeba dodać nowy wpis w
konfiguracji i zmodyfikować połączenia
między obiektami. Dla twórczo – lewni-
wego programisty to zdecydowanie zbyt
duży wysiłek.
Jeśli pomyślimy całościowo o stylu
programowania, w którym napisany został Listing 10, to powinna nasunąć się
Listing 10. Przykład wykorzystania Pico dla PHP – lekkiego kontenera IoC – do
dodania dekoratorów do obiektów aplikacji
<?php
$parentPico = new DefaultPicoContainer();
$parentPico->regComponentImpl('FrontController', 'FrontControllerImpl');
$parentPico->regComponentImpl(
'ActionResolvingStrategy',
//componentKey
'PicoActionResolvingStrategy', //componentClass
array ('paramName' => 'action')
);
$parentPico->regComponentImplWithIncFileName(
dirname(__FILE__).'/lib/SmartyViewResolver.php',
'ViewResolvingStrategy',
'SmartyViewResolvingStrategy',
array (
array('compile_dir' => dirname(__FILE__).'/work/templates_c'),
'file:'.dirname(__FILE__).'/templates/smarty/',
'.tpl')
);
$pico = new DefaultPicoContainer(null, $parentPico);
$pico->regComponentInstance($pico, 'PicoContainer');
$pico->regComponentImplWithIncFileName(dirname(__FILE__).
'/../shared/model/model.inc.php','UserService','UserServiceImpl');
$pico->regComponentImplWithIncFileName(dirname(__FILE__).
'/../shared/model/model.inc.php','UserDAO','UserDAOPDOImpl');
$pico->regComponentImpl('PDO','PDOPicoAdapter',array
( 'pgsql:dbname=ditalkdb;host=localhost', 'postgres', 'postgres'));
//actions
$pico->regComponentImplWithIncFileName(dirname(__FILE__).
'/actions/loginaction_action.php','loginaction','loginaction');
$pico->regComponentImplWithIncFileName(dirname(__FILE__).
'/actions/useraction_action.php','useractionTarget', 'useraction');
$pico->regComponentImpl('useraction', 'UserActionSecurityDecoratorImpl', array
('decoratedAction' => new BasicComponentParameter('useractionTarget')));
$pico->regComponentImplWithIncFileName(dirname(__FILE__).
'/actions/homepage_action.php','homepageTarget','homepage');
$pico->regComponentImpl('homepage', 'HomepageActionSecurityDecoratorImpl', array
('decoratedAction' => new BasicComponentParameter('homepageTarget')));
$fc = $pico->getComponentInstance('FrontController');
$fc->doService(new HttpRequest());
?>
Listing 11. Przykład rozwiązania, które nie skazuje nas na żmudne deklarowanie
pojedynczych dekoratorów
<?php
//rejestracja poprzednich komponentów
//jak na Listingu 10
$pico->regComponentImplWithIncFileName(dirname(__FILE__).
'/actions/useraction_action.php','useraction', 'useraction');
$pico->regComponentImplWithIncFileName(dirname(__FILE__).
'/actions/homepage_action.php','homepage','homepage');
$pico->regComponentImplWithIncFileName(dirname(__FILE__).
'/actions/loginaction_action.php','loginaction','loginaction');
//security decorators
$pico->regComponentImpl('SecurityMethodInterceptor');
$pico->registerAspect(new Aspect(new RegExpNameMatchingPointcut(
'/^[a-km-z]+action$/'),new MatchingAllPointcut(),
'SecurityMethodInterceptor'));
?>
PHP Solutions Nr 4/2006
www.phpsolmag.org
Techniki
nam pewna refleksja. Otóż w opisywanym przypadku stosujemy dekoratory do
globalnych zmian w całej aplikacji. Patrzymy na działające skrypty i myślimy: teraz
chcielibyśmy dodać logowanie do wszystkich obiektów DAO, żeby zobaczyć, gdzie
jest wąskie gardło wydajnościowe, albo: teraz udekorujmy wszystkie kontrolery, żeby sprawdzać uprawnienia. Niestety, wypracowana do tej pory metoda skazuje nas na żmudne deklarowanie pojedynczych dekoratorów. Gdyby tylko istniała konstrukcja programistyczna, pozwalająca łatwo wyrazić żądania typu: obiekty
określonego typu udekoruj danym kodem,
moglibyśmy dosłownie w kilku linijkach kodu wprowadzić do aplikacji logowanie czy
sprawdzanie uprawnień. Zupełnie niezależnie od ilości obiektów do udekorowania. Na pocieszenie chcemy powiedzieć,
że takie metody już powstają (przykładowy Listing 11)! Jest to idea bardzo zbliżona do programowania aspektowego (ang.
Aspect Oriented Programming), ale to już
temat na zupełnie inny artykuł.
Podsumowanie
W artykule pokazaliśmy, że dekorator jest
prostym i niezwykle pożytecznym wzorcem projektowym. Jest tak użyteczny,
że w pewnym momencie możemy mieć
ochotę zastosować go na bardzo szeroką skalę. Aby jednak zrobić to efektywnie,
musimy zadbać o rozwiązanie dwóch problemów: automatycznego generowania
dekoratorów oraz ich konfiguracji w całej
aplikacji. Z pierwszym problem doskonale
poradzimy sobie dynamicznie generując
powtarzalny kod dekoratora. Zastosowanie kontenera IoC wpłynie dodatnio na architekturę naszej aplikacji i umożliwi łatwe
konfigurowanie dekoratorów. Jeśli zrozumiemy i opanujemy przedstawione powyżej triki, świat programowania aspektowego nie będzie miał przed nami tajemnic. n
O autorze:
Paweł Kozłowski jest pracownikiem SUPERMEDIA, gdzie od roku 2000 projektuje i tworzy złożone aplikacje WWW w
PHP. Obecnie zajmuje się rozwijaniem
frameworków i bibliotek ORM opartych
na PHP5. Jest autorem portu PicoContainer dla PHP5 i wielu publikacji poświęconych PHP.
Kontakt: [email protected]
9

Podobne dokumenty