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