Rozwiazywanie problemow przekrojowych z uzyciem IoC
Transkrypt
Rozwiazywanie problemow przekrojowych z uzyciem IoC
Dla zaawansowanych Rozwiązywanie problemów przekrojowych z użyciem IoC Stopień trudności: lll Piotr Szarwas W każdej aplikacji podzielonej na warstwy występują elementy, których nie można przypisać do żadnej z warstw i które stanowią twardy orzech do zgryzienia nawet dla dużego zespołu programistów. Istnieje jednak w miarę prosty sposób poradzenia sobie z nimi: użycie kontenera IoC... K W SIECI XXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXX XXXXX 2 ażdą aplikację można podzielić na logiczne warstwy. Najczęściej występują trzy warstwy: warstwa danych, logiki biznesowej i prezentacji. Wszystkie spośród nich powinny być nałożone na siebie tak, aby wyższa wiedziała tylko o istnieniu jednej warstwy niższej; wszystko, co znajduje się poniżej, powinno być dla niej niedostępne. Z drugiej strony, warstwa niższa nie wymaga dla swojego działania istnienia żadnej z warstw wyższych. Podział ten ilustrujemy na Rysunku 1. Można jeszcze bardziej dopracować ten schemat mówiąc, że aplikacja powinna posiadać aż pięć warstw: warstwę danych, dostępu do danych, logiki biznesowej/usług, kontrolerów i prezentacji. Ten drugi podział wynika bezpośrednio ze stosowania wzorców programistycznych i architektonicznych. Przykładowo, kontrolery i prezentacja stanowią element wzorca architektonicznego www.phpsolmag.org MVC. Pojawienie się warstwy dostępu do danych wynika z zastosowania wzorca DAO, który uniezależnia logikę biznesową od warstwy danych. Dzięki temu można łatwo wymieniać źródło danych. Schemat tego podziału pokazujemy na Rysunku 2. Rozbicie systemu na warstwy zapewnia szereg korzyści: Każdej z warstw można wymienić implementację, nie zmieniając jednocześnie implementacji pozostałych warstw. Jak już wspomnieliśmy, można na przykład wymienić warstwę DAO (dostępu do Co należy wiedzieć... Należy znać zasady programowania obiektowego w PHP5 z wykorzystaniem wzorców projektowych. Co obiecujemy... Pokażemy, jak rozwiązać niektóre spośród problemów przekrojowych (ang. crosscutting concerns). PHP Solutions Nr 6/2006 Crosscutting concerns ������������������� ������������������������� �������������� Niestety, praktyka bywa czasem bardziej skomplikowana i nie wszystkie zagadnienia programistyczne możemy umieścić na jednej wybranej warstwie. Te, których nie da się w ten sposób potraktować, nazywamy problemami przekrojowymi (ang. Crosscutting concerns). Najpopularniejsze z nich to: l l Rysunek 1. Podział aplikacji na trzy logiczne warstwy. Warstwa wyższa wie tylko o istnieniu warstwy znajdującej się bezpośrednio pod nią. Żadna z warstw niższych nie wie nic o wyższych danych) tak, aby obsługiwała inne źródło danych, nie zmieniając jednocześnie logiki biznesowej. Każda warstwa może być traktowana jako spójna całość. Pozwala to m.in. na tworzenie każdej warstwy przez osobny zespół programistów, który nie musi znać implementacji pozostałych warstw. Jeśli zostanie dobrze zaprojektowana, każda warwstwa(warstwa) może być wykorzystywana wielokrotnie, w różnych projektach lub miejscach tego samego projektu. logowanie błędów i debugowanie, bezpieczeństwo i uwierzytelnianie, transakcje. ������������������� ������������������� ������������������������� ������������������������� Problem logowania błędów Rozważmy problem logowania błędów. Praktycznie każda aplikacja zawiera elementy kodu odpowiadające za logowanie i debugowanie. Czy jednak możemy je przydzielić do którejś warstwy? Niestety nie. Ich kod jest rozproszony po całej aplikacji i występuje w każdej warstwie. Pamiętajmy, że z punktu widzenia logicznego podziału na warstwy, logowanie błędów nie jest ani częścią logiki biznesowej, ani innej warstwy. Bezpośrednie zakodowanie logiki logowania w każdej z warstw powoduje, że niezależność, przenaszalność i elastyczność tej warstwy drastycznie maleje. W przypadku logowania problem nie jest tak wielki, ale już w sytuacji, Listing 1. Implementacja klasy MailSender służącej do wysyłania maili. Kod klasy pozbawiony jest reguł problemów przekrojowych: logowania i bezpieczeństwa. Reguły te będą implementowane przez odpowiednie dekoratory interface MailSender { public function send($to, $from, $subject, $message); } class MailSenderImpl implements MailSender { public function send($to, $from, $subject, $message) { return mail($to,$subject,$message,'From: '.$from); } } Listing 2. Przykładowa klasa dekorująca dowolną klasę implementującą interface MailSender class MailSenderDecorator implements MailSender { private $mailSender; public function __construct(MailSender $mailSender ){ $this->mailSender = $mailSender; } public function send($to, $from, $subject, $message) { // Kod dekoratora $result = $this->send($to,$subject,$message,'From: '.$from); // Kod dekoratora return $result; } } PHP Solutions Nr 6/2006 Dla zaawansowanych www.phpsolmag.org �������������� Rysunek 2. Podział aplikacji na pięć logicznych warstw. Warstwa wyższa wie tylko o istnieniu warstwy znajdującej się bezpośrednio pod nią. Żadna z warstw niższych nie wie nic o wyższych gdy w aplikacji zaszyjemy reguły bezpieczeństwa, czy też zdefiniujemy miejsca, w których rozpoczynają się i kończą transakcje, cały kod przestaje w zasadzie być przenaszalny i staje się mocno związany z regułami biznesowymi danej aplikacji. Rysunek 3 pokazuje schematycznie nasz problem. Skoro problemy przekrojowe mogę sprawić takie trudności, czy da się je wydzielić do osobnych modułów, które nie będą w ogóle powiązane z żadną z warstw? Okazuje się, że tak. Rozwiązania są dwa. Pierwsze, którym się zajmiemy w tym artykule, będzie polegało na wykorzystaniu wzorca Dekorator i kontenera IoC, który zbudowaliśmy wspólnie w poprzednim artykule. Drugi sposób opiera się na zastosowaniu programowania aspektowego (ang. aspect-oriented programming - AOP). Ten drugi sposób byłby dużo lepszym podejściem; niestety, obecnie dla PHP nie istnieje żaden framework AOP, który w wystarczającym stopniu implementowałby wszystkie założenia programowania aspektowego. Tak jak poprzednio, cały kod zaprezentowany w artykule zostanie umieszczony na stronie http://flexi.sf.net/. Wzorzec Dekorator Zgodnie z definicją GoF (ang. Gang of Four – termin, który określa czwórkę pro- 3 Dla zaawansowanych Crosscutting concerns gramistów odpowiedzialnych za zapoczątkowanie idei wzorców projektowych), zadaniem wzorca Dekorator jest wzbogacanie funkcjonalności obiektów wybranych klas w sposób dynamiczny, bez konieczności modyfikowania oryginalnego kodu. Ponieważ ta definicja może być mało przejrzysta, posłużymy się przykładem. Zbudujemy prostą klasę MailSenderImpl, której zadaniem będzie wysyłanie maili (Listing 1). Wiemy, że klasa ta będzie na tyle uniwersalna, że będziemy chcieli wykorzystać ją w kilku innych, już istniejących projektach. Niestety, każdy z tych projektów ma inne wymagania biznesowe związane z regułami logowania i bezpieczeństwa. Dlatego nie wyposażymy klasy MailSenderImpl w logikę związaną z tymi aspektami, lecz zaimplementujemy tę ostatnią w dekoratorach (Listing 2). Za każdym razem, gdy w którymś z projektów zaistnieje potrzeba wysłania maila, wykorzystamy do tego odpowiedni dekorator, przykładowo: new MailSenderDecorator( new MailSenderImpl()) Stosując wzorzec Dekorator udało się nam osiągnąć dwie rzeczy. Po pierw- ������������������� ������������������� ������������������������� ������������������������� ��������������������� �������������� Rysunek 3. Problemy przekrojowe znajdują się zawsze z boku każdego podziału na warstwy, a ich kod jest rozproszony pomiędzy tymi ostatnimi sze, uniwersalna klasa MailSender pozostała niezmieniona – cała logika związana z wysyłaniem maili jest w jednym miejscu. Po drugie, kod charakterystyczny dla każdej z aplikacji znajduje się w osobnych klasach. Wykorzystując wzorzec Dekorator warto również pamiętać, że jest on bar- Listing 3. Implementacja nowej wersji kontenera IoC obsługującej dekorowanie obiektów class IoCContainerWithDecoratorSupport extends DefaultIoCContainter { private $decoratorsSupport = array(); public function create($className) { $classObj = parent::create($className); if (!$classObj instanceof IoCDecoratorSupport&&!$classObj instanceof IoCDecorator){ foreach( $this->getDecoratorsSupport() as $decoratorSupport ) { if ( $decoratorSupport->match($className,$classObj) ) { $decoratorObj = parent::create( $decoratorSupport-> getDecoratorName() ); $decoratorObj->setObject($classObj); $classObj = $decoratorObj; } } } return $classObj; } public function setDecoratorsSupport( array $decoratorsSupport ) { foreach($decoratorsSupport as $decorator) { if ( !$decorator instanceof IoCDecoratorSupport ) { throw new Exception('Class '.get_class($decorator). ' does not implement IoCDecoratorSupport interface'); } $this->decoratorsSupport[] = $decorator; } } private function getDecoratorsSupport(){ return $this->decoratorsSupport; } } 4 ���������������� www.phpsolmag.org dzo użyteczny, gdy kod źródłowy klas nie jest dostępny: jego użycie stanowi wtedy jedyną możliwość zmiany zachowania obiektu. Wzorzec Dekorator w połączeniu z kontenerem IoC Wróćmy teraz do przedstawionego w poprzednim artykule kontenera IoC. Jak zapewne pamiętamy, kontener IoC to zwyczajnie konfigurowalna fabryka obiektów potrafiąca powołać do życia całe drzewa tych ostatnich. Pokażemy teraz, jak można wykorzystać kontener IoC tak, aby poza tworzeniem obiektów potrafił je także dekorować. Wyobraźmy sobie następujący problem: klient, który zlecił nam projekt, zażyczył sobie, aby każda operacja modyfikacji danych (kto, kiedy i co zmieniał) była logowana. Typowy programista zapewne rozwiązałby ten problem dokonując modyfikacji każdej metody zmieniającej dane. To podejście wydaje się najbardziej oczywiste, ale niestety jest najgorszym z możliwych. Zmodyfikowane zostałyby bowiem wszystkie kluczowe metody aplikacji, co oznacza potrzebę przetestowania całego projektu od nowa. Dodatkowym problemem byłaby konieczność przekazania do każdej z warstw aplikacji informacji o tym, kto aktualnie wykonuje daną metodę. Ponadto, jeżeli modyfikowany kod był wykorzystywany w innych projektach, programista musiałby przestać go współdzielić. Jak PHP Solutions Nr 6/2006 Crosscutting concerns więc widać, podejście, które wydawało się najbardziej oczywiste, spowodowałoby całą lawinę problemów. My na szczęście będziemy mądrzejsi i do rozwiązania problemu wykorzystamy kontener IoC. Nasze założenie jest następujące: nie możemy dokonać żadnej modyfikacji kodu zapisującego i modyfikującego dane. Dlatego skonstruujemy kon- tener, który na podstawie odpowiedniego wzorca składającego się z nazwy klasy oraz implementowanego przez nią interfejsu będzie umiał udekorować tworzony obiekt. Zabierzmy się więc do pracy. Pierwszym krokiem będzie modyfikacja kontenera IoC tak, aby na podstawie zadanego wzorca mógł połączyć dekorowany Listing 4. Implementacja klasy dopasowującej dekoratory do klas na podstawie nazw klas obiekt nie implementuje jednego z interfejsów klas pomocniczych dekoratorów, aby uniknąć dekoracji tych ostatnich. Następnie kontener sprawdza przy pomocy klas pomocniczych, czy nazwa świeżo powołanej klasy pasuje do wzorca któregoś z dekoratorów. Jeżeli tak, to klasa pomocnicza zwraca nazwę dekoratora. Każda klasa pomocnicza musi implementować interfejs IoCDecoratorSupport, który z kolei składa się z dwóch metod: l class ClassNameIoCDecoratorSupport implements IoCDecoratorSupport { private $decoratorName; private $classNameToDecorator = array(); public function __construct($decoratorName,array $classNameToDecorator){ $this->decoratorName = $decoratorName; $this->classNameToDecorator = $classNameToDecorator; } public function match($className,$classObject) { return (in_array($className,$this->classNameToDecorator))?true:false; } obiekt$this->decoratorName; z dekoratorem. W tym public function getDecoratorName() { return } celu rozsze} rzymy klasę DefaultIoCContainer i zmo- dyfikujemy jej metodę create(). W nowej Listing 5. Implementacja klasy dopasowującej dekoratory na podstawie wersji tej klasy,do jejklas metoda create() najdowolnego wyrażenia regularnego pierw wywołuje create() klasy nadrzędNastępnie sprawdza, czy powołany class RegExpIoCDecoratorSupport implements nej. IoCDecoratorSupport { } private $decoratorName; private $pattern; public function __construct($decoratorName,$pattern){ $this->decoratorName = $decoratorName; $this->pattern = $pattern; } public function match($className,$classObject) { return (preg_match($this->pattern,get_class($classObject)))?true:false; } public function getDecoratorName() { return $this->decoratorName; } Listing 6. Implementacja klasy dopasowującej dekoratory do klas na podstawie interfejsu class InterfaceIoCDecoratorSupport implements IoCDecoratorSupport { private $decoratorName; private $interfaceName; public function __construct( $decoratorName,$interfaceName){ $this->decoratorName= $decoratorName; $this->interfaceName= $interfaceName; } public function match($className, $classObject) { return ($classObject instanceof $this->interfaceName)? true:false; } public function getDecoratorName(){ return $this->decoratorName; } } PHP Solutions Nr 6/2006 www.phpsolmag.org Dla zaawansowanych l – zwraca true, jeżeli wzorzec dekoratora pasuje do wzorca klasy, getDecoratorName() – zwraca nazwę dekoratora z pliku konfiguracyjnego IoC. match() Klasy pomocnicze dostarczane są do kontenera poprzez metodę setDecorators(). Następnie kontener IoC powołuje do życia tenże dekorator i umieszcza w nim dekorowany obiekt. Dekorator musi implementować interfejs IoCDecorator, który posiada jedną metodę – setObject(). Zauważmy, że w ten sposób możemy każdą z klas udekorować wieloma dekoratorami. Warto jeszcze zapamiętać, że zgodnie z implementacją naszego kontenera, dekorator nie może być singletonem. To znaczy: dla każdej dekorowanej klasy musi być powoływany do życia nowy dekorator. Na Listingu 3 przedstawiamy kod nowej wersji kontenera, a na Listingach 4, 5 i 6 – kilka przykładowych klas pomocniczych implementujących interfejs IoCDecoratorSupport. Pierwsza klasa odnajduje klasy według pełnej nazwy, druga wg wyrażenia regularnego, a trzecia wg konkretnego interfejsu, który implementuje dekorowana klasa. Wróćmy teraz do naszego przykładu. Załóżmy dla uproszczenia, że aplikacja, w której trzeba dokonać zmian, ma trzy klasy modyfikujące dane: UserDAO, OrderDAO oraz ItemDAO. Wszystkie one implementują jeden wspólny interfejs DAO zawierający metody setConnection(), findById(), save(), update(), delete(). Metoda setConnection() ustawia połączenie do źródła danych, findById() zwraca obiekt na podstawie jego id, save() zapisuje obiekt, update() go zmienia, a delete() kasuje. Dane są modyfikowane jedynie przez metody save(), update() i delete(). Szczegóły implementacji klas *DAO nie mają znaczenia w naszym przykładzie. Na Listingu 7 prezentujemy przykła- 5 Dla zaawansowanych Crosscutting concerns dową konfigurację kontenera bez dekoratorów. Jak widzimy, składa się ona z czterech wpisów, z których jeden dotyczy połączenia z bazą danych, a pozostałe trzy zawierają ustawienia klas DAO. Zmodyfikujemy teraz przykład tak, aby spełniał on wymagania biznesowe naszego klienta. Nic prostszego: zadanie rozpoczynamy od utworzenia klasy DAOLoggerDecorator. Klasa ta implementuje dwa interfejsy: IoCDecorator i DAO (Li- sting 8). Oczywiście, jej docelowa implementacja powinna zawierać bardziej wyrafinowany kod logujący. Teraz wykorzystując klasę z Listingu 6 modyfikujemy konfigurację kontenera. Nową konfigurację pokazujemy na Listingu 9. Zauważmy, że dodaliśmy tylko dwa wpisy i zgodnie z założeniami, kod klas UserDAO, OrderDAO oraz ItemDAO nie został zmieniony. W przykładzie pominęliśmy problem przekazywania informacji o tym, kto modyfikuje Listing 7. Plik konfiguracji kontenera IoC dla opisanego w tekście przykładu bez wsparcia dekoratorów $currentDir = dirname(__FILE__); $frameworkPath = realpath( $currentDir.'/../../flexi' ); ini_set( 'include_path', ini_get('include_path'). PATH_SEPARATOR.$frameworkPath.'/' ); require_once 'ioc/IoCContainerWithDecoratorSupport. class.php'; require_once 'ioc/MappingBuilderFromArray. class.php'; $iocMap = array( "connection" => array( "className" => "Connection", "file" => $currentDir."/Connection.class.php", "singleton" => true, "properties" => array(), "constructorParams" => array() ), "userDAO" => array( "className" => "UserDAO", "file" => $currentDir."/UserDAO.class.php", "singleton" => true, "properties"=>array( "connection"=>"&connection"), "constructorParams" => array() ), "orderDAO" => array( "className" => "OrderDAO", "file" => $currentDir."/OrderDAO.class.php", "singleton" => true, "properties" => array( "connection"=>"&connection"), "constructorParams" => array() ), "itemDAO" => array( "className" => "ItemDAO", "file" => $currentDir."/ItemDAO.class.php", "singleton" => true, "properties" => array( "connection"=>"&connection"), "constructorParams" => array() ), ); $mappingBuilder = new MappingBuilderFromArray($iocMap); $iocContainer=new IoCContainerWithDecoratorSupport( $mappingBuilder->getApplicationMap()); $userDAO = $iocContainer->create("userDAO"); $orderDAO = $iocContainer->create("orderDAO"); $itemDAO = $iocContainer->create("itemDAO"); var_dump($userDAO); var_dump($orderDAO); var_dump($itemDAO); 6 dane. Gdyby kod (zupełnie inny niż w naszym przykładzie) nie miał tak jasno zdefiniowanego interfejsu, jak nasze klasy DAO, do tworzenia dekoratorów moglibyśmy wykorzystać metodę _ _ call(), której działanie zostało opisane w dokumentacji PHP. W podobny sposób, jak reguły związane z logowaniem, moglibyśmy dodać do aplikacji kod odnoszący się do reguł bezpieczeństwa i praw dostępu użytkow- Listing 8. Klasa DAOLoggerDecorator jest dekoratorem klas UserDAO, OrderDAO i ItemDAO class DAOLoggerDecorator implements IoCDecorator, DAO { private $dao; public function findById($id){ // Kod dekoratora $result = $this->dao->findById($id); // Kod dekoratora ... } return $result; public function save($object){ // Kod dekoratora $result = $this->dao->save($object); // Kod dekoratora return $result; } public function update($object){ // Kod dekoratora $result = $this->dao->update($object); // Kod dekoratora ... } return $result; public function delete($object){ // Kod dekoratora $result = $this->dao->delete($object); // Kod dekoratora ... } return $result; public function setConnection($connection){ // Kod dekoratora $result = $this->dao-> setConnection($connection); // Kod dekoratora return $result; } public function setObject($object){ if ( !$object instanceof DAO ){ throw new Exception('Class '.get_class($object). ' must implement DAO interface'); } $this->dao = $object; } } www.phpsolmag.org PHP Solutions Nr 6/2006 Crosscutting concerns Listing 9. Nowa wersja pliku konfiguracyjnego kontenera $currentDir = dirname(__FILE__); $frameworkPath = realpath( $currentDir.'/../../flexi' ); ini_set( 'include_path', ini_get('include_path').PATH_SEPARATOR. $frameworkPath.'/' ); require_once 'ioc/IoCContainerWithDecoratorSupport.class.php'; require_once 'ioc/MappingBuilderFromArray.class.php'; $iocMap = array( ); "connection" => array( "className" => "Connection", "file" => $currentDir."/Connection.class.php", "singleton" => true, "properties" => array(), "constructorParams" => array() ), "userDAO" => array( "className" => "UserDAO", "file" => $currentDir."/UserDAO.class.php", "singleton" => true, "properties" => array("connection"=>"&connection"), "constructorParams" => array() ), "orderDAO" => array( "className" => "OrderDAO", "file" => $currentDir."/OrderDAO.class.php", "singleton" => true, "properties" => array("connection"=>"&connection"), "constructorParams" => array() ), "itemDAO" => array( "className" => "ItemDAO", "file" => $currentDir."/ItemDAO.class.php", "singleton" => true, "properties" => array("connection"=>"&connection"), "constructorParams" => array() ), "interfaceIoCDecoratorSupport" => array( "className" => "InterfaceIoCDecoratorSupport", "file" => $frameworkPath."/ioc/decorators/InterfaceIoCDecoratorSupport. class.php", "singleton" => true, "properties" => array(), "constructorParams" => array("daoDecorator","DAO") ), "daoDecorator" => array( "className" => "DAOLoggerDecorator", "file" => $currentDir."/DAOLoggerDecorator.class.php", "singleton" => false, "properties" => array(), "constructorParams" => array() ), $mappingBuilder = new MappingBuilderFromArray( $iocMap ); $iocContainer = new IoCContainerWithDecoratorSupport( $mappingBuilder->getApplicationMap() ); $iocContainer->setDecoratorsSupport( array( $iocContainer->create("interfaceIoCDecoratorSupport"))); $userDAO = $iocContainer->create("userDAO"); $orderDAO = $iocContainer->create("orderDAO"); $itemDAO = $iocContainer->create("itemDAO"); var_dump($userDAO); var_dump($orderDAO); var_dump($itemDAO); PHP Solutions Nr 6/2006 www.phpsolmag.org Dla zaawansowanych ników. Wyobraźmy sobie, że klient definiuje nowy wymóg: chce, aby modyfikacji danych mogli dokonywać tylko administratorzy aplikacji. Możemy tu zastosować podobne podejście, jak przy logowaniu. Utworzymy więc klasę DAOSecutiryDecorator, która przy wywoływaniu każdej metody save(), update() czy delete() będzie sprawdzała, czy wykonujący ją użytkownik ma prawa do jej użycia: jeżeli nie, aplikacja zgłosi wyjątek. Podsumowanie Stosowanie kontenera IoC i wzorca Dekorator nie jest remedium na wszelkie problemy przekrojowe. Aby wprowadzenie do kodu rzeczywistej aplikacji aspektów logowania i bezpieczeństwa było możliwe od samego początku, aplikacja ta musi mieć jasny podział na warstwy. Należy również ją tworzyć zgodnie z dobrymi praktykami programowania obiektowego, w szczególności kładąc nacisk na interfejsy i kompozycję obiektów. Warto też pamiętać, że reguły bezpieczeństwa i logowania nie są jedynymi problemami przekrojowymi: zaliczają się do nich również transakcje, profilowanie, keszowanie (caching) czy walidacja. Na szczęście i te problemy można rozwiązać przy pomocy kontenera IoC i odpowiednich dekoratorów. n O autorze Piotr Szarwas ma wieloletnie doświadczenie w programowaniu i tworzeniu aplikacji WWW (PHP, Java). Jest konsultantem w jednej z największych polskich firm IT, a także doktorantem na Wydziale Fizyki Politechniki Warszawskiej. Od dawna pisze artykuły dla PHP Solutions. Kontakt z autorem: [email protected] 7