PHP. Obiekty, wzorce, narzędzia [4 ed.] 9788324691814

Twój przewodnik po obiektowym PHP! Język PHP przebył długą drogę od swoich początków do obecnego poziomu rozwoju. Dziś j

1,032 66 8MB

Polish Pages [489] Year 2014

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

PHP. Obiekty, wzorce, narzędzia [4 ed.]
 9788324691814

Table of contents :
Spis treści
O autorze
O recenzencie technicznym
Podziękowania
Wprowadzenie
Rozdział 1. PHP — projektowanie i zarządzanie
Problem
PHP a inne języki programowania
O książce
Obiekty
Wzorce
Narzędzia
Nowości w czwartym wydaniu
Podsumowanie
Rozdział 2. Obiekty
Nieoczekiwany sukces obiektów w PHP
PHP/FI — u zarania języka
PHP3 — składniowy lukier
Cicha rewolucja — PHP4
PHP5 — nieuchronne zmiany
Debata obiektowa — za czy przeciw?
Podsumowanie
Rozdział 3. Obiektowy elementarz
Klasy i obiekty
Pierwsza klasa
Pierwszy obiekt (lub dwa)
Definiowanie składowych klasy
Metody
Metoda konstrukcji obiektu
Typy argumentów metod
Typy elementarne
Typy obiektowe
Dziedziczenie
Problemy związane z dziedziczeniem
Stosowanie dziedziczenia
Zarządzanie dostępem do klasy — słowa public, private i protected
Podsumowanie
Rozdział 4. Zaawansowana obsługa obiektów
Metody i składowe statyczne
Składowe stałe
Klasy abstrakcyjne
Interfejsy
Cechy typowe
Zadanie dla cech typowych
Definiowanie i stosowanie cechy typowej
Stosowanie wielu cech typowych
Łączenie cech z interfejsami
Unikanie kolizji nazw metod za pomocą słowa insteadof
Aliasy metod cech typowych
Cechy typowe z metodami statycznymi
Dostęp do składowych klasy włączającej
Definiowanie metody abstrakcyjnej cechy typowej
Zmiana dostępności metod cech typowych
Późne wiązanie statyczne: słowo static
Obsługa błędów
Wyjątki
Klasy i metody finalne
Przechwytywanie chybionych wywołań
Definiowanie destruktorów
Wykonywanie kopii obiektów
Reprezentacja obiektu w ciągach znaków
Wywołania zwrotne, funkcje anonimowe i domknięcia
Podsumowanie
Rozdział 5. Narzędzia obiektowe
PHP a pakiety
Pakiety i przestrzenie nazw w PHP
Automatyczne wczytywanie kodu
Klasy i funkcje pomocnicze
Szukanie klasy
Badanie obiektów i klas
Pozyskiwanie ciągu pełnej nazwy klasy
Badanie metod
Badanie składowych
Badanie relacji dziedziczenia
Badanie wywołań metod
Interfejs retrospekcji — Reflection API
Zaczynamy
Pora zakasać rękawy
Badanie klasy
Badanie metod
Badanie argumentów metod
Korzystanie z retrospekcji
Podsumowanie
Rozdział 6. Obiekty a projektowanie obiektowe
Czym jest projektowanie?
Programowanie obiektowe i proceduralne
Odpowiedzialność
Spójność
Sprzęganie
Ortogonalność
Zasięg klas
Polimorfizm
Hermetyzacja
Nieważne jak
Cztery drogowskazy
Zwielokrotnianie kodu
Przemądrzałe klasy
Złota rączka
Za dużo warunków
Język UML
Diagramy klas
Diagramy sekwencji
Podsumowanie
Rozdział 7. Czym są wzorce projektowe? Do czego się przydają?
Czym są wzorce projektowe?
Wzorzec projektowy
Nazwa
Problem
Rozwiązanie
Konsekwencje
Format wzorca według Bandy Czworga
Po co nam wzorce projektowe?
Wzorzec projektowy definiuje problem
Wzorzec projektowy definiuje rozwiązanie
Wzorce projektowe są niezależne od języka programowania
Wzorce definiują słownictwo
Wzorce są wypróbowane
Wzorce mają współpracować
Wzorce promują prawidła projektowe
Wzorce są stosowane w popularnych frameworkach
Wzorce projektowe a PHP
Podsumowanie
Rozdział 8. Wybrane prawidła wzorców
Olśnienie wzorcami
Kompozycja i dziedziczenie
Problem
Zastosowanie kompozycji
Rozprzęganie
Problem
Osłabianie sprzężenia
Kod ma używać interfejsów, nie implementacji
Zmienne koncepcje
Nadmiar wzorców
Wzorce
Wzorce generowania obiektów
Wzorce organizacji obiektów i klas
Wzorce zadaniowe
Wzorce korporacyjne
Wzorce baz danych
Podsumowanie
Rozdział 9. Generowanie obiektów
Generowanie obiektów — problemy i rozwiązania
Wzorzec Singleton
Problem
Implementacja
Konsekwencje
Wzorzec Factory Method
Problem
Implementacja
Konsekwencje
Wzorzec Abstract Factory
Problem
Implementacja
Konsekwencje
Prototyp
Problem
Implementacja
Ależ to oszustwo!
Podsumowanie
Rozdział 10. Wzorce elastycznego programowania obiektowego
Strukturalizacja klas pod kątem elastyczności obiektów
Wzorzec Composite
Problem
Implementacja
Konsekwencje
Composite — podsumowanie
Wzorzec Decorator
Problem
Implementacja
Konsekwencje
Wzorzec Facade
Problem
Implementacja
Konsekwencje
Podsumowanie
Rozdział 11. Reprezentacja i realizacja zadań
Wzorzec Interpreter
Problem
Implementacja
Ciemne strony wzorca Interpreter
Wzorzec Strategy
Problem
Implementacja
Wzorzec Observer
Implementacja
Wzorzec Visitor
Problem
Implementacja
Wady wzorca Visitor
Wzorzec Command
Problem
Implementacja
Podsumowanie
Rozdział 12. Wzorce korporacyjne
Przegląd architektury
Wzorce
Aplikacje i warstwy
Małe oszustwo na samym początku
Wzorzec Registry
Implementacja
Warstwa prezentacji
Wzorzec Front Controller
Wzorzec Application Controller
Wzorzec Page Controller
Wzorce Template View i View Helper
Warstwa logiki biznesowej
Wzorzec Transaction Script
Wzorzec Domain Model
Podsumowanie
Rozdział 13. Wzorce bazodanowe
Warstwa danych
Wzorzec Data Mapper
Problem
Implementacja
Wzorzec Identity Map
Problem
Implementacja
Konsekwencje
Wzorzec Unit of Work
Problem
Implementacja
Konsekwencje
Wzorzec Lazy Load
Problem
Implementacja
Konsekwencje
Wzorzec Domain Object Factory
Problem
Implementacja
Konsekwencje
Wzorzec Identity Object
Problem
Implementacja
Konsekwencje
Wzorce Selection Factory i Update Factory
Problem
Implementacja
Konsekwencje
Co zostało z wzorca Data Mapper?
Podsumowanie
Rozdział 14. Dobre (i złe) praktyki
Nie tylko kod
Pukanie do otwartych drzwi
Jak to zgrać?
Uskrzydlanie kodu
Dokumentacja
Testowanie
Ciągła integracja
Podsumowanie
Rozdział 15. PEAR i Pyrus
Czym jest PEAR?
Pyrus
Instalowanie pakietu
Kanały PEAR
Korzystanie z pakietu z PEAR
Obsługa błędów w pakietach PEAR
Tworzenie własnych pakietów PEAR
Plik package.xml
Składniki pakietu
Element contents
Zależności
Dookreślanie instalacji — phprelease
Przygotowanie pakietu do dystrybucji
Konfigurowanie własnego kanału PEAR
Podsumowanie
Rozdział 16. Generowanie dokumentacji — phpDocumentor
Po co nam dokumentacja?
Instalacja
Generowanie dokumentacji
Komentarze DocBlock
Dokumentowanie klas
Dokumentowanie plików
Dokumentowanie składowych
Dokumentowanie metod
Namespace support
Tworzenie odnośników w dokumentacji
Podsumowanie
Rozdział 17. Zarządzanie wersjami projektu z systemem Git
Po co mi kontrola wersji?
Skąd wziąć klienta Git?
Konfigurowanie serwera Git
Tworzenie repozytorium zdalnego
Rozpoczynamy projekt
Klonowanie repozytorium
Wprowadzanie i zatwierdzanie zmian
Dodawanie i usuwanie plików i katalogów
Dodawanie pliku
Usuwanie pliku
Dodawanie katalogu
Usuwanie katalogów
Etykietowanie wersji
Rozgałęzianie projektu
Podsumowanie
Rozdział 18. Testy jednostkowe z PHPUnit
Testy funkcjonalne i testy jednostkowe
Testowanie ręczne
PHPUnit
Tworzenie przypadku testowego
Metody asercji
Testowanie wyjątków
Uruchamianie zestawów testów
Ograniczenia
Atrapy i imitacje
Dobry test to oblany test
Testy dla aplikacji WWW
Przygotowanie aplikacji WWW do testów
Proste testy aplikacji WWW
Selenium
Słowo ostrzeżenia
Podsumowanie
Rozdział 19. Automatyzacja instalacji z Phing
Czym jest Phing?
Pobieranie i instalacja pakietu Phing
Montowanie dokumentu kompilacji
Różnicowanie zadań kompilacji
Właściwości
Typy
Operacje
Podsumowanie
Rozdział 20. Ciągła integracja kodu
Czym jest ciągła integracja?
Przygotowanie projektu do ciągłej integracji
Jenkins
Instalowanie Jenkinsa
Instalowanie rozszerzeń Jenkinsa
Konfigurowanie klucza publicznego serwera Git
Instalowanie projektu
Pierwsza kompilacja
Konfigurowanie raportów
Automatyzacja kompilacji
Podsumowanie
Rozdział 21. Obiekty, wzorce, narzędzia
Obiekty
Wybór
Hermetyzacja i delegowanie
Osłabianie sprzężenia
Zdatność do wielokrotnego stosowania kodu
Estetyka
Wzorce
Co dają nam wzorce?
Wzorce a zasady projektowe
Narzędzia
Testowanie
Dokumentacja
Zarządzanie wersjami
Automatyczna kompilacja (instalacja)
System integracji ciągłej
Co pominęliśmy?
Podsumowanie
Dodatek A. Bibliografia
Książki
Publikacje
Witryny WWW
Dodatek B. Prosty analizator leksykalny
Skaner
Analizator leksykalny
Skorowidz

Citation preview

Tytuł oryginału: PHP Objects, Patterns, and Practice, Fourth Edition Tłumaczenie: Przemysław Szeremiota ISBN: 978-83-246-9181-4 Original edition copyright © 2013 by Matt Zandstra All rights reserved. Polish edition copyright © 2014 by HELION SA. All rights reserved. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie praw autorskich niniejszej publikacji. Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich właścicieli. Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte w tej książce informacje były kompletne i rzetelne. Nie bierze jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Wydawnictwo HELION nie ponosi również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce. Wydawnictwo HELION ul. Kościuszki 1c, 44-100 GLIWICE tel. 32 231 22 19, 32 230 98 63 e-mail: [email protected] WWW: http://helion.pl (księgarnia internetowa, katalog książek) Pliki z przykładami omawianymi w książce można znaleźć pod adresem: ftp://ftp.helion.pl/przyklady/phpob4.zip Drogi Czytelniku! Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres http://helion.pl/user/opinie/phpob4_ebook Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.  Poleć książkę na Facebook.com

 Księgarnia internetowa

 Kup w wersji papierowej

 Lubię to! » Nasza społeczność

 Oceń książkę

Wszystko, co robię, robię dla Louise.

Spis treści

O autorze ................................................................................................................13 O recenzencie technicznym .....................................................................................15 Podziękowania .......................................................................................................17 Wprowadzenie .......................................................................................................19 Rozdział 1.

PHP — projektowanie i zarządzanie . .....................................................................21 Problem ................................................................................................................................................ 21 PHP a inne języki programowania . .................................................................................................. 22 O książce .............................................................................................................................................. 24 Obiekty .......................................................................................................................................... 24 Wzorce ........................................................................................................................................... 24 Narzędzia ...................................................................................................................................... 25 Nowości w czwartym wydaniu . ................................................................................................. 26 Podsumowanie .................................................................................................................................... 26

Rozdział 2.

Obiekty ...................................................................................................................27 Nieoczekiwany sukces obiektów w PHP . ........................................................................................ 27 PHP/FI — u zarania języka ........................................................................................................ 27 PHP3 — składniowy lukier ........................................................................................................ 27 Cicha rewolucja — PHP4 ........................................................................................................... 28 PHP5 — nieuchronne zmiany . .................................................................................................. 29 Debata obiektowa — za czy przeciw? . ............................................................................................. 30 Podsumowanie .................................................................................................................................... 30

Rozdział 3.

Obiektowy elementarz ...........................................................................................31 Klasy i obiekty ..................................................................................................................................... 31 Pierwsza klasa ............................................................................................................................... 31 Pierwszy obiekt (lub dwa) . ......................................................................................................... 32 Definiowanie składowych klasy . ....................................................................................................... 33 Metody ................................................................................................................................................. 35 Metoda konstrukcji obiektu . ...................................................................................................... 36

SPIS TREŚCI

Typy argumentów metod .................................................................................................................. 37 Typy elementarne ........................................................................................................................ 38 Typy obiektowe ............................................................................................................................ 40 Dziedziczenie ....................................................................................................................................... 42 Problemy związane z dziedziczeniem . ...................................................................................... 42 Stosowanie dziedziczenia . ........................................................................................................... 46 Zarządzanie dostępem do klasy — słowa public, private i protected . ................................. 50 Podsumowanie .................................................................................................................................... 54

Rozdział 4.

Zaawansowana obsługa obiektów . .......................................................................55 Metody i składowe statyczne . ............................................................................................................ 55 Składowe stałe ..................................................................................................................................... 58 Klasy abstrakcyjne .............................................................................................................................. 59 Interfejsy ............................................................................................................................................... 61 Cechy typowe ...................................................................................................................................... 62 Zadanie dla cech typowych ........................................................................................................ 62 Definiowanie i stosowanie cechy typowej . ............................................................................... 63 Stosowanie wielu cech typowych . .............................................................................................. 64 Łączenie cech z interfejsami . ...................................................................................................... 64 Unikanie kolizji nazw metod za pomocą słowa insteadof . .................................................... 65 Aliasy metod cech typowych . ..................................................................................................... 67 Cechy typowe z metodami statycznymi . .................................................................................. 68 Dostęp do składowych klasy włączającej . ................................................................................. 68 Definiowanie metody abstrakcyjnej cechy typowej . ............................................................... 69 Zmiana dostępności metod cech typowych . ............................................................................ 70 Późne wiązanie statyczne: słowo static . ........................................................................................... 71 Obsługa błędów ................................................................................................................................... 73 Wyjątki .......................................................................................................................................... 75 Klasy i metody finalne ........................................................................................................................ 80 Przechwytywanie chybionych wywołań . ......................................................................................... 81 Definiowanie destruktorów ............................................................................................................... 86 Wykonywanie kopii obiektów . ......................................................................................................... 87 Reprezentacja obiektu w ciągach znaków . ...................................................................................... 90 Wywołania zwrotne, funkcje anonimowe i domknięcia . .............................................................. 91 Podsumowanie .................................................................................................................................... 94

Rozdział 5.

Narzędzia obiektowe ..............................................................................................95 PHP a pakiety ...................................................................................................................................... 95 Pakiety i przestrzenie nazw w PHP . .......................................................................................... 95 Automatyczne wczytywanie kodu . .......................................................................................... 103 Klasy i funkcje pomocnicze . ............................................................................................................ 105 Szukanie klasy ............................................................................................................................. 106 Badanie obiektów i klas . ............................................................................................................ 107 Pozyskiwanie ciągu pełnej nazwy klasy . ................................................................................. 108 Badanie metod ............................................................................................................................ 108 Badanie składowych .................................................................................................................. 110 Badanie relacji dziedziczenia . ................................................................................................... 110 Badanie wywołań metod ........................................................................................................... 110

6

SPIS TREŚCI

Interfejs retrospekcji — Reflection API . ........................................................................................ 112 Zaczynamy .................................................................................................................................. 112 Pora zakasać rękawy .................................................................................................................. 112 Badanie klasy .............................................................................................................................. 114 Badanie metod ............................................................................................................................ 116 Badanie argumentów metod .................................................................................................... 117 Korzystanie z retrospekcji . ....................................................................................................... 118 Podsumowanie .................................................................................................................................. 121

Rozdział 6.

Obiekty a projektowanie obiektowe . ..................................................................123 Czym jest projektowanie? ................................................................................................................ 123 Programowanie obiektowe i proceduralne . .................................................................................. 124 Odpowiedzialność ..................................................................................................................... 127 Spójność ...................................................................................................................................... 127 Sprzęganie ................................................................................................................................... 127 Ortogonalność ............................................................................................................................ 128 Zasięg klas .......................................................................................................................................... 128 Polimorfizm ....................................................................................................................................... 129 Hermetyzacja ..................................................................................................................................... 131 Nieważne jak ...................................................................................................................................... 132 Cztery drogowskazy ......................................................................................................................... 132 Zwielokrotnianie kodu .............................................................................................................. 133 Przemądrzałe klasy .................................................................................................................... 133 Złota rączka ................................................................................................................................ 133 Za dużo warunków . ................................................................................................................... 133 Język UML ......................................................................................................................................... 133 Diagramy klas ............................................................................................................................. 134 Diagramy sekwencji .................................................................................................................. 139 Podsumowanie .................................................................................................................................. 141

Rozdział 7.

Czym są wzorce projektowe? Do czego się przydają? . ........................................143 Czym są wzorce projektowe? . ......................................................................................................... 143 Wzorzec projektowy ......................................................................................................................... 145 Nazwa .......................................................................................................................................... 145 Problem ....................................................................................................................................... 146 Rozwiązanie ................................................................................................................................ 146 Konsekwencje ............................................................................................................................. 146 Format wzorca według Bandy Czworga . ....................................................................................... 146 Po co nam wzorce projektowe? . ..................................................................................................... 147 Wzorzec projektowy definiuje problem . ................................................................................ 147 Wzorzec projektowy definiuje rozwiązanie . .......................................................................... 147 Wzorce projektowe są niezależne od języka programowania . ............................................ 147 Wzorce definiują słownictwo . .................................................................................................. 148 Wzorce są wypróbowane . ......................................................................................................... 148 Wzorce mają współpracować . .................................................................................................. 149 Wzorce promują prawidła projektowe . .................................................................................. 149 Wzorce są stosowane w popularnych frameworkach . ......................................................... 149 Wzorce projektowe a PHP .............................................................................................................. 149 Podsumowanie .................................................................................................................................. 150

7

SPIS TREŚCI

Rozdział 8.

Wybrane prawidła wzorców .................................................................................151 Olśnienie wzorcami .......................................................................................................................... 151 Kompozycja i dziedziczenie ............................................................................................................ 152 Problem ....................................................................................................................................... 152 Zastosowanie kompozycji ......................................................................................................... 155 Rozprzęganie ..................................................................................................................................... 157 Problem ....................................................................................................................................... 157 Osłabianie sprzężenia ................................................................................................................ 158 Kod ma używać interfejsów, nie implementacji . .......................................................................... 160 Zmienne koncepcje .......................................................................................................................... 161 Nadmiar wzorców ............................................................................................................................ 161 Wzorce ............................................................................................................................................... 161 Wzorce generowania obiektów . ............................................................................................... 162 Wzorce organizacji obiektów i klas . ........................................................................................ 162 Wzorce zadaniowe ..................................................................................................................... 162 Wzorce korporacyjne ................................................................................................................ 162 Wzorce baz danych .................................................................................................................... 162 Podsumowanie .................................................................................................................................. 162

Rozdział 9.

Generowanie obiektów ........................................................................................163 Generowanie obiektów — problemy i rozwiązania . .................................................................... 163 Wzorzec Singleton ............................................................................................................................ 167 Problem ....................................................................................................................................... 167 Implementacja ............................................................................................................................ 168 Konsekwencje ............................................................................................................................. 169 Wzorzec Factory Method ................................................................................................................ 170 Problem ....................................................................................................................................... 170 Implementacja ............................................................................................................................ 172 Konsekwencje ............................................................................................................................. 174 Wzorzec Abstract Factory ............................................................................................................... 174 Problem ....................................................................................................................................... 174 Implementacja ............................................................................................................................ 175 Konsekwencje ............................................................................................................................. 177 Prototyp .............................................................................................................................................. 178 Problem ....................................................................................................................................... 178 Implementacja ............................................................................................................................ 179 Ależ to oszustwo! .............................................................................................................................. 181 Podsumowanie .................................................................................................................................. 182

Rozdział 10. Wzorce elastycznego programowania obiektowego . ..........................................183 Strukturalizacja klas pod kątem elastyczności obiektów . ........................................................... 183 Wzorzec Composite ......................................................................................................................... 183 Problem ....................................................................................................................................... 184 Implementacja ............................................................................................................................ 186 Konsekwencje ............................................................................................................................. 189 Composite — podsumowanie . ................................................................................................. 191 Wzorzec Decorator ........................................................................................................................... 192 Problem ....................................................................................................................................... 192 Implementacja ............................................................................................................................ 194 Konsekwencje ............................................................................................................................. 197 8

SPIS TREŚCI

Wzorzec Facade ................................................................................................................................ 197 Problem ....................................................................................................................................... 197 Implementacja ............................................................................................................................ 199 Konsekwencje ............................................................................................................................. 199 Podsumowanie .................................................................................................................................. 200

Rozdział 11. Reprezentacja i realizacja zadań . .........................................................................201 Wzorzec Interpreter ......................................................................................................................... 201 Problem ....................................................................................................................................... 201 Implementacja ............................................................................................................................ 202 Ciemne strony wzorca Interpreter . ......................................................................................... 209 Wzorzec Strategy .............................................................................................................................. 209 Problem ....................................................................................................................................... 209 Implementacja ............................................................................................................................ 211 Wzorzec Observer ............................................................................................................................ 214 Implementacja ............................................................................................................................ 215 Wzorzec Visitor ................................................................................................................................ 220 Problem ....................................................................................................................................... 220 Implementacja ............................................................................................................................ 221 Wady wzorca Visitor ................................................................................................................. 225 Wzorzec Command .......................................................................................................................... 226 Problem ....................................................................................................................................... 226 Implementacja ............................................................................................................................ 226 Podsumowanie .................................................................................................................................. 230

Rozdział 12. Wzorce korporacyjne ............................................................................................231 Przegląd architektury ....................................................................................................................... 231 Wzorce ......................................................................................................................................... 232 Aplikacje i warstwy .................................................................................................................... 232 Małe oszustwo na samym początku . .............................................................................................. 235 Wzorzec Registry ....................................................................................................................... 235 Implementacja ............................................................................................................................ 236 Warstwa prezentacji ......................................................................................................................... 244 Wzorzec Front Controller ........................................................................................................ 244 Wzorzec Application Controller . ............................................................................................ 253 Wzorzec Page Controller .......................................................................................................... 264 Wzorce Template View i View Helper . .................................................................................. 268 Warstwa logiki biznesowej .............................................................................................................. 270 Wzorzec Transaction Script ..................................................................................................... 270 Wzorzec Domain Model ........................................................................................................... 274 Podsumowanie .................................................................................................................................. 277

Rozdział 13. Wzorce bazodanowe ............................................................................................279 Warstwa danych ................................................................................................................................ 279 Wzorzec Data Mapper ..................................................................................................................... 280 Problem ....................................................................................................................................... 280 Implementacja ............................................................................................................................ 280 Wzorzec Identity Map ..................................................................................................................... 293 Problem ....................................................................................................................................... 293 Implementacja ............................................................................................................................ 294 Konsekwencje ............................................................................................................................. 296 9

SPIS TREŚCI

Wzorzec Unit of Work ..................................................................................................................... 297 Problem ....................................................................................................................................... 297 Implementacja ............................................................................................................................ 297 Konsekwencje ............................................................................................................................. 301 Wzorzec Lazy Load ........................................................................................................................... 301 Problem ....................................................................................................................................... 301 Implementacja ............................................................................................................................ 302 Konsekwencje ............................................................................................................................. 303 Wzorzec Domain Object Factory . .................................................................................................. 303 Problem ....................................................................................................................................... 303 Implementacja ............................................................................................................................ 304 Konsekwencje ............................................................................................................................. 305 Wzorzec Identity Object .................................................................................................................. 306 Problem ....................................................................................................................................... 306 Implementacja ............................................................................................................................ 307 Konsekwencje ............................................................................................................................. 311 Wzorce Selection Factory i Update Factory . ................................................................................ 312 Problem ....................................................................................................................................... 312 Implementacja ............................................................................................................................ 312 Konsekwencje ............................................................................................................................. 315 Co zostało z wzorca Data Mapper? . ............................................................................................... 316 Podsumowanie .................................................................................................................................. 318

Rozdział 14. Dobre (i złe) praktyki ............................................................................................319 Nie tylko kod ..................................................................................................................................... 319 Pukanie do otwartych drzwi . .......................................................................................................... 320 Jak to zgrać? ....................................................................................................................................... 321 Uskrzydlanie kodu ............................................................................................................................ 322 Dokumentacja ................................................................................................................................... 323 Testowanie ......................................................................................................................................... 324 Ciągła integracja ................................................................................................................................ 325 Podsumowanie .................................................................................................................................. 325

Rozdział 15. PEAR i Pyrus .........................................................................................................327 Czym jest PEAR? .............................................................................................................................. 327 Pyrus ................................................................................................................................................... 328 Instalowanie pakietu ........................................................................................................................ 329 Kanały PEAR .............................................................................................................................. 331 Korzystanie z pakietu z PEAR ........................................................................................................ 333 Obsługa błędów w pakietach PEAR . ....................................................................................... 334 Tworzenie własnych pakietów PEAR . ........................................................................................... 337 Plik package.xml ........................................................................................................................ 337 Składniki pakietu ....................................................................................................................... 338 Element contents ....................................................................................................................... 339 Zależności .................................................................................................................................... 342 Dookreślanie instalacji — phprelease . .................................................................................... 343 Przygotowanie pakietu do dystrybucji . .................................................................................. 344 Konfigurowanie własnego kanału PEAR . .............................................................................. 345 Podsumowanie .................................................................................................................................. 348

10

SPIS TREŚCI

Rozdział 16. Generowanie dokumentacji — phpDocumentor . ................................................349 Po co nam dokumentacja? ............................................................................................................... 349 Instalacja ............................................................................................................................................ 350 Generowanie dokumentacji ............................................................................................................ 350 Komentarze DocBlock ..................................................................................................................... 352 Dokumentowanie klas ..................................................................................................................... 354 Dokumentowanie plików ................................................................................................................ 354 Dokumentowanie składowych ........................................................................................................ 355 Dokumentowanie metod ................................................................................................................. 357 Namespace support .......................................................................................................................... 357 Tworzenie odnośników w dokumentacji . ..................................................................................... 359 Podsumowanie .................................................................................................................................. 361

Rozdział 17. Zarządzanie wersjami projektu z systemem Git . ..................................................363 Po co mi kontrola wersji? ................................................................................................................ 363 Skąd wziąć klienta Git? .................................................................................................................... 364 Konfigurowanie serwera Git . .......................................................................................................... 365 Tworzenie repozytorium zdalnego . ........................................................................................ 365 Rozpoczynamy projekt .................................................................................................................... 367 Klonowanie repozytorium ........................................................................................................ 369 Wprowadzanie i zatwierdzanie zmian . .......................................................................................... 370 Dodawanie i usuwanie plików i katalogów . .................................................................................. 373 Dodawanie pliku ........................................................................................................................ 373 Usuwanie pliku .......................................................................................................................... 374 Dodawanie katalogu .................................................................................................................. 374 Usuwanie katalogów .................................................................................................................. 374 Etykietowanie wersji ......................................................................................................................... 375 Rozgałęzianie projektu ..................................................................................................................... 375 Podsumowanie .................................................................................................................................. 379

Rozdział 18. Testy jednostkowe z PHPUnit . .............................................................................381 Testy funkcjonalne i testy jednostkowe . ........................................................................................ 381 Testowanie ręczne ............................................................................................................................ 382 PHPUnit ............................................................................................................................................. 384 Tworzenie przypadku testowego . ............................................................................................ 384 Metody asercji ............................................................................................................................ 385 Testowanie wyjątków ................................................................................................................ 386 Uruchamianie zestawów testów . ............................................................................................. 387 Ograniczenia ............................................................................................................................... 388 Atrapy i imitacje ......................................................................................................................... 389 Dobry test to oblany test ........................................................................................................... 392 Testy dla aplikacji WWW ................................................................................................................ 394 Przygotowanie aplikacji WWW do testów . ........................................................................... 395 Proste testy aplikacji WWW . ................................................................................................... 396 Selenium ...................................................................................................................................... 398 Słowo ostrzeżenia .............................................................................................................................. 402 Podsumowanie .................................................................................................................................. 404

11

SPIS TREŚCI

Rozdział 19. Automatyzacja instalacji z Phing . ........................................................................405 Czym jest Phing? ............................................................................................................................... 406 Pobieranie i instalacja pakietu Phing . ............................................................................................ 406 Montowanie dokumentu kompilacji . ............................................................................................ 407 Różnicowanie zadań kompilacji . ............................................................................................. 408 Właściwości ................................................................................................................................ 410 Typy ............................................................................................................................................. 416 Operacje ...................................................................................................................................... 420 Podsumowanie .................................................................................................................................. 424

Rozdział 20. Ciągła integracja kodu ..........................................................................................425 Czym jest ciągła integracja? ............................................................................................................. 425 Przygotowanie projektu do ciągłej integracji . ....................................................................... 427 Jenkins ................................................................................................................................................ 436 Instalowanie Jenkinsa ................................................................................................................ 436 Instalowanie rozszerzeń Jenkinsa . ........................................................................................... 438 Konfigurowanie klucza publicznego serwera Git . ................................................................ 439 Instalowanie projektu ................................................................................................................ 439 Pierwsza kompilacja .................................................................................................................. 441 Konfigurowanie raportów . ....................................................................................................... 441 Automatyzacja kompilacji ........................................................................................................ 444 Podsumowanie .................................................................................................................................. 446

Rozdział 21. Obiekty, wzorce, narzędzia ...................................................................................447 Obiekty ............................................................................................................................................... 447 Wybór .......................................................................................................................................... 448 Hermetyzacja i delegowanie . .................................................................................................... 448 Osłabianie sprzężenia ................................................................................................................ 448 Zdatność do wielokrotnego stosowania kodu . ...................................................................... 449 Estetyka ....................................................................................................................................... 449 Wzorce ............................................................................................................................................... 450 Co dają nam wzorce? ................................................................................................................. 450 Wzorce a zasady projektowe . ................................................................................................... 451 Narzędzia ........................................................................................................................................... 452 Testowanie .................................................................................................................................. 453 Dokumentacja ............................................................................................................................ 453 Zarządzanie wersjami ................................................................................................................ 453 Automatyczna kompilacja (instalacja) . .................................................................................. 454 System integracji ciągłej ............................................................................................................ 454 Co pominęliśmy? ....................................................................................................................... 454 Podsumowanie .................................................................................................................................. 455

Dodatek A

Bibliografia ...........................................................................................................457 Książki ................................................................................................................................................ 457 Publikacje ........................................................................................................................................... 458 Witryny WWW ................................................................................................................................ 458

Dodatek B

Prosty analizator leksykalny .................................................................................461 Skaner ................................................................................................................................................. 461 Analizator leksykalny ....................................................................................................................... 468

Skorowidz .............................................................................................................481 12



O autorze

Matt Zandstra od przeszło dwóch dekad pracuje jako programista WWW, konsultant i pisarz. Był starszym programistą w Yahoo!  pracował w Londynie oraz w Dolinie Krzemowej. Obecnie pracuje jako niezależny konsultant i pisarz. Przed przebojowym PHP Objects Patterns and Practice napisał wydawaną trzykrotnie książkę Teach Yourself PHP in 24 Hours1 (wydawnictwa SAMS) i pomagał przy wydaniu DHTML Unleashed (również wydawnictwa SAMS). Zamieszczał też artykuły na łamach „Linux Magazine”, Zend.com, IBM DeveloperWorks oraz „php|architect Magazine”. Ponadto studiuje literaturę i pisze opowiadania science fiction. Jest absolwentem Manchester University oraz University of East Anglia. Kiedy akurat nie prowadzi studiów ani rozlicznych konsultacji u klientów z całego świata, wraz z żoną Louise i dwójką dzieci o imionach Holly i Jake mieszka w Liverpoolu.

1

Wydanie polskie: Poznaj PHP w 24 godziny, Infoland, 2001.

PHP. OBIEKTY, WZORCE, NARZĘDZIA

14



O recenzencie technicznym

Wes Hunt od początku lat 90. poprzedniego stulecia pracował jako programista aplikacji i lider projektowania interfejsu. Obecnie jest współzałożycielem i głównym programistą w Armigent, serwisie operującym w branży inwestycji w nieruchomości. Do szybkiego prototypowania i programowania usług WWW i REST używa języków PHP i Scala. Poza pracą zawodową zajmuje się zachęcaniem tysiąca dzieciaków z Montany do programowania w ramach inicjatywy CodeMontana.org i propaguje programowanie z ramienia grupy MontanaProgrammers.org.

PHP. OBIEKTY, WZORCE, NARZĘDZIA

16



Podziękowania

Jak zwykle, również i przy pracy nad tym wydaniem bardzo skorzystałem na wsparciu ze strony wielu osób. I jak zwykle, muszę się powołać na pochodzenie książki. Niektóre z pomysłów składających się na tę książkę wypróbowywałem na prelekcjach w Brighton, kiedy jeszcze wszyscy zachwycaliśmy się nowinkami zapowiadanymi wraz z PHP5. Dziękuję więc gospodarzowi prelekcji Andy’emu Buddowi, a także całej żywotnej społeczności programistycznej miasta Brighton. A obecny na prelekcji Jessey White-Cinis skontaktował mnie po raz pierwszy z Martinem Streicherem z wydawnictwa Apress. Po raz kolejny ekipa Apress wykazała się wielką uczynnością, zachęcając mnie i zapewniając wsparcie i wskazówki. Jestem wdzięczny za to, że mogłem korzystać z jej profesjonalizmu. Podziękowania i wyrazy miłości kieruję też do mojej żony Louise i do naszych dzieci Holly i Jake’a — niezmordowanie dostarczały mi odświeżających przerw w pracy. Dziękuję też Stevenowi Metskerowi za pozwolenie na wtórną realizację uproszczonej wersji interfejsu parsera, opublikowanej przez niego w książce Building Parsers in Java. Kiedy piszę, towarzyszy mi muzyka, i w poprzednich trzech wydaniach wspominałem w tym miejscu wybitnego DJ-a Johna Peela, mistrza eklektyzmu i undergroundu. Wypadałoby tu podziękować również nadawcom radiowym, którzy podjęli jego dzieło i raczyli mnie jego muzyką, a zwłaszcza tym z BBC 6 Music i Dandelion Radio.

PHP. OBIEKTY, WZORCE, NARZĘDZIA

18



Wprowadzenie

Kiedy wymyśliłem tę książkę, projektowanie obiektowe w PHP było tematem wielce ezoterycznym. W międzyczasie jednak doszło nie tylko do gwałtownego nasilenia się postrzegania PHP jako języka obiektowego, ale również do upowszechnienia się frameworków. Frameworki są, rzecz jasna, wielce użyteczne. Skutecznie przejmują zarządzanie „bebechami” wielu (a obecnie zapewne nawet większości) aplikacji WWW. A co więcej, w wielu przypadkach stanowią wcielenie zasad projektowych analizowanych i omawianych w tej książce. Dla programistów kryje się tu jednak zagrożenie, jak zresztą zawsze, kiedy ktoś wyręcza nas aż nadto. Nikt nie chce być zdegradowany z programisty do użytkownika cudzego kodu i w roli petenta czekać, aż niedosiężni guru-programiści frameworka naprawią zauważone błędy albo dodadzą oczekiwane funkcje. Z tej pozycji nie brakuje już dużo do sytuacji, w której framework z czarnej skrzynki przeradza się w czarną magię, jego działania nikt nie zna i nikt w nie nie wnika, a praca programisty aplikacji to tylko niewielki (a więc pewnie i nieznaczący) dodatek do wysiłku twórców frameworka. Nie zachęcam tu wcale do uporczywego wyważania otwartych drzwi: nie twierdzę, że wszyscy powinniśmy zarzucić frameworki i budować aplikacje MVC zupełnie od podstaw (w każdym razie nie zawsze). Chodzi raczej o to, że jako programiści powinniśmy rozumieć problemy rozwiązywane przez frameworki i stosowane w nich strategie rozwiązań. Powinniśmy być w stanie oceniać jakość frameworków nie tylko po zakresie oferowanych funkcji, ale też na podstawie decyzji projektowych podejmowanych przez ich twórców i na bazie jakości implementacji tych decyzji. I owszem, kiedy czas i okoliczności na to pozwalają, nie zaszkodzi zbudować od podstaw całkiem własną, choćby niewielką aplikację, a przy najróżniejszych okazjach montować własny zbiór kodu bibliotecznego. Mam nadzieję, że ta książka pozwoli programistom PHP zyskać ogląd zagadnień obiektowych nieodzownych w projektowaniu platform i bibliotek i wyposaży ich w narzędzia koncepcyjne niezbędne do wdrażania zdobytej wiedzy w praktyce, kiedy zajdzie taka konieczność. Ostatnio poświęciłem rok na powrót do szkoły. Jest to przedsięwzięcie, które z najróżniejszych powodów szczerze polecam każdemu. Jedną z zalet takiego powrotu jest nowa perspektywa, z którą wchodzi się w ten znajomy z przeszłości świat. Potem wróciłem do pracy i odkryłem, że większość moich klientów używa Gita (jego popularność skłoniła mnie do uwzględnienia go w książce), a wszyscy bez wyjątku twierdzą, że stosują zwinne metodologie wytwarzania oprogramowania. Jednak po moim powrocie do pracy aż trzech z czterech pierwszych klientów prosiło mnie o analizę i pomoc w przerabianiu naprędce wytwarzanego kodu. W każdym przypadku jeszcze przed rozpoczęciem właściwej pracy z kodem konieczne było zmontowanie zestawu testów jednostkowych, wytworzenie podstawowej dokumentacji i zaprojektowanie i wdrożenie automatycznego systemu budowania aplikacji. Znalazłem się więc w sytuacji intensywnego korzystania z praktyk i narzędzi omawianych w ostatniej części książki. Mam wielką nadzieję, że i czytelnicy uznają je za przydatne w tworzeniu solidnego i elastycznego oprogramowania.

PHP. OBIEKTY, WZORCE, NARZĘDZIA

20

ROZDZIAŁ 1 

PHP — projektowanie i zarządzanie Do najważniejszych cech języka PHP5 zaliczono rozszerzenie obsługi programowania obiektowego. Znacznie zwiększyło to zainteresowanie społeczności programistów języka PHP obiektami i projektowaniem. Doszło więc do intensyfikacji procesu zapoczątkowanego wraz z pojawieniem się PHP4, w którym nareszcie można było programować obiektowo. W niniejszym rozdziale przyjrzymy się wybranej klasie potrzeb realizowanych przez programowanie obiektowe. Streścimy też ewolucję wzorców projektowych i powiązanych z nimi praktyk w świecie języka Java. Zerkniemy również na symptomy tego samego procesu widoczne u programistów PHP. Naszkicujemy także zagadnienia, którym poświęcona jest reszta książki. Zajmiemy się:  Ewolucją katastrofy — kiedy projekt idzie „w maliny”.  Projektowaniem w PHP — czyli tym, jak w społeczności programistów PHP zadomawiają się obiektowe techniki programowania.  Zawartością książki — obiektami, wzorcami i narzędziami.

Problem Problem z PHP polega na jego nadmiernej prostocie. Prostota ta zbyt wcześnie satysfakcjonuje programistę zadowalającymi rezultatami, a w związku z tym zachęca do szybkiego wypróbowywania własnych idei. Kod piszesz wprost na stronach WWW — bo przecież PHP właśnie do tego służy. Dodajesz kolejne funkcje pomocnicze (np. realizujące dostęp do bazy danych) włączane do coraz większej liczby stron serwisu i zanim się spostrzeżesz, powstaje działająca aplikacja WWW. To jednak prosta droga ku przepaści. Krawędzi nie widać, bo przesłania ją świetny wygląd witryny. Wszystko działa, klienci są zadowoleni, a użytkownicy sięgają do portfeli. Kłopoty zaczynają się wraz z początkiem nowej fazy rozwoju projektu. Wiąże się ona z większym zespołem programistycznym, większą liczbą użytkowników i większym budżetem. Ale nagle wszystko zaczyna się psuć, jakby projekt został przeklęty. Nowi programiści nie mogą jakoś wdrożyć się do projektu — kod nie jest dla nich tak naturalny, jak był dla Ciebie, więc jego analiza jest dla nich uciążliwa i długo trzeba czekać, zanim osiągną pełnię efektywności. Prosta zmiana, której wprowadzenie powinno zająć nie więcej niż dzień roboczy, rozciąga się na trzy dni, bo okazuje się, że wymaga aktualizacji ponad dwudziestu stron WWW. Jeden z programistów zapisuje własną wersję pliku, zamazując wprowadzone przez Ciebie przed chwilą ważne zmiany. Strata nie ujawnia się przez kilka dni, w czasie których zdążyłeś już zmienić swoją lokalną kopię. Porządkowanie bałaganu zajmuje cały dzień, przy zaangażowaniu jeszcze trzeciego programisty, który również pracował w międzyczasie nad plikiem.

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Z racji popularności aplikacji trzeba ją przenieść na nowy serwer. Projekt wymaga ręcznej instalacji i okazuje się, że ścieżki dostępu do plików, nazwy baz danych i hasła są zaszyte w kodzie źródłowym i rozproszone po wielu plikach. Trzeba też wstrzymać rozwój projektu na czas przeprowadzki, aby nie zamazać zmian konfiguracyjnych wymuszonych migracją. To, co miało zająć dwie godziny, przeciąga się do godzin ośmiu, gdy okazuje się, że któryś z programistów wykorzystał w swoim module moduł ModRewrite serwera Apache i bez niego całość nie działa w ogóle. Wreszcie udaje się dotrzeć do fazy drugiej i wszystko zdaje się iść po naszej myśli. Mamy spokój przez półtora dnia. Pierwsze doniesienie o błędzie nadchodzi chwilę przed fajrantem — w drzwiach łapie Cię telefon od klienta. Zgłaszany przez niego błąd jest podobny do pierwszego, ale staranniejsza analiza pokazuje, że choć objawia się podobnie, ma zupełnie inną przyczynę. Przypominasz sobie drobną zmianę na początku wdrożenia, którą najwyraźniej trzeba było uwzględnić w całej reszcie kodu. Okazuje się, że tu i ówdzie zmian zabrakło. Może zostały przeoczone, a może zniesione w wyniku kolizji przy scalaniu różnych wersji plików. Tak czy inaczej trzeba jak najszybciej wprowadzić poprawki. Nie ma czasu na testowanie zmian, ale przecież poprawki polegają na kopiowaniu i wklejaniu kodu tu i ówdzie, więc trudno o błąd. Następnego ranka okazuje się, że moduł koszyka zakupowego nie działał przez całą noc. W gorączkowych i wprowadzanych na ostatnią chwilę zmianach zabrakło jednego znaku cudzysłowu, więc kod stał się bezużyteczny. Szkoda jedynie, że kiedy spałeś, druga półkula pełna potencjalnych klientów sklepu nie spała, ale i nie mogła wydać u Ciebie pieniędzy. Naprawiasz własny błąd, uspokajasz zdenerwowanych klientów i zbierasz zespół do następnego dnia gaszenia szalejącego w firmie pożaru. Taka wizja wydarzeń „z życia programisty” wydaje się być przesadzona, ale niestety często jest prawdziwa — wciąż widuję tego typu sytuacje. Wiele projektów PHP rozpoczyna bowiem swój żywot jako małe i proste aplikacje, nieprzystosowane u zarania do czekającej je ewolucji. Ponieważ w PHP mamy pokrywanie się logiki aplikacji z warstwą prezentacji (kod HTML jest generowany z poziomu języka PHP i odwrotnie — HTML wywołuje kod PHP), już na początku dochodzi do dublowania kodu poprzez powtarzanie na wszystkich stronach tych samych zapytań, testów uwierzytelniających i kodu przetwarzającego formularze. Wszelkie zmiany w kodzie muszą również być powtarzane w wielu miejscach, a niekonsekwencja w ich wprowadzaniu oznacza błędne działanie aplikacji. Brak dokumentacji utrudnia lekturę i analizę kodu, a brak testów pozwala na przenikanie błędów do fazy wdrożenia. Zmienna natura interesów klientów często wymusza szybkie zmiany projektu, który zaczyna odbiegać od pierwotnych założeń tak dalece, że przestaje się nadawać do realizacji stawianych przed nim kolejnych zadań. Wszystko to jest pozytywem dla niezależnego konsultanta-programisty języka PHP, który analizując i poprawiając tego rodzaju projekt, może zapewnić sobie niezły chleb na co najmniej kilka miesięcy. A mówiąc poważnie, wyliczane problemy mogą przesądzić o sukcesie albo porażce przedsięwzięcia komercyjnego.

PHP a inne języki programowania Fenomenalna popularność języka PHP oznacza równocześnie, że został on wszechstronnie przetestowany w całym zakresie zastosowań. Jak przekonamy się w następnym rozdziale, PHP rozpoczął żywot jako zestaw makrodefinicji służących do zarządzania witrynami domowymi. Wraz z nastaniem PHP3 i PHP4 język ten przekształcił się szybko w mechanizm napędowy nawet największych, korporacyjnych witryn WWW, ale do dziś dźwiga dziedzictwo wieku dziecięcego, zwłaszcza w zakresie podejścia do projektowania skryptów i zarządzania projektem. W pewnych kręgach z tego względu właśnie PHP zyskał niesławną reputację języka amatorów i hobbystów, w najlepszym przypadku nadającego się do zadań prezentacyjnych. Mniej więcej w tym czasie (w okolicach przełomu tysiącleci) w środowiskach programistów innych języków pojawiły się nowe prądy i idee. Projektowanie obiektowe najsilniejszy oddźwięk zyskało wśród programistów Javy. To zakrawa na oczywistość, bo język Java jest wybitnie obiektowy — jednak to, że język promuje programowanie obiektowe, nie oznacza wcale, że programista (nawet korzystający z klas i obiektów) realizuje projekt obiektowy. W latach siedemdziesiątych koncepcja wzorców projektowych pojawiła się jako metoda opisu problemów wraz z sednem ich rozwiązań. Co ciekawe, sam pomysł wywodzi się nie od informatyków, ale od architektów. Na początku lat dziewięćdziesiątych programiści będący zwolennikami obiektowości przyjęli technikę nazywania, identyfikowania i rozwiązywania problemów z dziedziny inżynierii oprogramowania. Nie da się tutaj przecenić wpływu książki tzw. Bandy Czworga (E. Gamma, R. Helm, R. Johnson i J. Vlissides) zatytułowanej

22

ROZDZIAŁ 1.  PHP — PROJEKTOWANIE I ZARZĄDZANIE

Design Patterns: Elements of Reusable Object-Oriented Software1, wydanej w roku 1995 i dziś stanowiącej klasykę tematu. Zawarte w tej pracy opisy wzorców to elementarz dla wszystkich przymierzających się do projektowania — nic więc dziwnego, że i w tej książce większość proponowanych wzorców to zapożyczenia z klasyki. Język Java wdrożył w swoim API szereg wzorców projektowych, ale dopiero w końcu lat dziewięćdziesiątych wzorce weszły wreszcie do świadomości ogółu programistów. Półki księgarni informatycznych wypełniły się publikacjami, a fora i grupy dyskusyjne przetrwały pierwsze wojny pomiędzy zwolennikami i przeciwnikami wzorców. Niezależnie od tego, czy sam uważasz wzorce projektowe za znakomity sposób komunikacji idei i pomysłów, czy też uważasz je za czczą gadaninę (z tytułu książki łatwo wywnioskujesz moją pozycję na tej skali), nie możesz zaprzeczyć, że przeniesienie nacisku z kodowania na projektowanie, charakterystyczne dla wzorców projektowych, samo w sobie jest korzystne. Na wadze zaczęły zyskiwać również zagadnienia i techniki pokrewne. Wśród nich znaczną popularność zdobyła koncepcja eXtreme Programming (XP) proponowana przez Kenta Becka. XP to podejście do realizacji projektów programistycznych zakładające elastyczność, obiektowość i ścisłe skupienie na planowaniu i realizacji projektu drobnymi i dobrze zdefiniowanymi etapami. Wśród zasad XP pierwszorzędną rolę gra nacisk na testowanie, które jest tu zasadniczym elementem dla powodzenia projektu. Testy mają być zautomatyzowane i najlepiej projektowane jeszcze przed przystąpieniem do implementacji danego fragmentu kodu projektu. XP mówi też, że projekty powinny być dzielone na niewielkie (wręcz bardzo małe) etapy. Podczas ich realizacji następuje ciągła konfrontacja kodu i wymagań. Cały czas trzeba też mieć na uwadze architekturę i projekt całości, co prowadzi do częstych rewizji implementacji. Jeśli przyjąć, że XP był zbrojnym ramieniem ruchu zwolenników projektowania, to za nurt umiarkowany należałoby uznać ten reprezentowany przez jedną z najlepszych książek, jakie udało mi się przeczytać — The Pragmatic Programmer Andrew Hunta i Davida Thomasa, opublikowaną w 2000 roku. XP był dla niektórych jedynie nurtem pobocznym, ale w istocie wyrósł on z dwóch dekad obiektowej praktyki, a jego zasady zostały szeroko zaakceptowane. Do koncepcji wzorców projektowych dopasowała się zwłaszcza rewizja kodu, znana też pod nazwą refaktoryzacji. Refaktoryzacja ewoluowała od lat osiemdziesiątych, ale skodyfikował ją dopiero Martin Fowler w książce Refactoring: Improving the Design of Existing Code2 opublikowanej w roku 1999. Z podobnie przychylnym przyjęciem spotkała się koncepcja wszechstronnego i wyczerpującego testowania. Znaczenie zautomatyzowanych testów zostało jeszcze podkreślone wydaniem platformy testowej JUnit, która stała się podstawowym wyposażeniem „zbrojowni” programisty języka Java. Przełomowym artykułem w tym temacie był Test Infected: Programmers Love Writing Tests Kenta Becka i Ericha Gammy (http://junit.sourceforge.net/doc/testinfected/testing.htm) — stanowiący znakomite wprowadzenie do zagadnienia i wciąż aktualny. Mniej więcej w tym czasie pojawił się PHP4, z ulepszoną wydajnością i — co ważniejsze — rozszerzeniami obiektowymi. Owe rozszerzenia dały możliwość realizacji w PHP prawdziwie obiektowych projektów. Programiści ochoczo przyjęli tę możliwość, ku niejakiemu zdziwieniu założycieli Zend, Zeeva Suraskiego i Andy’ego Gutmansa, którzy wsparli Rasmusa Lerdorfa w pracach nad rozwojem języka PHP. W następnym rozdziale przekonasz się, że obsługa obiektów PHP nie była bynajmniej doskonała, ale przy pewnej dyscyplinie i staranności stosowania składni można było poważnie myśleć o realizacji projektów obiektowych w języku PHP. Wszystko to nie zmniejszyło wcale częstotliwości katastrof podobnych do opisanej na początku rozdziału. Kultura projektowa jakoś nie zagrzała miejsca w książkach poświęconych PHP. Inaczej było w publikacjach sieciowych. Leon Atkinson w 2001 roku napisał dla Zend artykuł traktujący o stosowaniu wzorców w PHP, a w 2002 roku Harry Fuecks uruchomił swój znakomity magazyn publikowany pod adresem http://www.phppatterns.com (teraz już nieczynny). Zaczęły powstawać pierwsze projekty architektur i platform programistycznych bazujących na wzorcach, jak BinaryCloud; pojawiły się też narzędzia automatyzujące testowanie i tworzenie dokumentacji.

1 2

Wydanie polskie: Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku, Helion, 2010 — przyp. tłum. Wydanie polskie: Refaktoryzacja. Ulepszanie struktury istniejącego kodu, Helion, 2014 — przyp. tłum.

23

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Wydanie w roku 2003 piątej wersji języka jednoznacznie sklasyfikowało PHP jako język programowania obiektowego. Zend 2 Engine dysponuje znacznie poprawionymi mechanizmami obsługi obiektów. Równie ważne jak poprawienie obsługi programowania obiektowego jest jednak zasygnalizowanie społeczności programistów PHP nieodwołalnego połączenia obiektów i projektowania obiektowego z językiem PHP. Język PHP5 był stale rozwijany i ulepszany; w miarę upływu lat zyskiwał kolejne elementy i mechanizmy, jak przestrzenie nazw i domknięcia (ang. closure). A reputacja tego języka jako świetnego narzędzia do programowania serwerowej strony aplikacji WWW została utrwalona na dobre.

O książce Książka ta nie stanowi próby stworzenia nowych fundamentów projektowania obiektowego — w tym aspekcie podpieram się raczej autorytetem uznanych gigantów. Zamiast tego książka ma analizować pewne uznane zasady projektowania i kilka kluczowych wzorców projektowych (zwłaszcza tych opisanych w Design Patterns3, klasyce pióra Bandy Czworga) w kontekście języka PHP. W pewnym momencie porzucę też kod jako taki, zwracając się ku narzędziom i technikom pomocnym w pomyślnej realizacji projektu. Poza niniejszym wprowadzeniem i krótkim podsumowaniem kończącym książkę, całość została podzielona na trzy główne części traktujące odpowiednio o obiektach, wzorcach i narzędziach.

Obiekty Część 2. rozpocznie się krótkim przeglądem historii PHP i obiektów wraz z prześledzeniem ich przeobrażenia z dodatków do wersji PHP3 w zasadnicze elementy PHP5. Można być skutecznym programistą języka PHP, nie mając pojęcia o obiektach. Dlatego zaczniemy od wyjaśnienia zagadnień związanych z obiektami, klasami i dziedziczeniem. Już w ramach tego wprowadzenia zwrócę uwagę na niektóre rozszerzenia obiektowe wprowadzone w PHP5. Po przebrnięciu przez podstawy zagłębimy się we właściwą treść rozdziału, analizując wspólnie bardziej zaawansowane elementy obiektowe języka PHP. Jeden z rozdziałów poświęcony będzie narzędziom pomocnym w pracy z obiektami i klasami. Nie wystarczy jednak wiedzieć, jak zadeklarować klasę i jak wykorzystać ją do utworzenia jej egzemplarza. Trzeba najpierw wytypować właściwe elementy systemu, a potem zdecydować o ich wzajemnych interakcjach. Zasady podejmowania tych decyzji są znacznie trudniejsze w opisie i przyswojeniu niż suche fakty dotyczące składni i narzędzi obiektowych. Część 2. zostanie więc zakończona wprowadzeniem do projektowania obiektowego w PHP.

Wzorce Wzorce projektowe to opisy problemów programistycznych wraz z propozycjami ich rozwiązania. Rozwiązanie nie jest przy tym gotowym do wykorzystania kodem, gotową receptą z książki kucharskiej. Wzorzec projektowy opisuje raczej podejście, które należałoby zastosować celem rozwiązania danego problemu. Opisowi może towarzyszyć przykładowa implementacja, ale jest ona mniej ważna niż koncepcja, którą ilustruje. Część 3. rozpoczyna się od definicji wzorców projektowych i opisu ich struktury, jak również próby naświetlenia przyczyn ich popularności. Wzorce promują i równocześnie spełniają podstawowe zasady projektowania. Zrozumienie ich może pomóc w analizie przyczyny wyróżnienia wzorca i z pewnością pozwoli na stosowanie wzorców w programowaniu w ogóle. Niektóre z tych zasad doczekają się szerszego omówienia. Przy okazji zaprezentowany zostanie również język UML (Unified Modeling Language), stanowiący niezależną od platformy metodę opisu klas i interakcji pomiędzy nimi. 3

Wydanie polskie: Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku, Helion, 2010 — przyp. tłum.

24

ROZDZIAŁ 1.  PHP — PROJEKTOWANIE I ZARZĄDZANIE

Niniejsza książka nie jest bynajmniej katalogiem wzorców projektowych, ale znajdzie się miejsce na omówienie najsłynniejszych i najbardziej przydatnych. Omówienie będzie składać się z opisu problemu, którego wzorzec dotyczy, analizy proponowanego rozwiązania i prezentacji przykładowej implementacji wzorca w języku PHP.

Narzędzia Nawet najbardziej wyważona architektura załamie się kiedyś pod własnym ciężarem, jeśli zaniedbane zostanie właściwe zarządzanie jej elementami. W części 4. przyjrzymy się więc narzędziom pomocnym w tworzeniu szkieletu zwiększającego szansę powodzenia projektu. Tak jak reszta książki poświęcona jest praktyce projektowania i programowania, tak część 4. poświęcona jest praktyce zarządzania kodem. Prezentowane w niej narzędzia razem tworzą strukturę wspomagającą realizację projektów — pomagają w wyśledzeniu obserwowanych błędów, promują współpracę w zespole programistycznym, ułatwiają instalację kodu i zwiększają jego przejrzystość. Mówiliśmy już o wadze automatyzacji testów. Problematyką takich testów zajmiemy się w części 4., w ramach rozdziału wprowadzającego. Wielu programistów popełnia grzech poddawania się chęci samodzielnego kodowania wszystkiego, co jest możliwe. Tymczasem społeczność programistów PHP wytworzyła PEAR, repozytorium sprawdzonych pakietów nadających się do bezpośredniego wykorzystania we własnych projektach. Sprawdzimy, czy lepiej implementować pewną funkcję samodzielnie, czy może wdrożyć do projektu stosowny pakiet z repozytorium PEAR. Przy okazji omawiania zalet repozytorium PEAR zaprezentowany zostanie mechanizm sprowadzający instalację pakietu do pojedynczego polecenia. Mechanizm ten, sprawdzający się najlepiej z pojedynczymi i samodzielnymi pakietami, można wykorzystać do automatyzacji instalacji własnego kodu, co też zostanie pokazane w części 4. Tworzenie dokumentacji potrafi być wyjątkowo żmudne i jest zazwyczaj — obok testowania — pierwszą z czynności, które mogą spowodować niedotrzymanie terminów realizacji, a przez to wydłużenie harmonogramu całego projektu. Decyzja o tworzeniu dokumentacji od podstaw jest jednak swoistym błędem, zwłaszcza w obliczu dostępności pakietu PHPDocumentor — narzędzia kompletującego dokumentację projektu na podstawie komentarzy osadzonych w kodzie źródłowym. Otrzymuje ona postać zestawu dokumentów HTML, które są połączone odnośnikami i opisują poszczególne elementy API danego projektu. Niemal wszystkie narzędzia i techniki opisywane w książce odnoszą się do PHP wprost albo są w PHP implementowane. Jedynym wyjątkiem od tej reguły jest Git. Git to system kontroli wersji, koordynujący pracę wielu programistów nad tym samym kodem i koordynujący efekty ich pracy. Git pozwala na wyciągnięcie z repozytorium kodu migawki stanu projektu z dowolnego momentu jego rozwoju, sprawdzenie wprowadzonych zmian i ich autorstwa oraz wyodrębnienie nowej gałęzi rozwoju projektu. Prędzej czy później stosowanie Git-a okazuje się dla projektu zbawienne. Nie da się zaprzeczyć dwóm faktom: po pierwsze, błędy często pojawiają się ponownie w tych samych obszarach kodu, co niekiedy przyprawia programistę o uczucie déjà vu. Po drugie, poprawki i ulepszenia często psują tyle samo, co poprawiają, albo i więcej. Obu problemom można zaradzić za pomocą zautomatyzowanych testów, stanowiących element wczesnego ostrzegania przed problemami zaszytymi w kodzie. Przyjrzymy się więc mechanizmowi PHPUnit, będącemu wcieleniem tzw. platformy testowej xUnit, zaprojektowanej pierwotnie dla języka Smalltalk, ale przystosowanej potem do mnóstwa innych języków obiektowych, z Javą na czele. Zajmiemy się zarówno poszczególnymi cechami implementacji PHPUnit, jak i ogólnymi zaletami tego rodzaju zabezpieczenia metodą testów — nie pomijając przy tym zagadnienia kosztów. Repozytorium PEAR udostępnia narzędzie świetnie nadające się do instalowania samodzielnych pakietów. Przy wdrażaniu kompletnej i rozbudowanej aplikacji potrzebna jest większa elastyczność. Instalacja aplikacji nie jest zwykle prosta — trzeba instalować różne pliki w różnych niestandardowych miejscach, instalować i konfigurować bazy danych, łatać konfigurację serwera. Krótko mówiąc, instalacja aplikacji to masa pracy. Na szczęście jest Phing, wierny port znanego z Javy narzędzia Ant. Zarówno Phing, jak i Ant to interpretatory plików kompilacji (instalacji) przetwarzające pliki kodu źródłowego we wskazany sposób. Zazwyczaj przetwarzanie to sprowadza się do ich kopiowania z katalogów źródłowych do rozmaitych katalogów docelowych, ale to nie koniec ich możliwości. Testowanie i budowanie aplikacji to świetna sprawa, ale najpierw trzeba zainstalować i uruchomić testy, i to nie okazjonalne, ale regularne — jedynie wtedy będą coś warte. Jeśli system testów nie będzie automatyczny, narazimy się na zniechęcenie i czasem nawet nieświadome pomijanie bądź obniżanie rangi testów. Warto więc poznać narzędzia i techniki, które stosowane razem złożą się na system „integracji ciągłej”.

25

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Nowości w czwartym wydaniu PHP to żywy język i jako taki jest polem ciągłych zmian i korekt. Nowe wydanie książki zostało więc starannie przejrzane pod kątem zmian w języku i możliwości omówienia nowych elementów. W książce opisano nowe elementy języka, takie jak cechy typowe (ang. traits), klauzula finally w obsłudze wyjątków czy generatory, za pomocą których można prosto budować klasy obiektów iterowalnych. Od pierwszego wydania w książce nie brakowało tematu testów jednostkowych; to nie uległo zmianie, za to omówienie Selenium (interfejsu programistycznego i zestawu narzędzi do testowania stron WWW) zostało całkowicie przeredagowane z uwzględnieniem istotnych zmian i ulepszeń, które pojawiły się w międzyczasie. Aktualizacji doczekał się rozdział o kontroli wersji kodu źródłowego; pierwotnie bazował na systemie kontroli wersji Subversion, postanowiłem jednak przedstawić czytelnikom system Git. Zmiana ta odzwierciedla ogólny trend migracji w kierunku coraz to nowych platform, dający się łatwo zaobserwować na polu programowania aplikacji WWW niemal od zawsze. W książce znalazł się też rozdział o tzw. integracji ciągłej, w rozumieniu zarówno zestawu zasad, jak i narzędzi wspomagających realizację tej idei poprzez automatyzację i monitoring etapu budowania projektu i etapu testów. W poprzednim wydaniu omawiana była aplikacja o nazwie CruiseControl. Tym razem postanowiłem przedstawić system integracji Jenkins, który obecnie bryluje w prostocie użycia i cieszy się dużą popularnością w społeczności programistów PHP.

Podsumowanie To książka o obiektowym projektowaniu i programowaniu, jak i o narzędziach zarządzających kodem PHP, koordynujących współpracę członków zespołów programistycznych i pośredniczących we wdrożeniu gotowej aplikacji. Te dwa na pozór rozłączne zagadnienia w rzeczywistości odnoszą się do tego samego problemu, podchodząc do niego z odmiennych, choć uzupełniających się pozycji. Celem obu jest bowiem efektywne tworzenie systemów spełniających stawiane im zadania z naciskiem położonym na pracę zespołową. Drugorzędnym celem jest swego rodzaju estetyka oprogramowania. Jako programiści tworzymy systemy posiadające nie tylko zadania, ale i kształt. Na dbaniu o ich estetykę spędzamy wiele godzin i dni. Chcielibyśmy, aby tworzone przez nas narzędzia (czy to będą pojedyncze klasy i obiekty, czy moduły programowe, czy wreszcie kompletne produkty) były eleganckie i wygodne. Mechanizmy takie jak kontrola wersji, testowanie, dokumentacja i instalacja nie są bynajmniej wyłącznie środkami do osiągnięcia tego celu — one są kamieniami milowymi na drodze do niego. Celem jest posiadanie nie tylko przejrzystego i sprytnego kodu realizującego zadania aplikacji, ale i narzędzi swobodnego i skutecznego przetwarzania tego kodu. Mechanizmy współużytkowania, dokumentowania i wdrażania projektu powinny być równie ważne jak powstający w jego ramach kod.

26

ROZDZIAŁ 2 

Obiekty

Obiekty nie zawsze miały swoje miejsce w PHP. Ich obsługa była pierwotnie jedynie prowizorycznym dodatkiem. Co charakterystyczne dla prowizorek, ta okazała się wyjątkowo trwała. W niniejszym rozdziale podsumuję rozwój elementów obsługi programowania obiektowego w języku PHP. W rozdziale przyjrzymy się:  PHP/FI 2.0 — PHP innemu niż nam znany.  PHP3 — pierwszym mechanizmom obiektowym.  PHP4 — fazie dojrzewania programowania obiektowego w PHP.  PHP5 — obiektom w samym sercu języka.  PHP6 — podróży do przyszłości.

Nieoczekiwany sukces obiektów w PHP Przy współczesnej dostępności mnóstwa bibliotek obiektowych i obiektowych aplikacji, nie mówiąc już o zaawansowanych rozszerzeniach w obiektowym PHP5, miejsce obiektów w PHP zdaje się być jak najbardziej naturalne. Nic bardziej mylnego — pojawienie się obiektów w PHP bynajmniej nie było procesem naturalnym, a już na pewno nie nieuchronnym.

PHP/FI — u zarania języka Geneza języka PHP, jaki znamy dziś, tkwi w dwóch narzędziach opracowanych przez Rasmusa Lerdorfa w języku Perl. PHP oznaczało wówczas Personal Homepage Tools, a FI było skrótem od Form Interpreter. Razem stanowiły zestaw makrodefinicji wysyłających zapytania do baz danych przetwarzających formularze HTML i sterujących przepływem danych. Owe narzędzia zostały z czasem przepisane w języku C i przyłączone do pakietu o nazwie PHP/FI 2.0. Kształt języka z owego czasu różnił się znacznie od dzisiejszego, jednak nie we wszystkich aspektach. Język obsługiwał zmienne, tablice asocjacyjne i funkcje. Obiektów nie było jednak ani na lekarstwo.

PHP3 — składniowy lukier Obiektów nie było na horyzoncie nawet wtedy, kiedy w planach była już trzecia wersja PHP. Głównymi architektami PHP3 byli Zeev Suraski i Andi Gutmans. PHP3 zupełnie przemodelował PHP/FI 2.0, obiektów jednak w nowej składni nie uwzględniono.

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Wedle Zeeva Suraskiego obsługa klas została wprowadzona praktycznie po fakcie (stało się to 27 sierpnia 1997 roku). Klasy i obiekty były wtedy jedynie innymi sposobami definiowania tablic asocjacyjnych i ich używania. Oczywiście wprowadzenie metod i dziedziczenia uczyniło klasy narzędziem znacznie bardziej rozbudowanym od osławionych tablic asocjacyjnych, ale klasy te wciąż cierpiały na pewne ograniczenia funkcjonalne. W szczególności nie można było odwoływać się w klasie pochodnej do przesłoniętych metod klasy bazowej (kto nie wie jeszcze, co to oznacza, nie musi się martwić — rzecz wyjaśni się później). Innym ograniczeniem była nieoptymalna metoda przekazywania obiektów w skryptach PHP — zajmiemy się tym w następnym punkcie. Obiekty były w tym kształcie mechanizmami marginalnymi, czego dowodzi potraktowanie ich po macoszemu w oficjalnej dokumentacji. Podręcznik języka poświęcał tej kwestii jedno zdanie i jeden przykładowy kod. Przykład ten zresztą nie ilustrował ani dziedziczenia, ani korzystania z własności obiektu.

Cicha rewolucja — PHP4 Kolejnym milowym krokiem w rozwoju języka była wersja 4, choć większość zmian uzasadniających takie postrzeganie jej znaczenia zaszła „pod podszewką”. Język został zaimplementowany od nowa w postaci mechanizmu Zend (od nazwisk Zeeva i Andiego). Zend Engine to jeden z najważniejszych komponentów napędowych języka PHP. Każda funkcja PHP jest w istocie elementem wysokopoziomowej warstwy rozszerzającej; funkcje te implementowane w owej warstwie zajmują się tym, czego oczekuje od nich użytkownik: komunikacją z bazą danych, maltretowaniem ciągów znaków itp. Pod spodem natomiast działa Zend Engine, zajmujący się zarządzaniem pamięcią, przekazywaniem sterowania pomiędzy komponentami i tłumaczeniem znanej każdemu programiście przyjemnej składni PHP na kod bajtowy zdatny do wykonania. Jeśli język wzbogaca się o rdzenne, zaawansowane mechanizmy, jak np. klasy, dzieje się to na poziomie implementacji Zend Engine. Z naszej — obiektowej — perspektywy najważniejszą zmianą wdrożoną w PHP4 była możliwość przesłaniania metod klas bazowych i odwoływania się do nich z klas pochodnych. Nie wyeliminowano jednak głównej wady — wciąż przypisanie obiektu do zmiennej, przekazanie go do funkcji i zwrócenie z metody powodowało wykonanie kopii obiektu. Przypisanie w rodzaju poniższego: $my_obj = new User('bob'); $other = $my_obj;

prowokowało więc utworzenie w programie zamiast dwóch referencji tego samego obiektu — dwóch obiektów klasy User. W większości obiektowych języków programowania w takim przypisaniu spodziewalibyśmy się przypisania przez referencję, a nie przez wartość. Chodzi o uzyskanie możliwości przypisywania i przekazywania do funkcji nie tyle samych obiektów i ich kopii, co ich referencji, czyli uchwytów czy też wskaźników. Przyjęcie domyślnego przekazywania przez wartość prowokuje wiele mało oczywistych błędów wynikających z niemożności odzwierciedlenia zmian obiektów w różnych częściach skryptu. W dalszej części książki niejeden raz będziemy zaś wykorzystywać w przykładach wiele referencji tego samego obiektu. Na szczęście programista mógł wymusić przekazanie przez referencję, jeśli pamiętał o stosowaniu dość niezgrabnej konstrukcji. Przypisanie przez referencję wyglądało tak: $other =& $my_obj; // $other i $my_obj odnoszą się do tego samego obiektu.

Przekazanie przez referencję następująco: function setSchool(& $school) { // $school jest tutaj referencją przekazanego obiektu, a nie jego kopią. }

Zwracać obiekty przez referencję można było zaś tak: function & getSchool() { // Zwrócenie referencji obiektu zamiast jego kopii: return $this->school; }

28

ROZDZIAŁ 2.  OBIEKTY

Działało to nawet nie najgorzej, ale o znaku & łatwo było zapomnieć, przez co kod obiektowy był bardzo podatny na błędy. Szczególnie że błędy te były trudne w diagnozowaniu, gdyż rzadko prowokowały stosowne komunikaty — najczęściej objawiały się jedynie dziwacznym zachowaniem programu. Tak czy inaczej w oficjalnym podręczniku programowania rozszerzono znacznie opis składni obiektowej, a programowanie obiektowe zaczęło przenikać do głównego nurtu. Stosowanie obiektów w PHP wciąż budziło kontrowersje (tak jak i dziś) i łatwo było na forach dyskusyjnych o zaciekłe kłótnie prowokowane pytaniem: „Po co mi obiekty?”. Nawet witryna Zend zaczęła publikować artykuły zachęcające do programowania obiektowego obok artykułów pobrzmiewających raczej jako ostrzeżenia przed nimi. Nie zważając na trudności przekazywania przez referencję i kontrowersje wokół samej koncepcji obiektowości, programiści zaczęli szpikować kod znakami &. Obiektowe zastosowania PHP rosły w siłę. Ostatnio Zeev Suraski napisał w artykule dla witryny DevX.com (http://www.devx.com/webdev/Article/10007/0/page/1): „Jednym z dziwniejszych wydarzeń w historii języka PHP było to, że mimo bardzo niewielkiej funkcjonalności i w obliczu masy problemów i ograniczeń programowanie obiektowe w języku PHP miało się dobrze i urosło do rangi najpopularniejszego paradygmatu programowania w coraz większej liczbie aplikacji. Ów prawie zupełnie nieoczekiwany trend postawił PHP w nieciekawej sytuacji — widać było jasno, że obiekty w PHP nie zachowują się jak obiekty w innych językach obiektowych, zachowując się za to jak tablice asocjacyjne”. W poprzednim rozdziale wspominałem, że nurt zainteresowania projektowaniem obiektowym zdominował swego czasu witryny i publikacje sieciowe. Model obiektowy zaadaptowało nawet PEAR, oficjalne repozytorium języka PHP — wśród pakietów rozszerzających funkcjonalność PHP można znaleźć znakomite przykłady implementacji obiektowych wzorców projektowych. Z perspektywy czasu łatwo uznać, że przyjęcie mechanizmów obiektowych do rdzenia PHP stanowiło niechętną kapitulację przed siłą wyższą. Trzeba jednak pamiętać, że choć programowanie obiektowe jest w użyciu od lat sześćdziesiątych poprzedniego stulecia, faktyczną pozycję zdobyło sobie dopiero w połowie lat dziewięćdziesiątych. Wielkim popularyzatorem koncepcji obiektowych był język Java, który przecież pojawił się dopiero w 1995 roku. C++ — obiektowe rozszerzenie proceduralnego języka C — był niby w użyciu od 1979 roku, ale dopiero pod koniec wieku można było mówić o faktycznej karierze C++. To samo dotyczy Perla w wersji 5, wydanego w 1994 roku, a stanowiącego kolejny przykład przejścia z modelu proceduralnego na obiektowy (choć wielu uważa, że obiektowość w Perlu jest jednak „wpasowana” do języka na siłę). Jak na prościutki język proceduralny PHP doczekał się elementów obiektowych wręcz zaskakująco szybko, wykazując się faktyczną elastycznością i wrażliwością na potrzeby użytkowników.

PHP5 — nieuchronne zmiany W PHP5 w sposób jawny przyjęto już kierunek obiektowy. Ale wcale nie oznacza to, że nie da się już programować w PHP inaczej niż obiektowo (i bynajmniej nie tego ma dowodzić ta książka). Obiekty są jednak współcześnie efektywnymi i ważnymi środkami programowania systemów korporacyjnych i PHP obsługuje je już nie „przy okazji” — obsługa ta stanowi element rdzenia języka. Wydaje się, że jednym z istotniejszych efektów rozszerzania języka PHP w wersji 5 jest zaakceptowanie języka również w większych firmach internetowych. Z PHP korzystają w swoich platformach chociażby tacy giganci jak Yahoo! czy Facebook. Wraz z wersją 5 PHP stał się jednym z równoprawnych języków tworzenia aplikacji internetowych również w klasie enterprise. Obiekty awansowały z dodatku na podstawowy element języka. Najważniejszą chyba zmianą w PHP5 było zniesienie domyślnego przekazywania przez wartość na rzecz przekazywania przez referencję. Ale zmiany na tym się nie skończyły. W dalszej części książki niejednokrotnie będzie się można natknąć na kolejne zmiany rozszerzające i ulepszające obsługę obiektów w PHP, z wymuszaniem typów argumentów (ang. type hinting), rozdzieleniem składowych klas na publiczne i prywatne, słowem kluczowym static, przestrzeniami nazw i wyjątkami włącznie. PHP pozostaje jednak wciąż raczej językiem obsługującym programowanie obiektowe niż prawdziwym językiem programowania obiektowego. Jednak zakres obsługi programowania obiektowego jest już wystarczający, aby usprawiedliwiał poświęcenie mu książek takich jak niniejsza, kładących nacisk na projektowanie wyłącznie obiektowe. Świeżo po wydaniu PHP 5.5, w momencie przygotowywania tego wydania, nie było jeszcze na widoku następnej „przełomowej” (choćby psychologicznie) wersji 6. Andi Gutmans pytany o plany wydawnicze PHP6

29

PHP. OBIEKTY, WZORCE, NARZĘDZIA

w wywiadzie z roku 2012 (http://venturebeat.com/2012/10/24/zends-andi-gutmans-on-php-6-being-a-developerceo-and-how-apple-is-the-biggest-barrier-to-the-future-of-mobile/) powiedział: Nie mamy w tej chwili określonego planu — społeczność programistów PHP nie zawsze działa według kalendarza. Pracujemy nad 5.5, ale decyzja o zamknięciu wydania 5.5 czy też 6 będzie zależna od liczby i zakresu dodanych rozszerzeń. Na PHP6 czekamy już bodaj od trzech (z czterech) wydań niniejszej książki, ale „numerologia” ma drugorzędne znaczenie, skoro mechanizmy i rozszerzenia zapowiadane i wyczekiwane w tej wersji trafiają stopniowo do kolejnych głównych wydań PHP5. Na przykład w PHP 5.3 otrzymaliśmy przestrzenie nazw. Przestrzenie nazw umożliwiają definiowanie zasięgów widoczności nazw dla klas i funkcji, co pozwala na uniknięcie powielania nazw, zwłaszcza jeśli w projekcie korzystamy z wielkiej liczby bibliotek zewnętrznych. Przestrzenie nazw ratują też przed nieładnymi, a dotychczas koniecznymi konwencjami nazewniczymi w rodzaju: class megaquiz_util_Conf { }

Takie nazwy klas były dobierane właśnie pod kątem minimalizacji ryzyka kolizji z nazwami klas z innych modułów systemu. Ich wymyślanie i stosowanie jest jednak wybitnie uciążliwe. Z innych rozszerzeń warto wymienić chociażby domknięcia (ang. closures), generatory, cechy typowe (ang. traits) czy mechanizm późnego wiązania statycznego (ang. late static binding). Obecnie (kiedy piszę te słowa) wciąż nie wzięto się na poważnie za sygnalizowanie typu zwracanego funkcji. Taki mechanizm pozwoliłby na deklarowanie w metodzie bądź funkcji typu obiektu zwracanego. Zobowiązanie do utrzymania zadeklarowanego typu byłoby wymuszane przez sam rdzeń języka. Deklarowane typy wartości zwracanych pozwolą jeszcze bardziej ulepszyć implementację zasad projektowych w języku PHP (takich jak: „koduj pod kątem interfejsu, a nie pod kątem implementacji”). Mam nadzieję, że któregoś dnia będę miał okazję publikować wydanie opisujące tę nowość!

Debata obiektowa — za czy przeciw? Obiekty i programowanie obiektowe zdają się wciąż wywoływać mnóstwo emocji po obu stronach barykady — tak wśród zwolenników, jak i przeciwników. Wielu znakomitych programistów od lat tworzy świetne oprogramowanie, nie korzystając z obiektów, i nie wolno zapominać, że PHP wciąż najlepiej sprawdza się w proceduralnym programowaniu aplikacji WWW. W oczywisty sposób niniejsza książka prezentuje odchylenie proobiektowe, nieuniknione ze względu na osobiste przekonanie jej autora. Ponieważ książka ta jest pochwałą obiektów, a przy okazji wprowadzeniem do obiektowego projektowania, nie ma mowy o zachowaniu obiektywności. W żadnym jednak razie nie jest moim zamiarem sugerowanie Czytelnikowi, że obiekty to jedyna możliwość skutecznego kodowania w PHP. Podczas lektury książki należy pamiętać o słynnym motcie języka Perl: „Da się to zrobić inaczej”. Hasło to będzie prawdziwe zwłaszcza w odniesieniu do prostszych skryptów, w których ważniejsze jest szybkie wdrożenie w życie pewnego pomysłu (tego rodzaju prototypy noszą w nomenklaturze eXtreme Programming miano „pinezek”, ang. spikes) niż pieczołowite konstruowanie struktury, którą da się potem skutecznie skalować do postaci systemu rozleglejszego niż teraz projektowany. Kod to bardzo elastyczne medium. Sztuka w tym, aby rozpoznać moment, w którym ów prosty prototyp staje się rdzeniem rozleglejszego projektu, i odpowiednio szybko powstrzymać wymuszanie zmian projektowych z powodu bezwładności kodu. Po podjęciu decyzji o zastosowaniu podejścia projektowego łatwo znaleźć książki zachęcające do projektowania proceduralnego, które sprawdza się znakomicie w wielu różnych kategoriach projektów. Ta książka będzie jednak zachęcać do projektowania obiektowego. I mam nadzieję, że będzie stanowić choćby dobry punkt wyjścia dla Czytelnika.

Podsumowanie Niniejszy krótki rozdział prezentował historię obiektów w języku PHP. Przyszłość tego języka jako zbliżającego się do języków obiektowych jest już raczej przesądzona. W następnych kilku rozdziałach omówię obecny stan obsługi obiektów w PHP; wprowadzę też Czytelnika w podstawowe zagadnienia projektowe.

30

ROZDZIAŁ 3 

Obiektowy elementarz

Obiekty i klasy stanowią oś tej książki; od momentu wprowadzenia PHP5 stanowią również oś języka PHP. Niniejszy rozdział ma przygotować niezbędny grunt dla pogłębionego omówienia obiektów i projektowania obiektowego w kontekście mechanizmów języka PHP. W piątej wersji PHP doszło do radykalnego polepszenia mechanizmów obiektowości, więc w niniejszym rozdziale liczne nowinki odnajdą również czytelnicy znający już PHP4. Ci zaś, którym programowanie obiektowe w tym języku jest całkiem obce, powinni przeczytać ten rozdział bardzo uważnie. Omawiam w nim:  Klasy i obiekty — deklarowanie klas i tworzenie ich egzemplarzy (obiektów).  Metody konstrukcji — automatyzację konfiguracji obiektów.  Typy elementarne i klasy — jakie znaczenie ma typ w PHP.  Dziedziczenie — gdzie się przydaje i jak je stosować.  Widoczność — udostępnianie interfejsów klas i zabezpieczanie metod i składowych obiektów przed ingerencją.

Klasy i obiekty Pierwszą przeszkodą na drodze do zrozumienia programowania obiektowego jest dziwaczność i niezwykłość relacji pomiędzy klasą a jej obiektami. Dla wielu osób właśnie pojęcie tej relacji stanowi pierwsze olśnienie, wywołuje pierwszą ekscytację programowaniem obiektowym. Nie skąpmy więc energii na poznawanie podstaw.

Pierwsza klasa Klasy są często opisywane w odniesieniu do obiektów. To bardzo ciekawe, ponieważ obiekty są z kolei niejednokrotnie opisywane przez pryzmat klas. Ta zależność bardzo spowalnia pierwsze postępy adeptów programowania obiektowego. Ponieważ to klasy definiują obiekty, zaczniemy od definicji klasy. Krótko mówiąc, klasa to swego rodzaju szablon wykorzystywany do generowania obiektów. Deklaracja klasy zawiera słowo kluczowe class i dowolnie wybraną nazwę klasy. Nazwa klasy może być dowolną kombinacją cyfr i liter, nie może się jednak od cyfry zaczynać. Kod skojarzony z klasą musi być ograniczony nawiasami klamrowymi. Spróbujmy na podstawie tych informacji skonstruować klasę: class ShopProduct { // ciało klasy }

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Zdefiniowana właśnie klasa ShopPoduct jest prawidłową klasą, choć jej przydatność jest na razie znikoma. Mimo to osiągnęliśmy coś bardzo znaczącego, bo zdefiniowaliśmy typ. Utworzyliśmy więc kategorię danych, którą możemy wykorzystywać w skryptach. Znaczenie tego faktu stanie się niebawem jaśniejsze.

Pierwszy obiekt (lub dwa) Skoro klasa jest szablonem generowania obiektów, obiekt stanowią dane, które zostały zaaranżowane zgodnie z szablonem definiowanym w klasie. Obiekt zwie się egzemplarzem klasy bądź jej konkretyzacją. Klasa definiuje jego typ. Wykorzystamy teraz klasę ShopProduct jako formę do generowania obiektów typu ShopProduct. Pomoże nam w tym operator new. Operator new jest zazwyczaj stosowany w połączeniu z nazwą klasy, jak tutaj: $product1 = new ShopProduct(); $product2 = new ShopProduct();

Operator new, jeśli zostanie wywoływany z nazwą klasy w roli swojego jedynego operandu, generuje egzemplarz tejże klasy. W naszym przykładzie generuje obiekt klasy ShopProduct. Klasę ShopProduct wykorzystaliśmy do wygenerowania dwóch obiektów typu ShopProduct. Choć funkcjonalnie $product1 i $product2 są identyczne (tzn. puste), stanowią obiekty rozłączne, a ich wspólnym mianownikiem jest jedynie wspólna klasa, na podstawie której zostały wygenerowane. Istnienie klas i obiektów można sprowadzić do następującej analogii: klasa to jakby maszyna tłocząca gumowe kaczki. Obiektami zaś są owe gumowe kaczki produkowane przy użyciu maszyny. Ich „typ” jest wyznaczany formą, w której są odciskane. Wszystkie wypluwane z maszyny kaczki wyglądają więc identycznie, ale są niezależnymi od siebie obiektami materialnymi. Innymi słowy, są różnymi egzemplarzami pewnego przedmiotu. Aby rozróżnić poszczególne kaczki, można by im zresztą podczas wytłaczania nadawać numery seryjne. Każdy obiekt tworzony w języku PHP również posiada odrębną tożsamość, jednak unikatową jedynie w okresie życia danego obiektu (PHP ponownie wykorzystuje nieużywane już identyfikatory obiektów w obrębie tego samego procesu). Można się o tym przekonać, wydrukowując obiekty $product1 i $product2: var_dump($product1); var_dump($product2);

Wykonanie powyższego kodu spowoduje wypisanie na wyjściu: object(ShopProduct)#1 (0) { } object(ShopProduct)#2 (0) { }

 Uwaga W PHP4 i PHP5 do wersji 5.1 można wypisywać zawartość obiektu wprost. Taka operacja powoduje zrzutowanie obiektu na ciąg znaków zawierający identyfikator obiektu. Od PHP 5.2 język został pozbawiony tej automagicznej konwersji, więc traktowanie obiektu jako ciągu znaków jest błędem, chyba że obiekt definiuje metodę __toString(). Metodami obiektów, w tym metodą __toString(), zajmiemy się w rozdziale 4.

Przekazanie obiektu do wywołania var_dump() pozwala wypisać ciekawe dane o obiekcie, z identyfikatorem obiektu na czele. Aby klasę ShopProduct uczynić ciekawszą, możemy uzupełnić ją o obsługę specjalnych pól danych, zwanych składowymi bądź właściwościami (ang. properties).

32

ROZDZIAŁ 3.  OBIEKTOWY ELEMENTARZ

Definiowanie składowych klasy Klasy mogą definiować specjalne zmienne zwane właściwościami bądź składowymi. Składowa przechowuje dane, które różnią się pomiędzy egzemplarzami danej klasy. W przypadku obiektów klasy ShopProduct (niech będzie to asortyment księgarski, choć ogólnie chodzi o dowolne artykuły) możemy na przykład zażyczyć sobie obecności pól ceny (price) i tytułu (title). Składowa klasy przypomina zwykłą zmienną, z tym że przy deklarowaniu składowej jej nazwę trzeba poprzedzić słowem kluczowym określającym widoczność. Może być nim public, protected albo private. Wybór słowa kluczowego widoczności określa zasięg, w którym możliwe będzie realizowanie odwołań do składowych.  Uwaga Zasięg to inaczej kontekst (klasy czy funkcji), w ramach którego zmienna posiada znaczenie (to samo dotyczy metod, o których więcej w dalszej części rozdziału). Zmienna zdefiniowana wewnątrz funkcji istnieje jedynie lokalnie, a zmienna definiowana poza funkcją istnieje globalnie. Należy przyjąć, że nie istnieje możliwość odwoływania się do danych definiowanych w zasięgu, który jest bardziej lokalny niż bieżący. Po zdefiniowaniu zmiennej wewnątrz funkcji nie można się do niej później odwołać spoza tejże funkcji. Obiekty są w tym względzie bardziej przenikalne — niektóre z ich zmiennych mogą być niekiedy dostępne z innych kontekstów. Ową dostępność reguluje się słowami public, protected i private.

Do kwestii widoczności i regulujących ją słów kluczowych wrócimy później. Na razie spróbujemy po prostu zadeklarować kilka składowych klasy za pomocą słowa kluczowego public: class ShopProduct { public $title public $producerMainName public $producerFirstName public $price }

= = = =

"bez tytułu"; "nazwisko"; "imię"; 0;

Jak widać, uzupełniliśmy klasę o cztery składowe, przypisując do każdej z nich wartość domyślną. Wszelkie obiekty konkretyzowane na bazie takiej klasy będą teraz zawierać owe dane domyślne. Słowo kluczowe public poprzedzające deklarację każdej składowej umożliwia odwoływanie się do niej spoza kontekstu obiektu.  Uwaga Słowa określające widoczność (public, private i protected) zostały wprowadzone dopiero w piątej wersji PHP. Czytelnicy korzystający z PHP4 nie uruchomią więc prezentowanych przykładów. W PHP4 wszystkie składowe obiektów deklarowało się ze słowem kluczowym var, którego znaczenie jest identyczne z dzisiejszym public. Dostęp do składowych obiektów regulują niniejszym słowa public, private i protected, z których nieraz skorzystamy.

Do składowych definiowanych w obrębie klasy, a konkretyzowanych w obiektach możemy się odwoływać za pośrednictwem operatora dostępu do składowej -> kojarzącego zmienną obiektu i nazwę składowej: $product1 = new ShopProduct(); print $product1->title; bez tytułu

Ponieważ składowe zostały zdefiniowane jako publiczne (public), możemy odczytywać ich wartości i je do nich przypisywać, zmieniając domyślne stany obiektów definiowane w klasie: $product1 = new ShopProduct(); $product2 = new ShopProduct(); $product1->title = "Moja Antonia"; $product2->title = "Paragraf 22";

33

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Deklarując i ustawiając w klasie ShopProduct składową $title, wymuszamy podaną początkową wartość tej składowej we wszystkich nowo tworzonych obiektach klasy. Oznacza to, że kod użytkujący klasę może bazować na domniemaniu obecności tej składowej w każdym z obiektów klasy. Nie może już jednak domniemywać wartości składowych, gdyż te — jak wyżej — mogą się w poszczególnych obiektach różnić między sobą.  Uwaga Kod wykorzystujący klasę, funkcję bądź metodę nazwiemy kodem klienckim wobec tej klasy, metody czy funkcji, albo po prostu klientem klasy (metody, funkcji). Termin „klient” będzie się więc w tej książce pojawiał stosunkowo często.

Zresztą PHP nie zmusza nas do deklarowania wszystkich składowych w klasie. Obiekty można uzupełniać składowymi dynamicznie, jak tutaj: $product1->arbitraryAddition = "nowość";

Taka metoda uzupełniania obiektów o składowe nie jest jednak zalecana w programowaniu obiektowym i prawie nigdy nie jest stosowana. Czy dynamiczne uzupełnianie składowych to zła praktyka? Definiując klasę, definiuje się typ obiektów. Informuje się tym samym otoczenie, że dana klasa (i wszelkie jej konkretyzacje w postaci obiektów) składa się z ustalonego zestawu pól danych i funkcji. Jeśli klasa ShopProduct definiuje składową $title, wtedy dowolny kod użytkujący obiekty klasy ShopProduct może śmiało odwoływać się do składowej $title, ponieważ jej dostępność jest pewna. Nie da się podobnej pewności stosowania uzyskać względem składowych dodawanych do obiektów w sposób dynamiczny. Nasze obiekty są na razie cokolwiek nieporęczne. Chcąc manipulować ich składowymi, musimy bowiem czynić to poza samymi obiektami. Sięgamy do nich jedynie celem ustawienia i odczytania składowych. Konieczność ustawienia wielu takich składowych szybko stanie się wyjątkowo uciążliwa: $product1 = new ShopProduct(); $product1->title = "Moja Antonia"; $product1->producerMainName = "Cather"; $product1->producerFirstName = "Willa"; $product1->price = 59.99;

W powyższym kodzie zamazaliśmy jedną po drugiej pierwotne, definiowane w klasie wartości składowych obiektów, aż wprowadziliśmy komplet pożądanych zmian obiektu. Po arbitralnym wymuszeniu wartości składowych możemy się swobodnie odwołać do nowych wartości: print "Autor: {$product1->producerFirstName} " ."{$product1->producerMainName}\n";

Powyższy kod wypisze na wyjściu programu: Autor: Willa Cather

Taka metoda ustawiania i odwoływania się do składowych powoduje szereg problemów. Największym jest potencjalne niebezpieczeństwo dynamicznego uzupełnienia zestawu składowych obiektu w wyniku literówki w odwołaniu. O taką pomyłkę naprawdę łatwo — wystarczyłoby, byśmy zamiast: $product1->producerMainName

= "Cather";

napisali: $product1->producerSecondName = "Cather";

Z punktu widzenia samego języka PHP kod taki byłby jak najbardziej dozwolony, więc programista nie otrzymałby żadnego ostrzeżenia. Ale kiedy przyszłoby do wyprowadzania nazwiska autora (ogólnie: wytwórcy), wyniki odbiegałyby od oczekiwanych.

34

ROZDZIAŁ 3.  OBIEKTOWY ELEMENTARZ

Kolejnym problemem jest zbytnie rozluźnienie relacji pomiędzy składowymi klasy. Nie mamy obowiązku ustawiać tytułu, ceny czy nazwiska autora — użytkownik obiektu może być pewien, że obiekt takie składowe posiada, ale nie ma żadnej gwarancji przypisania do nich jakichkolwiek wartości (poza ewentualnymi wartościami domyślnymi). Tymczasem najlepiej byłoby, gdyby każdy nowo utworzony obiekt posiadał znaczące wartości swoich składowych. Wreszcie traktowanie z osobna każdej składowej jest nużące, zwłaszcza kiedy zamierzamy robić to częściej. Już samo wyświetlenie nazwiska autora jest uciążliwe: print "Autor: {$product1->producerFirstName} " ."{$product1->producerMainName}\n";

Byłoby miło, gdyby podobne zadania dało się złożyć na barki samego obiektu. Wszystkie te problemy można wyeliminować, uzupełniając klasę ShopProduct o zestaw własnych funkcji, które pośredniczyłyby w manipulowaniu składowymi, operując nimi z poziomu kontekstu obiektu.

Metody Składowe pozwalają obiektom na przechowywanie danych, metody zaś umożliwiają obiektom wykonywanie zadań. Metody to specjalne funkcje (zwane też niekiedy funkcjami składowymi), deklarowane we wnętrzu klasy. Jak można się spodziewać, deklaracja metody przypomina deklarację zwykłej funkcji. Nazwę metody poprzedza słowo kluczowe function, a uzupełnia ją opcjonalna lista parametrów ujęta w nawiasy. Ciało metody ograniczone jest nawiasami klamrowymi: public function myMethod($argument, $another) { // … }

W przeciwieństwie do zwykłych funkcji metody muszą być deklarowane w ciele klasy. Mogą też być opatrywane szeregiem modyfikatorów, w tym słowem kluczowym określającym widoczność. Podobnie jak składowe, tak i metody można deklarować jako publiczne (public), chronione (protected) albo prywatne (private). Deklarując metodę jako publiczną, umożliwiamy wywoływanie jej spoza kontekstu obiektu. Pominięcie określenia widoczności w deklaracji metody oznacza niejawnie jej widoczność i dostępność publiczną. Do modyfikatorów metod wrócimy nieco później. W większości okoliczności metoda wywoływana jest na rzecz konkretnego obiektu, z którym jej nazwa jest kojarzona operatorem dostępu do składowej ->. Nazwa metody musi być w wywołaniu uzupełniona nawiasami — niezależnie od tego, czy metoda przyjmuje jakiekolwiek argumenty (dokładnie tak jak w funkcji). class ShopProduct { public $title public $producerMainName public $producerFirstName public $price

= = = =

"bez tytułu"; "nazwisko"; "imię"; 0;

function getProducer() { return "{$this->producerFirstName}". "{$this->producerMainName}"; } } $product1 = new ShopProduct(); $product1->title = "Moja Antonia"; $product1->producerMainName = "Cather"; $product1->producerFirstName = "Willa"; $product1->price = 59.99; print "Autor: {$product1->getProducer()}\n";

35

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Na wyjściu programu uzyskamy: Autor: Willa Cather

Do klasy ShopProduct dodaliśmy klasę getProducer(). Zauważmy, że nie opatrzyliśmy jej deklaracji słowem regulującym widoczność. Tym samym uznaliśmy publiczność metody getProducer() i możliwość jej wywoływania spoza klasy. W niniejszej metodzie pojawiła się pewna nowinka. Chodzi o pseudozmienną $this, za pośrednictwem której kod klasy odwołuje się do egzemplarza klasy, na rzecz którego metoda została wywołana. Wszelkie wątpliwości co do znaczenia $this w kodzie klasy należy rozstrzygać, zastępując zmienną wyrażeniem „bieżący egzemplarz klasy”. Stąd instrukcja: $this->producerFirstName

oznacza tyle, co: Składowa $producerFirstName bieżącego egzemplarza klasy

Jak widać, metoda getProducer() realizuje i zwraca konkatenację składowych $producerFirstName i $producerMainName. Obecność tej metody oszczędza nam odwołań do poszczególnych składowych i własnoręcznego konstruowania ciągu nazwiska autora. Tym sposobem ulepszyliśmy nieco naszą klasę. Mimo to nie uniknęliśmy pułapki nadmiernej elastyczności — inicjalizację obiektów klasy ShopProducer składamy bowiem na barki programisty kodu klienckiego klasy ShopProduct i musimy polegać na jego solidności. Poprawna i pełna inicjalizacja obiektu naszej klasy wymaga pięciu wierszy kodu (pięciu instrukcji) — żaden programista nam za to nie podziękuje. A do tego jako twórcy klasy nie mamy możliwości zagwarantowania prawidłowej inicjalizacji którejkolwiek ze składowych obiektów klasy ShopProduct w kodzie klienckim. Potrzebowalibyśmy do tego metody wywoływanej automatycznie w przebiegu konkretyzacji obiektu.

Metoda konstrukcji obiektu Metoda konstrukcji obiektu, zwana po prostu konstruktorem, wywoływana jest w ramach konkretyzacji, czyli tworzenia obiektu klasy. W jej ramach można wykonać operacje inicjalizujące obiekt oraz wykonujące pewne przewidziane dla całej klasy operacje wstępne. W wersjach PHP poprzedzających wersję piątą konstruktor przyjmował nazwę klasy, w ramach której operował — klasa ShopProduct miała więc zawsze konstruktor ShopProduct(). Ta konwencja wciąż jest dozwolona, choć zalecana nazwa metody konstrukcji obiektu klasy to __construct(). Zauważmy, że nazwa tej metody rozpoczyna się od dwóch znaków podkreślenia, charakterystycznych również dla wielu innych specjalnych metod deklarowanych w klasach PHP. Zdefiniujmy więc konstruktor klasy ShopProduct: class ShopProduct { public $title; public $producerMainName; public $producerFirstName; public $price = 0; function __construct($title, $this->title $this->producerFirstName $this->producerMainName $this->price }

36

$firstName, $mainName, $price) { = $title; = $firstName; = $mainName; = $price;

ROZDZIAŁ 3.  OBIEKTOWY ELEMENTARZ

function getProducer() { return "{$this->producerFirstName}". "{$this->producerMainName}"; } }

Znów ulepszyliśmy nieco klasę, oszczędzając sobie i innym użytkownikom klasy konieczności zwielokrotniania kodu inicjalizacji każdego obiektu z osobna. Teraz przy okazji konkretyzacji obiektu w ramach operatora new wywoływana jest każdorazowo metoda __construct(). Obiekt tworzy się teraz tak: $product1 = new ShopProduct("Moja Antonia", "Willa", "Cather", 59.99); print "Autor: {$product1->getProducer()}\n";

Na wyjściu tego programu otrzymamy: Autor: Willa Cather

Wszelkie argumenty przekazane w wywołaniu new są przekazywane do konstruktora klasy. W naszym przykładzie przekazujemy w ten sposób do konstruktora tytuł, imię i nazwisko autora oraz cenę książki. Konstruktor w swoim ciele odwołuje się do składowych tworzonego obiektu za pośrednictwem pseudozmiennej $this.  Uwaga PHP4 nie rozpoznaje metody o nazwie __construct() jako metody konstruktora. Użytkownicy PHP4 mogą definiować konstruktor, deklarując w klasie metodę o nazwie identycznej z nazwą klasy. W przypadku klasy ShopProduct trzeba by więc zadeklarować konstruktor na bazie metody o nazwie ShopProduct(). PHP wciąż honoruje taką konwencję nazewniczą, jednak jeśli nie zależy nam na zgodności wstecz, najlepiej do roli konstruktora deklarować metodę __construct().

Obiekty klasy ShopProduct dają się teraz tworzyć znacznie łatwiej i bezpieczniej. Całość operacji związanych z inicjalizacją realizuje z punktu widzenia użytkownika pojedyncze wywołanie operatora new. Teraz w kodzie wykorzystującym obiekty klasy ShopProduct można w pełni polegać na prawidłowej inicjalizacji wszystkich składowych obiektu. Tego rodzaju pewność i przewidywalność to bardzo istotny aspekt programowania obiektowego. Klasy należy projektować tak, aby ich użytkownicy mogli w sposób pewny wykorzystywać ich cechy. Dzięki temu użytkownicy obiektów mają pewność co do ich stanu i zachowania, ale powinni mieć również pewność co do ich typu. Mechanizmom wymuszania konkretnego typu obiektów w deklaracjach metod przyjrzymy się w następnym podrozdziale.

Typy argumentów metod Typy określają w skryptach sposób zarządzania danymi. Typy łańcuchowe są wykorzystywane do przechowywania i wyświetlania ciągów znaków oraz do manipulowania takimi ciągami za pośrednictwem odpowiednich funkcji. Zmienne liczbowe są wykorzystywane w wyrażeniach matematycznych. Zmienne logiczne osadzane są w wyrażeniach logicznych. Tego rodzaju typy zaliczamy do typów elementarnych (ang. primitive types). Klasy stanowią w systemie typów znacznie wyższy poziom. Obiekt klasy ShopProduct stanowi wartość elementarnego typu „obiekt”, ale równocześnie jest wcieleniem (egzemplarzem) konkretnej klasy — ShopProduct. Zajmijmy się więc relacjami typów i metod. Definicje metod i funkcji nie muszą nakładać na parametry żadnych ograniczeń co do typów. To zarówno możliwość zbawienna, jak i katastrofalna. Fakt, że argument wywołania funkcji może być dowolnego typu, daje niezrównaną elastyczność. Można dzięki temu konstruować metody reagujące inteligentnie na różne przekazywane do nich dane, dostosowując realizowane w nich funkcje do okoliczności wywołania. Elastyczność ta jest jednak równocześnie przyczyną niejednoznaczności i nieoczekiwanego działania kodu, kiedy przekazany argument jest typu innego niż spodziewany.

37

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Typy elementarne PHP to język o osłabionej kontroli typów. Oznacza to, że deklaracja zmiennej nie musi określać i narzucać jej typu. Zmienna $number może w ramach jednego zasięgu zostać zainicjalizowana wartością liczbową 2, a za chwilę zapisana ciągiem "dwa". W językach ze ścisłą kontrolą typów, jak C i Java, typ zmiennej musi być zadeklarowany jeszcze przed przypisaniem jej wartości, a przypisywana wartość musi być typu zgodnego z deklarowanym. Nie oznacza to, że w języku PHP w ogóle nie istnieje pojęcie typu. Każda wartość, którą da się przypisać do zmiennej, posiada typ. Typ zmiennej można określić za pośrednictwem jednej z wbudowanych funkcji PHP. Lista typów elementarnych wyróżnianych w PHP wraz z funkcjami wykrywającymi przynależność do tegoż typu widnieje w tabeli 3.1. Każda z tych funkcji przyjmuje w wywołaniu badaną zmienną i zwraca wartość true, jeśli zmienna ta należy do określonego typu. Tabela 3.1. Typy elementarne i funkcje kontroli typów w PHP Funkcja testująca przynależność do typu

Nazwa typu

Opis

is_bool()

boolean

Jedna z dwóch wyróżnionych wartości: true („prawda”) i false („fałsz”).

is_integer()

integer

Liczba całkowita (równoważne z wywołaniami is_int() i is_long()).

is_double()

double

Liczba zmiennoprzecinkowa (z częścią ułamkową; równoważne wywołaniu is_float()).

is_string()

string

Ciągi znaków.

is_object()

object

Obiekt.

is_array()

array

Tablica.

is_resource()

resource

Uchwyt identyfikujący i pośredniczący w komunikacji z zasobami zewnętrznymi, np. bazami danych i plikami.

is_null()

null

Wartość pusta.

Sprawdzanie typu wartości w PHP ma szczególne znaczenie przy przetwarzaniu argumentów wywołania funkcji i metod.

Znaczenie typu elementarnego — przykład W kodzie trzeba bezwzględnie kontrolować wykorzystywane typy. Spójrzmy na przykład jednego z wielu problemów związanych z systemem typów. Wyobraźmy sobie, że skrypt wyodrębnia konfigurację aplikacji z pliku XML. Element XML instruuje aplikację co do podejmowania próby odwzorowania adresu IP na nazwę domenową — często odwzorowanie takie jest przydatne, ale zazwyczaj jest operacją stosunkowo kosztowną czasowo. Oto próbka pliku konfiguracyjnego:

false

Skrypt wyodrębnia z pliku konfiguracyjnego ciąg "false" i przekazuje go w roli znacznika do metody o nazwie outputAddresses(), wyświetlającej dane adresowe (IP i ewentualnie — w zależności od wartości znacznika — nazwę domenową). Oto kod metody outputAddresses(): class AddressManager { private $addresses = array("209.131.36.159", "74.125.19.106"); function outputAddresses($resolve) { foreach($this->addresses as $address) {

38

ROZDZIAŁ 3.  OBIEKTOWY ELEMENTARZ

print $address; if ($resolve) { print " (" . gethostbyaddr($address) . ")"; } print "\n"; } } }

Klasa AddressManager mogłaby oczywiście zostać nieco ulepszona; wpisywanie na sztywno adresu IP w kodzie klasy rzadko kiedy jest dobrym pomysłem. Tak czy inaczej metoda outputAddresses() przegląda tablicę ze składowej $addresses i wypisuje wartości poszczególnych elementów tablicy. Jeśli parametr $resolve ma wartość true, obok adresów IP wyprowadzane są nazwy domenowe. Oto nieco inne podejście, z wykorzystaniem w klasie AddressManager pliku konfiguracyjnego w formacie XML. Zobacz, czy uda Ci się wychwycić słabość tego wariantu: $settings = simplexml_load_file("settings.xml"); $manager = new AddressManager(); $manager->outputAddresses((string) $settings->resolvedomains);

Celem wyodrębnienia z pliku ustawień wartości elementu resolvedomains odwołujemy się tu do SimpleXML API (wprowadzonego w PHP5). Wiemy skądinąd, że wartością owego elementu jest u nas ciąg znaków "false" i zgodnie z dokumentacją SimpleXML rzutujemy tę wartość na typ string. Kod, niestety, nie będzie zachowywał się prawidłowo. Otóż przekazując w wywołaniu metody outputAddresses() ciąg "false", wykazujemy się niezrozumieniem niejawnego założenia, jakie metoda czyni odnośnie do wartości argumentu wywołania. Otóż metoda spodziewa się przekazania wartości logicznej (czyli wartości true albo false). Tymczasem ciąg "false" nie jest wartością logiczną, a co gorsza, jeśli już użyjemy go w roli takiej wartości, da wartość true. PHP wykona bowiem rzutowanie niepustego ciągu znaków na typ logiczny, a w dziedzinie wartości typu logicznego niepusty ciąg znaków reprezentowany jest jako true. Dlatego: if ("false") { // … }

jest równoznaczne z: if (true) { // … }

Błąd tego rodzaju można wyeliminować na kilka sposobów. Można metodę outputAddresses() uodpornić na mylne interpretacje typów argumentów, wyposażając ją w kod rozpoznający argument typu ciągu znaków i konwertujący taki ciąg na wartość logiczną wedle własnych kryteriów: // Klasa AddressManager... function outputAddresses($resolve) { if (is_string($resolve)) { $resolve = (preg_match("/false|no|off/i", $resolve))? false: true; } // … }

Istnieją jednak solidne przesłanki do unikania takich sposobów. W zasadzie lepiej jest udostępnić przejrzysty, zwarty i ograniczony interfejs metody niż interfejs otwarty i wieloznaczny. Funkcje i metody przyjmujące niejasne semantycznie argumenty prowokują bowiem do niechlujnego stosowania, a więc i do wprowadzania błędów użycia. Można jeszcze inaczej: zostawić ciało metody outputAddresses() w spokoju, opatrując jej deklarację komentarzem dającym użytkownikom jasność co do wymagań metody wobec typu argumentu $resolve i jego interpretacji w ciele funkcji. Decydujemy się tym samym na złożenie odpowiedzialności za poprawne działanie metody na barki użytkownika. 39

PHP. OBIEKTY, WZORCE, NARZĘDZIA

/** * Wyświetla listę adresów. * Przy wartości true argumentu $resolve adresy będą odwzorowywane do nazwy domenowej. * @param $resolve Boolean Wyszukać nazwy domenowe? */ function outputAddresses($resolve) { // … }

To całkiem niezłe rozwiązanie, pod warunkiem że programiści mający używać klasy są uważnymi czytelnikami dokumentacji. Można też wreszcie rygorystycznie odrzucać w ciele metody wszystkie argumenty nieodpowiednich typów, jawnie wymuszając na wywołujących dostosowanie typu argumentu wywołania: function outputAddresses($resolve) { if (!is_bool($resolve)) { die("outputAddresses() wymaga przekazania wartości typu boolean!"); } // … }

Takie podejście wymusza na użytkowniku metody przekazanie do niej wartości konkretnego typu. Wyręczenie wywołującego i konwersja argumentu typu łańcuchowego na typ logiczny łagodniej traktuje użytkowników, ale prowokuje szereg kolejnych problemów. Udostępniając mechanizm konwersji, skazujemy się na odgadywanie intencji wywołującego. Narzucając mu stosowanie typu logicznego, dajemy mu z kolei wolną rękę co do sposobu odwzorowywania wartości logicznych w ciągach znaków — klient sam decyduje, czy to dopuszczalne i jakie słowo reprezentuje dla niego „prawdę”. Metoda outputAddresses() może zaś skupić się na swym podstawowym zadaniu, do którego została powołana. Tego rodzaju skupienie na własnych zadaniach z celowym ignorowaniem szerszego kontekstu jest ważną zasadą programowania obiektowego i będę się na nią często w książce powoływać. W istocie zaś strategie obsługi typów argumentów powinny być uzależnione od ważności ewentualnych błędów. PHP potrafi rzutować wartości pomiędzy większością elementarnych typów, zależnie od zastanego kontekstu wykorzystania wartości. Na przykład liczby w ciągach znaków, jeśli ciągi te występują w wyrażeniach arytmetycznych, są konwertowane na postać ich całkowitych i zmiennoprzecinkowych odpowiedników. W kodzie można polegać na tej konwersji, czyniąc go odpornym na szereg błędów typowania. Jeśli jednak któryś z argumentów metody ma być tablicą, nie można się spodziewać, że PHP dokona konwersji dowolnej wartości do sensownej tablicy — tego rodzaju pobłażliwość w dostosowaniu typów może prowadzić do istnej powodzi błędów w ciele metody. Trzeba więc wyznaczyć pewien punkt równowagi pomiędzy pobłażliwością względem niesfornych wywołujących a bezwzględnym wymuszaniem odpowiedniego typu. Bardzo istotną rolę gra tutaj dokumentacja, która nie powinna pozostawiać u wywołującego wątpliwości co do pożądanego typu argumentu wywołania metody. Niezależnie od sposobu radzenia sobie z tego rodzaju problemami trzeba mieć świadomość, że mimo bardzo luźnej kontroli typów w PHP typ ma istotne znaczenie. Ba, fakt liberalnego traktowania typów w PHP jeszcze to znaczenie potęguje. Nie można przy tym w zadaniu wykrywania błędów typowania zdawać się na kompilator. To programista musi oszacować ewentualny wpływ niedopasowania typów na wykonanie metody i odpowiednio do szacunków dobrać metodę obsługi typów argumentów. Nie sposób przy tym wymagać od wywołujących przenikliwości właściwej telepatom, stąd konieczność przygotowania kodu na okoliczność niepożądanych typów argumentów.

Typy obiektowe Jako że argument wywołania funkcji może reprezentować wartość dowolnego typu elementarnego, może też domyślnie reprezentować obiekt dowolnego typu. Taka elastyczność ma swoje zalety, ale powoduje też problemy, zwłaszcza w kontekście definicji metody. Wyobraźmy sobie metodę pewnej klasy pomocniczej, przeznaczonej do manipulowania obiektami klasy ShopProduct:

40

ROZDZIAŁ 3.  OBIEKTOWY ELEMENTARZ

class ShopProductWriter { public function write($shopProduct) { $str = "{$shopProduct->title}: ". $shopProduct->getProducer(). " ({$shopProduct->price})\n"; print $str; } }

Klasę tę możemy przetestować kodem: $product1 = new ShopProduct("Moja Antonia", "Willa", "Cather", 59.99); $writer = new ShopProductWriter(); $writer->write($product1);

Otrzymamy: Moja Antonia: Willa Cather (59.99)

Klasa ShopProductWriter zawiera tylko jedną metodę — write(). Metoda ta przyjmuje za pośrednictwem argumentu wywołania obiekt klasy ShopProduct, a odwołując się do jego składowych i metod, konstruuje ciąg podsumowujący wartość obiektu. Nazwa parametru metody, $shopProduct, sygnalizuje co prawda spodziewany typ obiektu, ale w żaden sposób go nie wymusza. Oznacza to, że argumentem wywołania metody mógłby być dowolny typ prosty albo obiektowy, a jego faktyczny typ mógłby się objawić dopiero przy próbie użycia go w operacji zakładającej obecność obiektu klasy ShopProduct. Tyle że jeszcze przed użyciem argumentu metoda może wykonać pewne operacje na bazie założenia, że ma do czynienia z obiektem odpowiedniej klasy.  Uwaga Metodę write() można by dodać bezpośrednio do klasy ShopProduct. Nie zrobimy tego jednak ze względu na podział odpowiedzialności. Klasa ShopProduct ma realizować zadania zarządzania danymi produktów; za wypisywanie danych o produktach odpowiedzialna jest klasa ShopProductWriter. Znaczenie i przydatność wyraźnego podziału odpowiedzialności stanie się bardziej oczywiste po lekturze dalszej części rozdziału.

Problem niemożności wymuszenia typu w wywołaniu metody wyeliminowano w PHP5 wraz z mechanizmem sygnalizowania oczekiwanego typu — tzw. class type hints. Otóż parametr deklarowanej metody można poprzedzić „wskazówką” czy też pouczeniem (ang. hint) w postaci nazwy klasy, której obiektów metoda się spodziewa. Metodę write() można by więc przepisać tak: public function write(ShopProduct $shopProduct) { // … }

Teraz metoda write() nie będzie akceptowała w roli argumentów wywołania obiektów klas innych niż ShopProduct. Możemy to sprawdzić, prowokując niepoprawne wywołanie: class Wrong {} $writer = new ShopProductWriter(); $writer->write(new Wrong());

Z racji obecności w deklaracji metody write() pouczenia wymuszającego typ przekazanie w wywołaniu obiektu nieodpowiedniej (Wrong) klasy sprowokuje krytyczny błąd programu: PHP Catchable fatal error: Argument 1 passed to ShopProductWriter::write() must be an instance of ShopProduct, instance of Wrong given ...

41

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Możemy teraz darować sobie testowanie typu argumentu przed przystąpieniem do jego przetwarzania. Otrzymujemy też bardziej przejrzystą dla użytkownika sygnaturę metody — użytkownik może na jej podstawie od razu wnioskować co do oczekiwanego w wywołaniu typu, bez konieczności uciekania się do dokumentacji. A ponieważ pouczenie co do typu jest rygorystycznie przestrzegane, unikamy nie zawsze łatwych do wykrycia błędów charakterystycznych dla błędów typowania. Choć tak zautomatyzowana kontrola poprawności typów skutecznie eliminuje obszerną kategorię błędów, trzeba zdawać sobie sprawę, że owe wskazówki są kontrolowane w czasie wykonania programu. Oznacza to, że błąd naruszenia wskazówki zostanie wykryty i zgłoszony dopiero w momencie, w którym nastąpi wywołanie metody z obiektem nieodpowiedniej klasy. Jeśli przypadkiem niefortunne wywołanie write() będzie osadzone w klauzuli warunkowej uruchamianej jedynie w Boże Narodzenie, możesz spodziewać się pracowitych świąt — wcześniej błąd pozostanie najprawdopodobniej ukryty. Sygnalizowanie typów argumentów nie może być użyte z parametrami typów prostych, jak ciągi znaków czy liczby całkowite. Dla takich typów musimy się jawnie uciec do klasycznych środków kontroli typu wartości, jak funkcja is_int(). Możemy jednak sygnalizować argumenty tablicowe: function setArray(array $storearray) { $this->array = $storearray; }

Mechanizm sygnalizowania tablicowych argumentów wywołania został dodany do PHP w wersji 5.1. Innym dodatkiem jest możliwość nadawania pustych wartości domyślnych argumentom podlegającym sygnalizacji typów. Oznacza to, że możemy zażądać od wywołującego przekazania wartości oczekiwanego typu, ewentualnie wartości pustej: function setWriter(ObjectWriter $objwriter = null) { $this->writer = $objwriter; }

Dotychczas traktowaliśmy typy i klasy jak pojęcia równoznaczne. Tymczasem pomiędzy typami a klasami istnieje zasadnicza różnica. Otóż definiując klasę, definiuje się równocześnie typ, ale typ jako taki może opisywać całą rodzinę klas. Mechanizm grupowania wielu klas w obrębie jednego typu nosi nazwę dziedziczenia. Będzie on tematem następnego podrozdziału.

Dziedziczenie Dziedziczenie to mechanizm wyprowadzania jednej bądź wielu klas pochodnych z pewnej wspólnej klasy bazowej. Klasa dziedzicząca po innej klasie staje się jej podklasą. Owa relacja często opisywana jest w oparciu o relację rodzic – dziecko. Owo „dziecko” (klasa potomna czy też pochodna) jest wyprowadzone z klasy „rodzica” (klasy nadrzędnej albo bazowej) i dziedziczy jej składowe i metody. Klasa pochodna zazwyczaj uzupełnia elementy odziedziczone własnymi składowymi i metodami — mówi się wtedy o „rozszerzaniu” klasy bazowej1. Zanim zagłębimy się w składnię dziedziczenia, powinniśmy rozpoznać problemy, w których rozwiązywaniu dziedziczenie okazuje się pomocne.

Problemy związane z dziedziczeniem Wróćmy do naszej klasy ShopProduct. Na razie jest ona dość ogólna, ponieważ nie ogranicza asortymentu produktów (mimo że dotychczas jej obiekty reprezentowały asortyment księgarski). $product1 = new ShopProduct("Moja Antonia", "Willa", "Cather", 59.99); $product2 = new ShopProduct("Exile on Coldharbour Lane", "The", "Alabama 3", 25.99); print "Autor : ".$product1->getProducer()."\n"; print "Wykonawca : ".$product2->getProducer()."\n"; 1

Choć właściwsze byłoby mówienie o „specjalizacji” — przyp. tłum.

42

ROZDZIAŁ 3.  OBIEKTOWY ELEMENTARZ

Program wypisze na wyjściu: Autor : Willa Cather Wykonawca : The Alabama 3

Rozdzielenie nazwy „producenta” na dwie części sprawdza się dla książek i nawet dla albumów CD. Możemy dzięki niemu wyszukiwać i porządkować asortyment wg „Alabama 3” i „Cather”, pozbywając się mniej znaczących „The” i „Willa”. Wygoda to zazwyczaj znakomita strategia projektowa, nie musimy więc na razie przejmować się dostosowaniem projektu klasy ShopProduct do artykułów innych rodzajów. Gdybyśmy jednak nasz przykład uzupełnili o pewne dodatkowe wymagania, rzecz szybko by się skomplikowała. Załóżmy na przykład, że obiekty klasy ShopProducer powinny jednak przechowywać dodatkowo informacje charakterystyczne dla ich asortymentu — inne w przypadku książek (np. liczba stron), inne w przypadku albumów CD (np. czas nagrania). Różnic może być znacznie więcej, ale i te wystarczą do ilustracji problemu. W jaki sposób powinniśmy rozszerzyć klasę, aby dało się odzwierciedlić w niej nowe wymagania? Niemal natychmiast na myśl przychodzą dwie możliwości. Pierwsza polega na zebraniu w klasie ShopProduct wszelkich możliwych składowych. Druga zakłada podział klasy na dwie osobne. Spróbujmy pierwszego sposobu — połączenia w jednej klasie składowych charakterystycznych dla płyt i książek: class ShopProduct { public $numPages; public $playLength; public $title; public $producerMainName; public $producerFirstName; public $price; function __construct($title, $firstName, $mainName, $price, $numPages = 0, $playLength = 0) { $this->title = $title; $this->producerFirstName = $firstName; $this->producerMainName = $mainName; $this->price = $price; $this->numPages = $numPages; $this->playLength = $playLength; } function getNumberOfPages() { return $this->numPages; } function getPlayLength() { return $this->playLength; } function getProducer() { return "{$this->producerFirstName}". "{$this->producerMainName}"; } }

W definicji klasy pojawiły się metody dające dostęp do składowych $numPages i $playLength; kod ten ilustruje też pewną nadmiarowość. Otóż obiekt konkretyzowany z takiej klasy będzie zawierał nadmiarową metodę, a w przypadku obiektów dla płyt CD konstruktor będzie przyjmował niepotrzebny argument: obiekt reprezentujący płytę CD będzie utrzymywać informacje i funkcje właściwe dla obiektów książek (tu: liczbę stron) i odwrotnie — obiekt reprezentujący w istocie książkę będzie niepotrzebnie przechowywał długość nagrania.

43

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Na razie zapewne możemy się z taką nadmiarowością pogodzić. Ale co, jeśli asortyment zostanie rozszerzony na kolejne kategorie produktów, a wraz z nimi pojawią się w klasie kolejne składowe i metody? Klasa nadmiernie się rozrośnie i stanie się po prostu niewygodna w użyciu. Jak widać, scalanie w jednej klasie danych i funkcji różnych klas prowadzi do rozszerzenia obiektów o nadmiarowe i zbędne składowe i metody. Problem nie kończy się jednak na nadmiarowości danych. Cierpi również funkcjonalność klasy. Weźmy choćby metodę zestawiającą informacje o produkcie. Niech dział sprzedaży zażyczy sobie możliwości generowania podsumowania informacji o artykule na potrzeby wystawianych w dziale faktur. W opisie albumu CD ma znaleźć się długość nagrania, a w opisie książki — liczba stron. Trzeba więc będzie przewidzieć różne implementacje zestawień dla każdego rodzaju asortymentu. Można by spróbować wygospodarować w klasie znacznik informujący o formacie obiektu, jak w tym przykładzie: function getSummaryLine() { $base = "{$this->title} ( {$this->producerMainName}, "; $base .= "{$this->producerFirstName} )"; if ( $this->type == 'książka' ) { $base .= ": liczba stron - {$this->numPages}"; } else if ($this->type == 'cd' ) { $base .= ": czas nagrania - {$this->playLength}"; } return $base; }

Alternatywnie, aby poprawnie ustawić składową $type, moglibyśmy sprawdzić wartość argumentu wywołania konstruktora dla parametru $numPages (jeśli wynosi 0, to mamy do czynienia z płytą CD). Słowem, dalej niepotrzebnie „rozdymamy” i komplikujemy klasę ShopProduct, a w miarę dokładania różnych formatów obiektów różnice funkcjonalne pomiędzy nimi będą coraz trudniejsze do ujęcia w spójnej implementacji. Może więc lepiej byłoby spróbować innego sposobu? Ponieważ klasa ShopProduct zaczyna przypominać siłowe sklejenie dwóch klas, możemy spróbować podzielić ją na dwoje. Moglibyśmy podejść do zadania tak: class CdProduct { public $playLength; public $title; public $producerMainName; public $producerFirstName; public $price; function __construct($title, $firstName, $mainName, $price, $playLength) { $this->title = $title; $this->producerFirstName = $firstName; $this->producerMainName = $mainName; $this->price = $price; $this->playLength = $playLength; } function getPlayLength() { return $this->playLength; } function getSummaryLine() { $base = "{$this->title} ( {$this->producerMainName}, "; $base .= "{$this->producerFirstName} )"; $base .= ": czas nagrania - {$this->playLength}"; return $base; }

44

ROZDZIAŁ 3.  OBIEKTOWY ELEMENTARZ

function getProducer() { return "{$this->producerFirstName}". "{$this->producerMainName}"; } } class BookProduct { public $numPages; public $title; public $producerMainName; public $producerFirstName; public $price; function __construct($title, $firstName, $mainName, $price, $numPages) { $this->title = $title; $this->producerFirstName = $firstName; $this->producerMainName = $mainName; $this->price = $price; $this->numPages = $numPages; } function getNumberOfPages() { return $this->numPages; } function getSummaryLine() { $base = "{$this->title} ( {$this->producerMainName}, "; $base .= "{$this->producerFirstName} )"; $base .= ": liczba stron - {$this->numPages}"; return $base; } function getProducer() { return "{$this->producerFirstName}". "{$this->producerMainName}"; } }

Rozwiązaliśmy problem rosnącej złożoności klasy. Teraz możemy tworzyć osobne wersje metody getSummaryLine() bez potrzeby kontrolowania w jej ciele znacznika właściwego formatu obiektu. Klasa

nie utrzymuje też zbędnych składowych i metod. Ale rozwiązanie nie jest bezkosztowe: doprowadziliśmy do powielenia kodu. Metoda getProducer() w obu klasach ma identyczny kod. Każdy z konstruktorów ustawia w identyczny sposób pewien podzbiór składowych obiektu. To istotna wada kodu i warto się jej pozbyć. Skoro metoda getProducer() zachowuje się identycznie dla każdej z klas, to jakakolwiek zmiana tego zachowania będzie musiała być zaimplementowana z osobna we wszystkich tych klasach. Prędzej czy później podczas tej synchronizacji będziemy przeklinać podjętą decyzję. Jeśli zaś jesteś przekonany, że poradzisz sobie z duplikacją kodu, to nie możesz zapomnieć, że teraz zamiast jednego typu mamy dwa różne (mimo podobieństw) typy. Wróćmy do klasy ShopProductWriter. Jej metoda write() została przystosowana do pracy na obiektach pojedynczego typu — obiektach klasy ShopProduct. W jaki sposób zmusić ją do obsługi obiektów dwóch różnych klas? Możemy oczywiście usunąć z deklaracji metody pouczenie co do typu argumentu, ale wtedy będziemy musieli w pełni zaufać wywołującemu — program będzie poprawny jedynie wtedy, kiedy do metody będą przekazywane obiekty właściwych typów. Możemy dokonywać kontroli typów w ciele metody:

45

PHP. OBIEKTY, WZORCE, NARZĘDZIA

class ShopProductWriter { public function write($shopProduct) { if (!($shopProduct instanceof CdProduct) && !($shopProduct instanceof BookProduct)) { die("Przekazano obiekt niewłaściwego typu"); } $str = "{$shopProduct->title}: ". $shopProduct->getProducer(). " ({$shopProduct->price})\n"; print $str; } }

W przykładzie wykorzystaliśmy operator instanceof. Wywołanie tego operatora daje wartość true, jeśli jego lewy operand jest egzemplarzem klasy występującej w roli prawego operandu. Raz jeszcze zostaliśmy więc zmuszeni do wprowadzenia dodatkowego poziomu złożoności w kodzie. Nie tylko musimy testować przynależność przekazanego w wywołaniu write() obiektu do jednego z dwóch typów, ale i ufać, że żaden z tych typów nie zaniecha obsługi wykorzystywanych przez nas składowych i metod. Rzecz wyglądała znacznie lepiej, kiedy żądaliśmy przekazania w wywołaniu konkretnego typu, większą mieliśmy też pewność, że typ ten — klasa ShopProduct — posiada taki, a nie inny interfejs. Odmiany książkowa i płytowa klasy ShopProduct nie współgrają ze sobą, ale mimo wszystko zdaje się, że mogą ze sobą przynajmniej koegzystować. Lepiej byłoby jednak, gdybyśmy i obiekty reprezentujące książki, i obiekty płyt muzycznych mogli traktować jak egzemplarze jednej klasy, ale wyposażone w nieco odmienną implementację stosowną do formatu wymaganego w prezentacji asortymentu towarów. Chcielibyśmy więc móc zdefiniować wspólny zestaw funkcji i cech, unikając duplikacji kodu, ale równocześnie umożliwić rozgałęzienie implementacji niektórych wywołań metod zależnie od formatu obiektu. Rozwiązaniem jest dziedziczenie.

Stosowanie dziedziczenia Pierwszym etapem konstrukcji hierarchii dziedziczenia jest identyfikacja tych elementów klasy bazowej, które nie są na tyle uniwersalne, aby dały się identycznie obsługiwać we wszystkich egzemplarzach. W naszej powstałej swego czasu klasie ShopProducer mieliśmy, na przykład, kolidujące ze sobą metody getPlayLength() i getNumberOfPages(). Pamiętamy też, że wspólna dla wszystkich obiektów metoda getSummaryLine() wymagała różnych implementacji dla różnych formatów obiektów. Te trzy różnice mogą nam posłużyć do wyodrębnienia klasy bazowej i dwóch klas pochodnych: class ShopProduct { public $numPages; public $playLength; public $title; public $producerMainName; public $producerFirstName; public $price; function __construct($title, $firstName, $mainName, $price, $numPages = 0, $playLength = 0) { $this->title = $title; $this->producerFirstName = $firstName; $this->producerMainName = $mainName; $this->price = $price; $this->numPages = $numPages; $this->playLength = $playLength; } function getProducer() { return "{$this->producerFirstName}".

46

ROZDZIAŁ 3.  OBIEKTOWY ELEMENTARZ

"{$this->producerMainName}"; } function getSummaryLine() { $base = "{$this->title} ( {$this->producerMainName}, "; $base .= "{$this->producerFirstName} )"; return $base; } } class CdProduct extends ShopProduct { function getPlayLength() { return $this->playLength; } function getSummaryLine() { $base = "{$this->title} ( {$this->producerMainName}, "; $base .= "{$this->producerFirstName} )"; $base .= ": czas nagrania - {$this->playLength}"; return $base; } } class BookProduct extends ShopProduct { function getNumberOfPages() { return $this->numPages; } function getSummaryLine() { $base = "{$this->title} ( {$this->producerMainName}, "; $base .= "{$this->producerFirstName} )"; $base .= ": liczba stron - {$this->numPages}"; return $base; } }

Utworzenie klasy pochodnej wymaga opatrzenia deklaracji klasy słowem extends. W powyższym przykładzie utworzyliśmy w ten sposób dwie nowe klasy: BookProduct i CdProduct. Obie rozszerzają i uzupełniają klasę ShopProduct. Ponieważ w klasach pochodnych zabrakło definicji konstruktorów, w momencie konkretyzacji obiektów tych klas wywoływany jest automatycznie konstruktor klasy bazowej. Klasy pochodne dziedziczą bowiem dostęp do wszystkich publicznych i chronionych metod klasy bazowej (z wyjątkiem składowych i metod prywatnych). Oznacza to, że możemy wywoływać metodę getProducer() na rzecz obiektu konkretyzowanego z klasy CdProduct, choć sama metoda getProducer() jest zdefiniowana nie w CdProduct, a w ShopProduct. $product2 = new CdProduct("Exile on Coldharbour Lane", "The", "Alabama 3", 25.99, null, 60.33); print "Wykonawca: {$product2->getProducer()}\n";

Jak widać, obie klasy pochodne dziedziczą zachowanie po rodzicu. Obiekt klasy BookProduct możemy więc traktować jak wcielenie obiektu klasy ShopProduct. I dlatego też możemy przekazywać obiekty klasy BookProduct bądź CdProduct w wywołaniu metody write() klasy ShopProductWriter. Zauważmy, że w klasach CdProduct i BookProduct nastąpiło przesłonięcie metody getSummaryLine() jej implementacjami odpowiednimi dla tych klas. Sęk w tym, że klasy pochodne mogą nie tylko rozszerzać i uzupełniać, ale i modyfikować zachowanie klas nadrzędnych.

47

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Implementacja tej metody w klasie bazowej wydaje się nadmiarowa, skoro i tak jest przepisywana w obu klasach pochodnych. Niemniej jednak ta bazowa implementacja udostępnia najbardziej podstawową realizację danej funkcji, dostępną do użycia w klasach pochodnych. Obecność metody w klasie bazowej daje też gwarancję, że wszelkie obiekty klasy ShopProduct (i klas pochodnych) w kodzie klienckim będą posiadać metodę getSummaryLine(). Później przekonamy się, że taką gwarancję można wymusić bez implementowania metody w klasie bazowej. Każda klasa pochodna ShopProduct dziedziczy komplet składowych klasy „rodzica”. Dlatego zarówno klasa CdProduct, jak i BookProduct mogą w swoich implementacjach metody getSummaryLine() odwoływać się do składowej $title. Dziedziczenie może z początku być koncepcją niejasną. Definiując klasę rozszerzającą inną klasę, gwarantujemy, że obiekt tejże nowej klasy będzie w pierwszym rzędzie określany cechami definiowanymi w klasie pochodnej, a dopiero w drugiej kolejności tymi z klasy bazowej. Można też zastosować inną analogię — gdybyśmy chcieli samodzielnie rozprowadzić wywołanie $product2->getProducer(), nie znaleźlibyśmy takiej metody w klasie CdProduct, więc wywołanie przenieślibyśmy do „domyślnej” implementacji tej metody, zdefiniowanej w ShopProduct. Ale już wywołanie $product2->getSummaryLine() możemy zrealizować za pomocą metody z klasy CdProduct. To samo dotyczy odwołań do składowych. Występującego w metodzie getSummaryLine() klasy BookProduct odwołania do składowej $title nie można zrealizować w ramach klasy BookProduct; jest ona pobierana z klasy bazowej. Pozostawienie jej w klasie bazowej ma uzasadnienie, ponieważ inaczej trzeba by ją dublować we wszystkich pochodnych. Rzut oka na konstruktor klasy bazowej ujawnia jednak, że wciąż w klasie bazowej obsługujemy dane, których obsługa powinna zostać przeniesiona do klas pochodnych. Otóż klasa BookProduct powinna przejąć obsługę argumentu i składowej $numPages, a składowa $playLength powinna zostać wyodrębniona do klasy CdProduct. W tym celu trzeba by w klasach pochodnych zdefiniować ich własne konstruktory.

Dziedziczenie a konstruktory Definiując konstruktor klasy pochodnej, trzeba wziąć na siebie odpowiedzialność za przekazanie argumentów do wywołania konstruktora klasy bazowej. Jeśli to zaniedbamy, otrzymamy częściowo tylko skonstruowany obiekt. Aby wywołać z wnętrza klasy pochodnej metodę klasy bazowej, musimy najpierw poznać sposób odwołania się do klasy jako takiej. W języku PHP służy do tego słowo kluczowe parent. Aby odwołać się do metody w kontekście klasy, a nie obiektu, powinniśmy zamiast operatora -> zastosować operator ::. Zapis: parent::__construct()

oznacza więc: „wywołanie metody __construct() klasy bazowej”. Spróbujmy zatem zmodyfikować nasz przykład tak, aby każda klasa odpowiadała jedynie za swoje własne składowe: class ShopProduct { public $title; public $producerMainName; public $producerFirstName; public $price; function __construct($title, $firstName, $mainName, $price) { $this->title = $title; $this->producerFirstName = $firstName; $this->producerMainName = $mainName; $this->price = $price; } function getProducer() { return "{$this->producerFirstName}". "{$this->producerMainName}"; }

48

ROZDZIAŁ 3.  OBIEKTOWY ELEMENTARZ

function getSummaryLine() { $base = "{$this->title} ( {$this->producerMainName}, "; $base .= "{$this->producerFirstName} )"; return $base; } } class CdProduct extends ShopProduct { public $playLength; function __construct( $title, $firstName, $mainName, $price, $playLength) { parent::__construct($title, $firstName, $mainName, $price); $this->playLength = $playLength; } function getPlayLength() { return $this->playLength; } function getSummaryLine() { $base = "{$this->title} ( {$this->producerMainName}, "; $base .= "{$this->producerFirstName} )"; $base .= ": czas nagrania - {$this->playLength}"; return $base; } } class BookProduct extends ShopProduct { public $numPages; function __construct( $title, $firstName, $mainName, $price, $numPages) { parent::__construct($title, $firstName, $mainName, $price); $this->numPages = $numPages; } function getNumberOfPages() { return $this->numPages; } function getSummaryLine() { $base = "{$this->title} ( {$this->producerMainName}, "; $base .= "{$this->producerFirstName} )"; $base .= ": liczba stron - {$this->numPages}"; return $base; } }

Każda klasa pochodna wywołuje w swoim konstruktorze konstruktor klasy bazowej, a dopiero potem przystępuje do ustawiania własnych składowych. Klasa bazowa troszczy się wyłącznie o swoje dane. Klasy pochodne są zaś w ogólności specjalizacjami klas bazowych. Należy więc unikać ujmowania w klasach bazowych (jako ogólniejszych) specjalistycznej wiedzy o klasach pochodnych.

49

PHP. OBIEKTY, WZORCE, NARZĘDZIA

 Uwaga W wersjach poprzedzających PHP5 konstruktory miały nazwy zgodne z nazwami klas. Teraz nazwy metod konstrukcji zostały ujednolicone — konstruktor każdej klasy nazywa się __construct(). Gdyby zechcieć skorzystać z przestarzałej składni, wywołanie konstruktora klasy bazowej wiązałoby kod klasy pochodnej z tą konkretną klasą: parent::ShopProduct();

Tego rodzaju przywiązanie do nazwy powoduje problemy w momencie wprowadzania zmian w hierarchii dziedziczenia. Łatwo wtedy o błędy, kiedy programiści wybierają dla klasy inną klasę nadrzędną, ale zapominają o aktualizacji kodu konstruktora. Gdy zaś korzystamy z ujednoliconej nazwy konstruktora, wywołanie konstruktora klasy bazowej: parent::__construct()

zawsze odnosi się do bezpośredniego „rodzica”, niezależnie od zmian wprowadzanych w hierarchii dziedziczenia. Ujednolicenie nie dotyczy jednak argumentów konstruktorów — i tak trzeba pamiętać o przekazaniu do nich odpowiednich wartości!

Wywołania metod przesłoniętych Słowo kluczowe parent można stosować w odwołaniach do wszelkich metod klasy bazowej, które zostały przesłonięte w klasie pochodnej. Niekiedy bowiem zamiast całkiem przesłaniać metodę klasy bazowej, chcemy jedynie uzupełnić jej działanie. Możemy się wtedy we własnej implementacji wesprzeć wersją metody z klasy bazowej. Mimo ulepszeń wprowadzonych do naszej hierarchii klas wciąż mamy do czynienia z pewną duplikacją kodu — dochodzi do niej w ramach metody getSummaryLine(). Tymczasem zamiast powtarzać kod w klasach pochodnych, moglibyśmy odwołać się do kodu klasy ShopProduct i tylko uzupełnić go stosownie do potrzeb klasy pochodnej: // Klasa ShopProduct… function getSummaryLine() { $base = "{$this->title} ( {$this->producerMainName}, "; $base .= "{$this->producerFirstName} )"; return $base; } // Klasa BookProduct… function getSummaryLine() { $base = parent::getSummaryLine(); $base .= ": liczba stron - {$this->numPages}"; return $base; }

Podstawowe zadania metody getSummaryLine() zdefiniowaliśmy w klasie ShopProduct. Teraz zamiast powtarzać jej kod w klasach CdProduct i BookProduct, możemy po prostu wywołać wersję z klasy bazowej i uzupełnić otrzymany z takiego wywołania ciąg danymi charakterystycznymi dla klasy pochodnej. Znając już podstawowe zasady dziedziczenia, możemy wrócić do zagadnienia widoczności metod i składowych.

Zarządzanie dostępem do klasy — słowa public, private i protected Jak dotąd wszystkie składowe i metody klas deklarowaliśmy jako publiczne (ze słowem kluczowym public). Dostęp publiczny do klasy jest zakładany domyślnie — jako publiczne są też traktowane te składowe, które są deklarowane z wykorzystaniem obowiązującego w PHP4 słowa var. Tymczasem elementy klas mogą być deklarowane jako publiczne (public), ale również jako chronione (protected) i prywatne (private):  Metody i składowe publiczne są dostępne niezależnie od kontekstu.  Metody i składowe prywatne są dostępne jedynie z wnętrza zawierającej je klasy. Dostęp do nich jest odmawiany nawet klasom pochodnym.  Metody i składowe chronione są dostępne z klasy, w której są deklarowane, oraz z jej klas pochodnych. Dostępu nie uzyskuje jednak kod zewnętrzny wobec danej klasy.

50

ROZDZIAŁ 3.  OBIEKTOWY ELEMENTARZ

Czy różnicowanie widoczności może być w ogóle przydatne? Owszem, ponieważ odpowiednio stosowane słowa regulujące widoczność pozwalają na eksponowanie z klasy jedynie tych jej elementów, które są potrzebne użytkownikom obiektów klasy i jako takie stanowią jej interfejs. Uniemożliwiając użytkownikom zewnętrznym odwoływanie się do niektórych składowych, możemy zapobiegać błędom. Wyobraźmy sobie, że obiekty ShopProduct miałyby przechowywać informacje o rabatach. Informacje te miałyby być przechowywane w składowych $discount, których ustawienie następowałoby przez wywołanie metody setDiscount(): // Klasa ShopProduct public $discount = 0; // … function setDiscount($num) { $this->discount = $num; }

Uzbrojeni w mechanizm przyznawania rabatów możemy zdefiniować metodę getPrice(), która wyceni artykuł z uwzględnieniem owego rabatu: // Klasa ShopProduct function getPrice() { return ($this->price - $this->discount); }

Pojawia się jednak problem. Otóż użytkownicy zewnętrzni powinni widzieć jedynie ostateczne ceny (uwzględniające rabaty), tymczasem użytkownik mający dostęp do obiektu może — zamiast wywoływać metodę getPrice() — pójść na skróty i odwołać się bezpośrednio do składowej przechowującej cenę: print "Cena artykułu wynosi {$product1->price}\n";

Co spowoduje wyprowadzenie mylącej (bo nieuwzględniającej rabatów) ceny. Takim przypadkom możemy zapobiec, deklarując $price jako składową prywatną i uniemożliwiając dostęp do niej z zewnątrz. Zmusi to użytkowników do korzystania z metody getPrice(). Próba odwołania się do prywatnej składowej spoza klasy ShopProduct będzie bowiem nieskuteczna. Dla świata zewnętrznego składowa ta przestanie po prostu istnieć. Ukrywanie składowych jako prywatnych może jednak okazać się nadgorliwością. Do składowych prywatnych nie mają dostępu nawet klasy pochodne. Wyobraźmy sobie, że zgodnie z założeniami biznesowymi książki zostały wyłączone z wszelkich promocji. Pomysł taki moglibyśmy zrealizować, przesłaniając w klasie BookProduct metodę getPrice() z pominięciem rabatu. Metoda ta musi jednak mieć dostęp do składowej $price: // Klasa BookProduct… function getPrice() { return $this->price; }

Jeśli składowa $price zostałaby zadeklarowana jako prywatna w klasie ShopProduct, to niestety powyższy kod byłby niepoprawny, gdyż dostęp do składowych prywatnych jest zablokowany nawet dla klas pochodnych. Rozwiązaniem byłoby zadeklarowanie $price jako składowej chronionej i jako takiej dostępnej z poziomu klas pochodnych. Trzeba przy tym pamiętać, że tak oznaczona składowa nie będzie w ogóle dostępna dla kodu spoza hierarchii dziedziczenia, w tym dla innych klas, które nie uczestniczą w tej hierarchii. Dostępność składowych i metod chronionych jest ograniczona do klasy, w której je zadeklarowano, i jej klas pochodnych. Warto przyjąć regułę faworyzowania prywatności składowych i metod. To, co nie jest zabronione, jest dozwolone, lepiej więc domyślnie zaostrzać kryteria dostępu do składowych i rozluźniać je w miarę potrzeb. Wiele (zwykle większość) metod konstruowanych przez nas klas będzie metodami publicznymi, ale jeśli potrzeba ich udostępniania jest wątpliwa, lepiej z tego zrezygnować. Metoda udostępniająca lokalne funkcje danej klasy pozostałym metodom tej klasy nie powinna być widoczna dla użytkowników zewnętrznych — niech więc będzie albo całkiem prywatna, albo przynajmniej chroniona przed dostępem z zewnątrz.

51

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Metody — akcesory Jeśli nawet użytkownicy zewnętrzni muszą odwoływać się do wartości przechowywanych w obiektach klasy, nie znaczy to, że mają otrzymać pełny dostęp do tych składowych — niejednokrotnie lepiej regulować ten dostęp, definiując metody — akcesory — pośredniczące w odwołaniach do owych składowych. Mieliśmy już okazję przekonać się o zaletach takich metod. Akcesor może bowiem nie tylko wprost udostępniać wartości, ale również filtrować je w zależności od okoliczności. Przykład takiego filtrowania mieliśmy w metodzie getPrice(). Metody, o których mowa, mogą także służyć do wymuszania typu składowej. Czytelnik zna już sposób wymuszania właściwego typu argumentu w wywołaniach metod, nie zna jednak analogicznego mechanizmu umożliwiającego kontrolowanie typu wartości przypisywanej do składowej klasy. Jak pamiętamy, klasa ShopProductWriter wykorzystywała do wyprowadzania danych obiekt ShopProduct. Spróbujmy przerobić ją tak, aby mogła służyć do wyprowadzania wartości wielu obiektów ShopProduct: class ShopProductWriter { public $products = array(); public function addProduct(ShopProduct $shopProduct) { $this->products[] = $shopProduct; } public function write() { $str = ""; foreach($this->products as $shopProduct) { $str .= "{$shopProduct->title}: "; $str .= $shopProduct->getProducer(); $str .= " ({$shopProduct->getPrice()})\n"; } print $str; } }

Klasa ShopProductWriter jest teraz znacznie bardziej użyteczna. Może przechowywać w swoich obiektach wiele egzemplarzy obiektów klasy ShopProduct i wypisywać dane wszystkich w jednym podejściu. Musimy jednak ufać, że użytkownicy kodu będą respektować nasze intencje; a pomimo że udostępniliśmy im metodę addProduct(), nie muszą wcale z niej korzystać — nie możemy zabronić programistom stosującym klasę ShopProductWriter bezpośredniego manipulowania składową $products. I narażamy się tym samym nie tylko na ryzyko wprowadzenia do składowej $products obiektów niepoprawnego typu, ale i zamazania całej tablicy albo zastąpienia jej wartością skalarną. Wszystkie te zagrożenia eliminujemy, oznaczając składową $products jako prywatną: class ShopProductWriter { private $products = array(); // …

Teraz nie ma możliwości zamazania składowej $products spoza klasy. Wszelkie odwołania do składowej muszą być realizowane za pośrednictwem metody addProduct(), która z kolei wymusza przekazanie argumentu stosownego typu.

Klasy hierarchii ShopProduct Zamknijmy rozdział wersjami deklaracji klas hierarchii ShopProduct uzupełnionymi o odpowiednie zabezpieczenia widoczności składowych i metod: class ShopProduct { private $title; private $producerMainName; private $producerFirstName; protected $price;

52

ROZDZIAŁ 3.  OBIEKTOWY ELEMENTARZ

private $discount = 0; public function __construct($title, $firstName, $mainName, $price) { $this->title = $title; $this->producerFirstName = $firstName; $this->producerMainName = $mainName; $this->price = $price; } public function getProducerFirstName() { return $this->producerFirstName; } public function getProducerMainName() { return $this->producerMainName; } public function setDiscount($num) { $this->discount = $num; } public function getDiscount() { return $this->discount; } public function getTitle() { return $this->title; } public function getPrice() { return ($this->price - $this->discount); } public function getProducer() { return "{$this->producerFirstName}". "{$this->producerMainName}"; } public function getSummaryLine() { $base = "{$this->title} ( {$this->producerMainName}, "; $base .= "{$this->producerFirstName} )"; return $base; } } class CdProduct extends ShopProduct { private $playLength = 0; public function __construct( $title, $firstName, $mainName, $price, $playLength) { parent::__construct($title, $firstName, $mainName, $price); $this->playLength = $playLength; } public function getPlayLength() { return $this->playLength;

53

PHP. OBIEKTY, WZORCE, NARZĘDZIA

} public function getSummaryLine() { $base = parent::getSummaryLine(); $base .= ": czas nagrania - {$this->playLength}"; return $base; } } class BookProduct extends ShopProduct { private $numPages = 0; public function __construct( $title, $firstName, $mainName, $price, $numPages) { parent::__construct($title, $firstName, $mainName, $price); $this->numPages = $numPages; } public function getNumberOfPages() { return $this->numPages; } public function getSummaryLine() { $base = parent::getSummaryLine(); $base .= ": liczba stron - {$this->numPages}"; return $base; } public public function getPrice() { return $this->price; } }

W nowej wersji rodziny klas ShopProduct nie ma szczególnych nowinek. Wszystkie metody zostały jedynie jawnie oznaczone jako publiczne, a wszystkie składowe albo jako prywatne, albo jako chronione. Wymusiło to uzupełnienie klas o odpowiednie akcesory składowych niepublicznych.

Podsumowanie Niniejszy rozdział zawiera solidną dawkę podstaw, bo zaczynając od zupełnie pustej klasy, doszliśmy do rozbudowanej hierarchii dziedziczenia. Udało się wyjaśnić szereg istotnych kwestii, w tym tych związanych z typami i dziedziczeniem. Udało się też zilustrować elementy obsługi obiektów w PHP. A niebawem poznasz kolejne obiektowe mechanizmy PHP.

54

ROZDZIAŁ 4 

Zaawansowana obsługa obiektów

W poprzednim rozdziale można się było przekonać, że już choćby dzięki narzucaniu typów argumentów wywołań metod i sterowaniu widocznością składowych klasy możemy znakomicie zwiększyć kontrolę nad interfejsem klasy. W niniejszym rozdziale zagłębimy się w nieco bardziej zaawansowane cechy obiektowe języka PHP5. Rozdział ten będzie poświęcony:  Metodom i składowym statycznym — czyli odwołaniom do danych i funkcji nie przez obiekty, a przez klasy.  Interfejsom i klasom abstrakcyjnym — czyli oddzielaniu projektu od implementacji.  Cechom typowym — czyli częściom wspólnym dla oddzielnych hierarchii klas.  Obsłudze błędów — a więc wprowadzeniu do wyjątków.  Klasom i metodom finalnym — celowym ograniczeniom dziedziczenia.  Metodom przechwytującym — przechwytywaniu chybionych wywołań.  Metodom destrukcji obiektów — czyli sprzątaniu po obiektach.  Klonowaniu obiektów — wykonywaniu ich kopii.  Odwzorowaniu obiektów do ciągów — czyli tworzeniu metod zestawiających.  Wywołaniom zwrotnym — czyli rozszerzaniu komponentów przez funkcje anonimowe.

Metody i składowe statyczne We wszystkich przykładach ilustrujących poprzedni rozdział działaliśmy na obiektach. Klasy scharakteryzowaliśmy jako szablony, z których „odciskane” są obiekty, a obiekty jako aktywne komponenty — jednostki, do których składowych się odwołujemy i których metody wywołujemy w kodzie programu. Była mowa o tym, że w programowaniu obiektowym praca w programie odbywa się przede wszystkim w ramach instancji klas — czyli w ramach obiektów. Same klasy są natomiast jedynie szablonami obiektów. Sprawa jest nieco bardziej złożona. Otóż okazuje się, że do niektórych składowych i metod klas możemy odwoływać się nie tylko w kontekście obiektu, ale i w kontekście klasy jako takiej. Takie metody i składowe noszą nazwę statycznych i muszą być deklarowane ze słowem static: class StaticExample { static public $aNum = 0; static public function sayHello() { print "Hej"; } }

PHP. OBIEKTY, WZORCE, NARZĘDZIA

 Uwaga Słowo kluczowe static wprowadzone zostało w PHP5 — w PHP4 nie jest ono rozpoznawane.

Metody statyczne są funkcjami o zasięgu klasy. Nie mogą one odwoływać się do żadnych zwyczajnych składowych klasy, ponieważ zwyczajne składowe klasy są przynależne poszczególnym obiektom; w metodach statycznych można za to odwoływać się do składowych statycznych klasy. Zmiana statycznej składowej klasy dotyczy wszystkich instancji klasy (obiektów) — w każdym z obiektów danej klasy widoczna jest nowa wartość zmodyfikowanej składowej statycznej. Ponieważ do elementów statycznych odwołujemy się przez klasę, a nie jej egzemplarz, nie potrzebujemy korzystać z pośrednictwa zmiennej reprezentującej obiekt klasy. Zamiast tego odwołujemy się do nazwy samej klasy, kojarząc ją z nazwą składowej czy metody za pomocą operatora ::: print StaticExample::$aNum; StaticExample::sayHello();

Powyższą składnię można było zobaczyć już w poprzednim rozdziale, nie jest więc nowością. Operator :: wykorzystywaliśmy tam w klasie pochodnej w odwołaniu do przesłoniętej metody z klasy bazowej. Tutaj również odwołujemy się nie tyle do danych poszczególnych obiektów, co do elementów klasy. W kodzie klasy pochodnej odwołania do elementów statycznych klasy bazowej można realizować za pośrednictwem słowa parent, unikając tym samym podawania wprost nazwy tej klasy, zaś w odwołaniach do statycznych elementów danej klasy w kodzie tej klasy należy stosować słowo self. Słowo self jest tym dla klas, czym pseudozmienna $this dla obiektów. Gdybyśmy więc chcieli odwołać się do statycznej składowej $aNum spoza klasy StaticExample, możemy powołać się na nazwę klasy: StaticExample::$aNum;

Wewnątrz klasy StaticExample można zaś w tym samym celu stosować słowo self: class StaticExample { static public $aNum = 0; static public function sayHello() { self::$aNum++; print "Hej, (".self::$aNum.")\n"; } }

 Uwaga Inicjowanie wywołania metody ze słowem parent to jedyna okoliczność, w której w PHP5 dozwolone jest stosowanie statycznej referencji do metody niestatycznej. Jeśli więc nie chodzi o odwołanie do przesłoniętej wersji metody, to nie wolno stosować operatora :: w wywołaniach tych metod, które nie zostały jawnie zadeklarowane jako statyczne. Składnię wywołania statycznego często widuje się w dokumentacji przy opisach składowych i metod klasy niekoniecznie będących metodami i składowymi statycznymi. Składnia ta ma jedynie uwidaczniać przynależność składowej bądź metody do danej klasy. Na przykład metoda write() klasy ShopProductWriter może występować w dokumentacji jako ShopProductWriter::write(), co nie znaczy wcale, że jest metodą statyczną (bo nie jest). Podobną składnię będę stosował również w treści tej książki, kiedy konieczne będzie ścisłe określenie przynależności opisywanej składowej czy metody.

Z zasady metody i składowe statyczne są wywoływane na rzecz klas, a nie na rzecz obiektów. Z tego względu metody i składowe statyczne są często określane mianem składowych i funkcji klasy. Oznacza to też, że nie można w ciele metody statycznej odwoływać się do pseudozmiennej $this. Po cóż nam więc składowe i metody statyczne? Otóż statyczne elementy klas posiadają szereg użytecznych własności. Po pierwsze, są one dostępne w dowolnym miejscu skryptu (o ile jest w nim dostęp do samej klasy). Daje to możliwość korzystania z klasy bez konieczności przekazywania obiektu pomiędzy jednostkami programu albo — co gorsza — wyznaczania obiektu do roli zmiennej globalnej. Po drugie, składowe statyczne są dostępne

56

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

dla każdego egzemplarza klasy, można więc za ich pośrednictwem udostępniać poszczególnym obiektom pewien zestaw wartości. Wreszcie brak konieczności powoływania do życia obiektu celem odwołania się do wybranych metod i składowych klasy oznacza najzwyklejszą oszczędność i prostotę. Zilustruję to poprzez wyposażenie klasy ShopProduct w metodę statyczną, która będzie automatyzować konkretyzację obiektów tej klasy. W tym celu zdefiniujmy w SQLite tabelę produktów: CREATE TABLE products ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT, firstname TEXT, mainname TEXT, title TEXT, price float, numpages int, playlength int, discount int )

Spróbujmy teraz ułożyć metodę getInstance(), która przyjmowałaby identyfikator wiersza tabeli i obiekt danych reprezentujących taki wiersz, wykorzystała je do pozyskania wiersza tabeli z bazy danych i potem na jego podstawie skonstruowała i zwróciła obiekt klasy ShopProduct. Metodę tę moglibyśmy włączyć do klasy ShopProduct utworzonej w poprzednim rozdziale. Obiekt danych to obiekt klasy PDO (od PHP Data Object). Klasa obiektów typu PDO udostępnia interfejs baz danych dla różnych aplikacji. // klasa ShopProduct private $id = 0; // … public function setID($id) { $this->id = $id; } // … public static function getInstance($id, PDO $pdo) { $stmt = $pdo->prepare("select * from products where id=?"); $result = $stmt->execute( array( $id ) ); $row = $stmt->fetch( ); if (empty($row)) { return null; } if ($row['type'] === "książka") { $product = new BookProduct( $row['title'], $row['frstname'], $row['mainname'], $row['price'], $row['numpages']); } else if ($row['type'] == "cd") { $product = new CdProduct( $row['title'], $row['frstname'], $row['mainname'], $row['price'], $row['playlength']); } else { $product = new ShopProduct( $row['title'], $row['frstname'], $row['mainname'], $row['price']); }

57

PHP. OBIEKTY, WZORCE, NARZĘDZIA

$product->setId( $row['id']); $product->setDiscount( $row['discount']); return $product; } // ...

Jak widać, metoda getInstance() zwraca obiekt klasy ShopProduct, przy czym jest na tyle inteligentna, że jest w stanie przy konkretyzacji obiektu wziąć pod uwagę jego konkretny typ i odpowiednio go specjalizować. Aby całość była maksymalnie przejrzysta, pominąłem w tym przykładzie kwestię obsługi błędów. W prawdziwym programie nie bylibyśmy rzecz jasna tak ufni co do stanu przekazanego obiektu danych — nie zakładalibyśmy na przykład, że został zainicjalizowany do komunikacji z odpowiednią bazą danych. Najlepiej byłoby ująć PDO w klasie, która zagwarantowałaby nam poprawną inicjalizację. O programowaniu obiektowym i bazach danych napiszę więcej w rozdziale 13. Ta metoda jest bardziej przydatna w kontekście klasy niż w kontekście obiektu. Pozwala bowiem łatwo przekładać dane z bazy danych do postaci obiektów. Metoda nie odwołuje się do składowych czy metod konkretnego egzemplarza, zanim sama tego egzemplarza nie utworzy — nie ma więc najmniejszego powodu, aby nie oznaczyć jej jako statycznej. Zakładając dostępność obiektu klasy PDO, możemy wywołać metodę z zupełnie dowolnego miejsca aplikacji: $dsn = "sqlite://home/bob/projects/products.db"; $pdo = new PDO( $dsn, null, null ); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $obj = ShopProduct::getInstance(1, $pdo);

Metody tego rodzaju określamy mianem „wytwórni” (tudzież „fabryki”), ponieważ z przekazywanych im „surowców” (takich jak wiersz danych czy informacje konfiguracyjne) potrafią produkować pełnoprawne obiekty. Pojęcie wytwórni odnosimy więc do kodu potrafiącego generować egzemplarze obiektów. Przykłady takich wytwórni będziemy mieli okazję analizować w dalszej części książki. W pewnym sensie w przykładzie tym wprowadzamy niemal tyle samo problemów, ile ich rozwiązujemy. Co prawda metoda ShopProduct::getInstance() została udostępniona w całym systemie bez konieczności dostępu do egzemplarza (obiektu) klasy ShopProduct, ale za to wywołujący musi do niej dostarczyć obiekt PDO. Skąd go wziąć? Problemy tego rodzaju — skąd pozyskać potrzebne obiekty i wartości — są w programowaniu obiektowym bardzo typowe. Sposobami wytwarzania obiektów zajmiemy się osobno w rozdziale 9.

Składowe stałe Niektóre ze składowych obiektów nie powinny zmieniać swoich wartości. Podobną niezmiennością powinny cechować się również choćby znaczniki stanu i kody błędów zaszywane w kodzie klasy. Kłopot w tym, że tego rodzaju składowe powinny równocześnie być dostępne publicznie i w sposób statyczny. PHP5 pozwala na zdefiniowanie w ramach klasy składowych niemodyfikowalnych albo stałych. Podobnie jak stałe globalne, tak i stałe zasięgu klasy po ustaleniu ich wartości nie mogą być zmieniane. Składowa niemodyfikowalna deklarowana jest ze słowem kluczowym const. Tak powstające stałe nie są w odwołaniach prefiksowane znakiem dolara, jak to ma miejsce w odwołaniach zwykłych zmiennych. Przyjęło się też, że ich nazwy zawierają wyłącznie wielkie litery: class ShopProduct { const AVAILABLE const OUT_OF_STOCK // …

= 0; = 1;

Składowe stałe mogą zawierać jedynie wartości typów elementarnych. Nie można przypisywać do nich obiektów. Podobnie jak składowe statyczne, składowe niemodyfikowalne są wykorzystywane nie za pośrednictwem obiektów, a klas. Skoro przy definicji stałej nie używamy znaku dolara, to nie będzie go również w odwołaniu do składowej stałej: print ShopProduct::AVAILABLE;

58

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

Próba przypisania wartości do stałej po jej zadeklarowaniu spowoduje błąd leksykalny. Stałe należy wykorzystywać tam, gdzie potrzebne są składowe dostępne co prawda publicznie i w oderwaniu od konkretnych egzemplarzy klasy, ale równocześnie nie ma potrzeby zmian wartości składowej.

Klasy abstrakcyjne Jedną z ważniejszych zmian w obsłudze obiektów w PHP było wprowadzenie klas abstrakcyjnych. Ich włączenie do listy rozszerzeń PHP było kolejnym znakiem coraz pełniejszej akceptacji linii projektowania obiektowego w rozwoju języka PHP. Klasa abstrakcyjna to taka, której nie da się skonkretyzować. Jej jedyną funkcją jest definiowanie (ewentualnie częściowe tylko implementowanie) interfejsu dla ewentualnych klas pochodnych. Klasę czyni się abstrakcyjną, umieszczając w jej deklaracji słowo kluczowe abstract. Spróbujmy przepisać deklarację utworzonej w poprzednim rozdziale klasy ShopProductWriter, tak aby stała się klasą abstrakcyjną: abstract class ShopProductWriter { protected $products = array(); public function addProduct(ShopProduct $shopProduct) { $this->products[] = $shopProduct; } }

Klasę tę można w zwykły sposób uzupełniać o metody i składowe, ale już próba utworzenia egzemplarza klasy w taki sposób: $writer = new ShopProductWriter();

sprowokuje błąd (nie można utworzyć egzemplarza klasy abstrakcyjnej): PHP Fatal error: Cannot instantiate abstract class ShopProductWriter in ...

W większości przypadków klasa abstrakcyjna zawiera przynajmniej jedną metodę abstrakcyjną. Metoda taka również jest deklarowana ze słowem kluczowym abstract. Metoda abstrakcyjna nie może posiadać implementacji. Deklaruje się ją w zwykły sposób, ale deklarację kończy się średnikiem, pomijając ciało metody. Oto klasa ShopProductWriter uzupełniona o abstrakcyjną metodę write(): abstract class ShopProductWriter { protected $products = array(); public function addProduct(ShopProduct $shopProduct) { $this->products[] = $shopProduct; } abstract public function write(); }

Tworząc metodę abstrakcyjną, obligujemy równocześnie klasy pochodne do zdefiniowania jej implementacji, jednak decyzje co do szczegółów tej implementacji zostawiamy twórcom owych klas. Gdyby zdarzyło się nam wyprowadzić z ShopProductWriter klasę, która nie implementuje metody write(): class ErroredWriter extends ShopProductWriter {}

otrzymalibyśmy następujący błąd: PHP Fatal error: Class ErroredWriter contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (ShopProductWriter::write) in...

59

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Jak widać, każda klasa rozszerzająca (czy też: specjalizująca) klasę abstrakcyjną powinna albo implementować wszystkie jej metody abstrakcyjne, albo sama zostać oznaczona jako abstrakcyjna. Klasa pochodna jest przy tym odpowiedzialna nie tylko za implementację każdej metody abstrakcyjnej, ale i za dokładne powtórzenie jej sygnatury. Oznacza to, że dostęp do metody abstrakcyjnej nie może być w klasie pochodnej ograniczony ściślej, niż był ograniczony w klasie bazowej. Implementacja metody abstrakcyjnej powinna zakładać również identyczną liczbę argumentów i powtarzać wszelkie „pouczenia” wymuszające typy argumentów wywołania. Zdefiniujmy więc dwie implementacje abstrakcji ShopProductWriter: class XmlProductWriter extends ShopProductWriter { public function write() { $writer =new XMLWriter(); $writer->openMemory(); $writer->startDocument('1.0','UTF-8'); $writer->startElement("products"); foreach($this->products as $shopProduct) { $writer->startElement("product"); $writer->writeAttribute( "title", $shopProduct->getTitle() ); $writer->startElement("summary"); $writer->text( $shopProduct->getSummaryLine() ); $writer->endElement(); // element summary $writer->endElement(); // element product } $writer->endElement(); // element products $writer->endDocument(); print $writer->flush(); } } class TextProductWriter extends ShopProductWriter { public function write() { $str = "PRODUCTS:\n"; foreach($this->products as $shopProduct) { $str .= $shopProduct->getSummaryLine()."\n"; } print $str; } }

Utworzyliśmy tu dwie klasy, z których każda zdefiniowała własną implementację metody write(). Pierwsza z tych implementacji generuje opisujące obiekt produktu elementy dokument XML, druga zaś wypisuje opis produktu w postaci tekstowej. Metoda, która wymaga przekazania w wywołaniu argumentu klasy ShopProductWriter, nie wie, którą z klas pochodnych reprezentować będzie przekazany w wywołaniu obiekt, może jednak mieć absolutną pewność, że obiekt ten będzie udostępniał zaimplementowaną odpowiednio dla swojej klasy metodę write(). Zauważmy, że nie ma tu sprawdzania typu wyrażenia $this->products — wiadomo, że to tablica, ponieważ ta właściwość jest inicjalizowana jako tablica (pusta) jeszcze w klasie ShopProductWriter. Klasy abstrakcyjne były w języku PHP4 realizowane jako klasy zawierające metody z ostrzeżeniami albo nawet wywołania funkcji die(). Zmuszało to klasę pochodną do przedefiniowania metody: class AbstractClass { function abstractFunction() { die("metoda AbstractClass::abstractFunction() jest abstrakcyjna\n"); } }

W takim wykonaniu klasy abstrakcyjne miały tę wadę, że ich abstrakcyjność ujawniała się dopiero po wywołaniu abstrakcyjnej metody na rzecz obiektu klasy. W języku PHP5 nie dochodzi już do konkretyzacji obiektów takich klas, co należy uznać za postęp. 60

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

Interfejsy Klasa abstrakcyjna może mimo wszystko być fragmentarycznie zaimplementowana; inaczej jest z interfejsami, które są czystymi szablonami. Interfejs może jedynie wyznaczać zakres funkcji, nie może ich jednak w żadnym razie implementować. Interfejs jest deklarowany ze słowem kluczowym interface. Interfejs może tak jak zwykła klasa zawierać deklaracje składowych i metod, ale wyłącznie deklaracje — interfejs nie może definiować ciał swoich metod. Zdefiniujmy prosty interfejs: interface Chargeable { public function getPrice(); }

Jak widać, interfejs bardzo przypomina klasę. Każda klasa przyjmująca taki interfejs zgadza się na implementowanie wszystkich przewidzianych w nim metod, chyba że zostanie zadeklarowana jako abstrakcyjna. Przyjęcie interfejsu do implementacji wymaga zastosowania w deklaracji klasy słowa kluczowego implements. Dalej proces implementacji interfejsu nie różni się od procesu rozszerzania klasy abstrakcyjnej, która zawiera wyłącznie metody abstrakcyjne. Spróbujmy więc zaimplementować w klasie ShopProduct interfejs Chargeable: class ShopProduct implements Chargeable { // … public function getPrice() { return ($this->price - $this->discount); } // …

Klasa ShopProduct wcześniej zawierała już metodę getPrice(), po cóż miałaby więc implementować dodatkowo tę metodę w ramach interfejsu Chargeable? Odpowiedzi należy poszukać w systemie typów. Otóż klasa implementująca zyskuje typ klasy abstrakcyjnej, którą rozszerza, i równocześnie typ interfejsu, który implementuje. Oznacza to, że obiekt klasy CdProduct jest obiektem typów: CdProduct ShopProduct Chargeable

Można to znakomicie wykorzystać w kodzie użytkującym klasę. Znajomość typu obiektu oznacza bowiem znajomość zakresu jego zastosowań. Zatem metoda: public function cdInfo(CdProduct $prod) { // … }

może w swojej implementacji bazować na możliwości wywołania na rzecz przekazanego obiektu klasy CdProduct zarówno metody getPlayLength(), jak i metod charakterystycznych dla wszystkich obiektów hierarchii ShopProduct i interfejsu Chargeable. Ten sam obiekt może być jednak przekazany również do metody: public function addProduct(ShopProduct $prod) { // … }

a w ciele tej metody można będzie wywoływać na rzecz obiektu wszystkie metody klasy ShopProduct, ale już bez gwarancji możliwości wywołania metody getPlayLength(). Ciągle ten sam obiekt (klasy CdProduct) może zostać wreszcie przekazany w wywołaniu metody: public function addChargeableItem(Chargeable $item) { // … }

która to metoda nie musi nic „wiedzieć” o istnieniu typów ShopProduct czy CdProduct. Dla tej metody ważne jest jedynie to, żeby przekazany obiekt udostępniał metodę getPrice(). 61

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Ponieważ interfejs może być implementowany przez dowolną klasę (co więcej, każda klasa może implementować nieograniczoną liczbę interfejsów), interfejsy pozwalają na kojarzenie zupełnie sobie obcych typów. Możemy, na przykład, zdefiniować całkowicie nową klasę implementującą interfejs Chargeable: class Shipping implements Chargeable { public function getPrice() { // … } }

W takim układzie metoda addChargeableItem będzie równie chętnie przyjmować w wywołaniu obiekty klasy ShopProduct, jak i obiekty klasy Shipping. Dla użytkownika korzystającego z obiektów Chargeable ważne jest to, że może na ich rzecz wywołać metodę getPrice(). Wszelkie inne dostępne metody (nieistotne zresztą dla użytkownika zainteresowanego cechą Chargeable) są już wynikiem przynależności obiektów do innych klas, dziedziczenia po innych klasach i implementowania innych interfejsów. Klasa może równocześnie dziedziczyć po klasie bazowej i implementować dowolną liczbę interfejsów. W takiej sytuacji klauzula extends powinna poprzedzać klauzulę implements, jak pokazano poniżej: class Consultancy extends TimedService implements Bookable, Chargeable { // … }

Zauważmy, że klasa Consultancy implementuje więcej niż jeden interfejs. Interfejsy implementowane przez klasę, jeśli występują w większej liczbie, są w deklaracji klasy wymieniane po przecinkach. W języku PHP dziedziczyć można tylko po jednej klasie, więc za słowem extends może znajdować się tylko jedna nazwa klasy.

Cechy typowe Wiemy już, że obecność interfejsów pozwala w PHP na obejście braku wielodziedziczenia (dziedziczenia z wielu klas bazowych). Innymi słowy, w PHP (podobnie zresztą jak w języku Java) klasa może rozszerzać tylko jedną klasę bazową. Można jednak zaznaczyć w deklaracji, że klasa implementuje dowolną liczbę interfejsów — i tak powstała klasa będzie zgodna co do typu z każdym implementowanym interfejsem. Interfejsy definiują więc typy pozbawione implementacji. A jeśli zechcemy współdzielić nie tylko typy, ale też ich implementacje pomiędzy osobnymi hierarchiami dziedziczenia? W języku PHP 5.4 jest to możliwe dzięki nowemu mechanizmowi tzw. cech typowych (ang. traits). Cecha typowa to struktura przypominająca klasę, która sama w sobie nie nadaje się do konkretyzacji (nie można utworzyć obiektu takiej klasy), ale może być wcielana do innych klas. Wszelkie metody zdefiniowane w klasie cechy typowej są dostępne jako składowe klasy używającej tej cechy typowej. Cecha typowa zmienia więc strukturę (implementację) klasy, ale nie zmienia jej typu. Cechy typowe należy więc traktować jako coś w rodzaju „komponentów” klas. Zobaczmy, w czym cechy typowe mogą się okazać przydatne.

Zadanie dla cech typowych Oto wersja klasy ShopProduct z metodą calculateTax(): class ShopProduct { private $taxrate = 17; function calculateTax($price) { return (($this->taxrate/100) * $price); } } $p = new ShopProduct(); print $p->calculateTax(100)."\n";

62

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

Metoda calculateTax() przyjmuje argument $price i oblicza wartość podatku na podstawie składowej $taxrate. Oczywiście do metody calculateTax() dostęp zyskują wszystkie klasy pochodne klasy ShopProduct. Ale już w osobnych hierarchiach klas identyczną metodę trzeba by było zaimplementować ponownie; weźmy za przykład klasę UtilityService, dziedziczącą po klasie Service. Jeśli w klasie UtilityService potrzebujemy identycznej implementacji obliczania podatku, musimy ją tam ponownie zdefiniować: abstract class Service { // składowe i metody do obsługi klas hierarchii Service } class UtilityService extends Service { private $taxrate = 17; function calculateTax($price) { return (($this->taxrate/100) * $price); } } $u = new UtilityService(); print $u->calculateTax(100)."\n";

Definiowanie i stosowanie cechy typowej Jednym z podstawowych celów projektowania obiektowego jest eliminowanie powielania kodu. Z rozdziału 11. dowiemy się, że jedną z metod zwalczania duplikacji jest wydzielanie implementacji do klas implementujących strategie wykonania poszczególnych operacji. Alternatywnym podejściem — może mniej eleganckim, ale niewątpliwie skutecznym — jest stosowanie cech typowych. Poniżej deklarowana jest pojedyncza cecha typowa definiująca metodę calculateTax(); tak zdefiniowaną cechę włączamy do klas ShopProduct i UtilityService: trait PriceUtilities { private $taxrate = 17; function calculateTax($price) { return (($this->taxrate/100) * $price); } // inne składowe } class ShopProduct { use PriceUtilities; } abstract class Service { // składowe i metody do obsługi klas hierarchii Service } class UtilityService extends Service { use PriceUtilities; } $p = new ShopProduct(); print $p->calculateTax(100)."\n"; $u = new UtilityService(); print $u->calculateTax(100)."\n";

63

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Cechę typową PriceUtilities zdefiniowaliśmy za pomocą słowa kluczowego trait. Ciało cechy typowej bardzo przypomina zwyczajną klasę. Jest to po prostu zestaw metod (a jak się przekonamy, również składowych) zamknięty pomiędzy nawiasami klamrowymi. Po zadeklarowaniu klasy typowej można się do niej odwoływać w innych klasach, niezależnie od ich klas bazowych i interfejsów. Polega to na zaznaczeniu włączenia cechy typowej za pomocą słowa use z nazwą cechy typowej. Tak więc po zadeklarowaniu i zaimplementowaniu metody calculateTax() w jednym miejscu możemy jej używać w dowolnie dużej liczbie klas, również niepowiązanych dziedziczeniem (u nas ShopProduct i UtilityService).

Stosowanie wielu cech typowych W klasie można odwoływać się do wielu cech typowych, włączanych do klasy za pośrednictwem deklaracji use z listą nazw cech typowych. W poniższym przykładzie definiujemy i stosujemy kolejną cechę typową: IdentityTrait razem ze zdefiniowaną wcześniej cechą PriceUtilities: trait IdentityTrait { public function generateId() { return uniqid(); } } trait PriceUtilities { private $taxrate = 17; function calculateTax($price) { return (($this->taxrate/100) * $price); } // inne składowe } class ShopProduct { use PriceUtilities, IdentityTrait; } $p = new ShopProduct(); print $p->calculateTax(100)."\n"; print $p->generateId()."\n";

Włączając do klasy ShopProducts obie cechy typowe: PriceUtilities i IdentityTrait, udostępniamy w tej klasie metody calculateTax() i generateId(). Oznacza to, że metody te klasa oferuje również zewnętrznym użytkownikom.  Uwaga Cecha IdentityTrait udostępnia metodę generateId(). W praktyce identyfikatory obiektów aplikacji są zazwyczaj generowane przez bazę danych, ale na potrzeby testów czasami warto przełączyć się na implementację lokalną. O obiektach, bazach danych i unikatowych identyfikatorach obiektów będzie mowa w rozdziale 13., omawiającym wzorzec projektowy Identity Map. Z kolei testowaniu będzie poświęcony rozdział 18.

Łączenie cech z interfejsami Cechy typowe są co prawda użyteczne, ale nie wpływają na typ klasy, do której są włączane. Jeśli więc dołączymy cechę typową IdentityTrait do wielu klas, nie będą to klasy zgodne co do typu, co umożliwiłoby ich grupowe określanie w oznaczeniach typów parametrów metod innych klas.

64

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

Na szczęście cechy typowe współgrają z interfejsami. Można więc zdefiniować interfejs wymagający metod generateId(), a następnie zadeklarować klasę ShopProduct jako implementującą ten interfejs (za pośrednictwem cechy typowej): interface IdentityObject { public function generateId(); } trait IdentityTrait { public function generateId() { return uniqid(); } } trait PriceUtilities { private $taxrate = 17; function calculateTax($price) { return (($this->taxrate/100) * $price); } // inne składowe } class ShopProduct implements IdentityObject { use PriceUtilities, IdentityTrait; }

Jak poprzednio klasa ShopProduct używa cechy typowej IdentityTrait. Ale tym razem implementacja metody generateId() włączana z cechy typowej jest równocześnie implementacją deklarowanego interfejsu IdentityObject. Dzięki temu klasę ShopProduct możemy przekazywać do metod i funkcji, które za pomocą oznaczania typów parametrów ograniczają argumenty wywołania do obiektów typu IdentityObject, jak poniżej: function storeIdentityObject(IdentityObject $idobj) { // operacje na egzemplarzu typu IdentityObject } $p = new ShopProduct(); storeIdentityObject($p);

Unikanie kolizji nazw metod za pomocą słowa insteadof Możliwość używania wielu cech typowych w klasie jest bardzo atrakcyjna, ale prędzej czy później prowadzi do kolizji nazw metod włączanych poprzez cechy typowe. Na przykład co się stanie, jeśli użyjemy dwóch cech typowych z metodami calculateTax()? trait TaxTools { function calculateTax($price) { return 222; } } trait PriceUtilities { private $taxrate = 17; function calculateTax($price) { return (($this->taxrate/100) * $price); }

65

PHP. OBIEKTY, WZORCE, NARZĘDZIA

// inne składowe } abstract class Service { // składowe i metody do obsługi klas hierarchii Service } class UtilityService extends Service { use PriceUtilities, TaxTools; } $u = new UtilityService(); print $u->calculateTax(100)."\n";

Ponieważ klasa użyła dwóch cech typowych zawierających identycznie nazwaną metodę calculateTax(), PHP nie jest w stanie rozprowadzić wywołania metody do konkretnej implementacji. Skutkiem może być tylko błąd wykonania: Fatal error: Trait method calculateTax has not been applied, because there are collisions with other trait methods on UtilityService in...

Aby temu zaradzić, należy użyć słowa kluczowego insteadof. Stosuje się je w taki sposób: trait TaxTools { function calculateTax($price) { return 222; } } trait PriceUtilities { private $taxrate = 17; function calculateTax($price) { return (($this->taxrate/100) * $price); } // inne składowe } abstract class Service { // składowe i metody do obsługi klas hierarchii Service } class UtilityService extends Service { use PriceUtilities, TaxTools { TaxTools::calculateTax insteadof PriceUtilities; } } $u = new UtilityService(); print $u->calculateTax(100)."\n";

Aby deklarację use uzupełnić o dodatkowe dyrektywy, należy ująć je w nawiasy klamrowe wyznaczające ciało deklaracji use. We wnętrzu tego bloku można użyć operatora insteadof, wymagającego podania po lewej stronie pełnej kwalifikowanej nazwy metody (to znaczy nazwy metody z nazwą klasy rozdzielonych operatorem zakresu). Po prawej stronie insteadof podaje się nazwę cechy typowej, która zostanie zasłonięta w kontekście tej metody. Tak więc zapis: TaxTools::calculateTax insteadof PriceUtilities;

66

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

oznacza: użyj metody calculateTax() z cechy typowej TaxTools zamiast z cechy typowej PriceUtilities.

Tym razem nasz przykład uda się uruchomić i zobaczymy fikcyjne obliczenie podatku według definicji z TaxTools::calculateTax(): 222

Aliasy metod cech typowych Wiemy już, że rozstrzyganie kolizji nazw metod z włączanych cech typowych może polegać na przesłanianiu niektórych metod przez inne. A jeśli zechcemy zachować dostęp do wszystkich kolidujących metod? Rozwiązaniem jest wtedy nadawanie przesłoniętym metodom cech alternatywnych nazw (aliasów) za pomocą operatora as. Ten operator również wymaga podania pełnej kwalifikowanej nazwy metody po lewej stronie i nowej nazwy metody po prawej. Poniżej mamy przykład ponownego udostępnienia metody calculateTax() z cechy typowej PriceUtilities pod nową nazwą basicTax(): trait TaxTools { function calculateTax($price) { return 222; } } trait PriceUtilities { private $taxrate = 17; function calculateTax($price) { return (($this->taxrate/100) * $price); } // inne składowe } abstract class Service { // składowe i metody do obsługi klas hierarchii Service } class UtilityService extends Service { use PriceUtilities, TaxTools { TaxTools::calculateTax insteadof PriceUtilities; PriceUtilities::calculateTax as basicTax; } } $u = new UtilityService(); print $u->calculateTax(100)."\n"; print $u->basicTax(100)."\n";

program da teraz na wyjściu takie wyniki: 222 17

Jak widać, metoda PriceUtilities::calculateTax() została przywrócona do klasy UtilityService pod nową nazwą basicTax().

67

PHP. OBIEKTY, WZORCE, NARZĘDZIA

 Uwaga Kiedy dochodzi do kolizji nazw metod cech typowych, nie wystarczy samo aliasowanie metod w bloku deklaracji use. Najpierw należy ustalić pierwszeństwo kolidujących metod cech typowych za pomocą operatora insteadof. Dopiero potem można przemianować przesłonięte metody za pomocą operatora as.

Przemianowywanie nazw metod użytych cech typowych można wykonywać również wtedy, kiedy nie dochodzi do kolizji nazw. Można dzięki temu „pożyczać” dowolnie nazwane metody cech typowych jako implementacje metod implementowanych interfejsów albo metod dziedziczonych z abstrakcyjnej klasy bazowej.

Cechy typowe z metodami statycznymi Większość dotychczasowych przykładów użycia cech typowych mogła równie dobrze zostać zaimplementowana za pomocą metod statycznych klas pomocniczych, ponieważ żadna z cech typowych nie przechowywała własnych danych egzemplarza klasy. Umieszczanie metod statycznych w cechach typowych nie różni się niczym od definiowania ich w klasach; poniżej zmieniamy składową PriceUtilities::$taxrate i metodę PriceUtilities::calculateTax() na statyczne: trait PriceUtilities { private static $taxrate = 17; static function calculateTax($price) { return ((self::$taxrate/100) * $price); } // inne składowe } class Service { } class UtilityService extends Service { use PriceUtilities; } $u = new UtilityService(); print $u::calculateTax(100)."\n";

Łatwo się domyślić, że na wyjściu programu pojawi się: 17

Jak widać, metody statyczne cech typowych są deklarowane i dostępne w klasie używającej cechy jak zwyczajne metody statyczne tej klasy.

Dostęp do składowych klasy włączającej Wydawałoby się, że w zasadzie użyteczność cech typowych ogranicza się do ich metod statycznych włączanych do implementacji klasy używającej cechy typowej. Nawet metody cech typowych deklarowane jako niestatyczne są przecież z natury statyczne, to znaczy oderwane od egzemplarza klasy włączającej, prawda? Otóż nie, jest dokładnie odwrotnie: metoda cechy typowej może odwoływać się do zmiennych i metod obiektu klasy włączającej: trait PriceUtilities { function calculateTax($price) { // czy to dobry pomysł? return (($this->taxrate/100) * $price); }

68

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

// inne składowe } abstract class Service { // składowe i metody do obsługi klas hierarchii Service } class UtilityService extends Service { public $taxrate = 17; use PriceUtilities; } $u = new UtilityService(); print $u->calculateTax(100)."\n";

Mamy tutaj „dozbrojoną” wersję cechy PriceUtilities, uzupełnioną o odwołanie do składowej obiektu klasy włączającej. Co prawda powstaje pytanie, czy bezpośrednie odwołanie do składowych klasy włączającej jest w ogóle dobrym pomysłem. Otóż jest to pomysł zdecydowanie zły. Sama dostępność zmiennych obiektu w metodzie cechy typowej jest jak najbardziej pożądana i przydatna, ale trudno wymagać od wszystkich klas włączających, aby dostosowały się do wymagania posiadania składowej o nazwie $taxrate. Jak pamiętamy, cechy typowe są przeznaczone do stosowania w wielu klasach, niekoniecznie powiązanych dziedziczeniem. Jaką więc mamy gwarancję albo chociaż jakie jest prawdopodobieństwo, że wszystkie klasy włączające będą definiować składową $taxrate? I jak tego upilnować? Z drugiej strony, byłoby świetnie, gdyby dało się ustanowić kontrakt, na przykład mówiący: „jeśli chcesz stosować tę cechę typową, powinieneś udostępnić u siebie następujące zasoby”. Dokładnie taki efekt jest osiągalny za pomocą metod abstrakcyjnych w cechach typowych.

Definiowanie metody abstrakcyjnej cechy typowej Definiowanie metody abstrakcyjnej w obrębie cechy typowej odbywa się tak samo jak w zwyczajnej klasie. I kiedy tak skonstruowana cecha jest włączana do klasy, klasa ta musi zaimplementować wszystkie abstrakcyjne metody deklarowane przez cechę typową. Możemy więc przerobić poprzedni przykład tak, aby cecha typowa wymuszała udostępnienie danych o stawce podatku: trait PriceUtilities { function calculateTax($price) { // teraz lepiej... wiemy, że klasa włączająca implementuje metodę getTaxRate() return (($this->getTaxRate()/100) * $price); } abstract function getTaxRate(); // inne składowe } abstract class Service { // składowe i metody do obsługi klas hierarchii Service } class UtilityService extends Service { use PriceUtilities; function getTaxRate() { return 17; } } $u = new UtilityService(); print $u->calculateTax(100)."\n";

69

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Deklaracja metody abstrakcyjnej getTaxRate() w deklaracji cechy typowej PriceUtilities wymusza zaimplementowanie tej metody w klasie włączającej UtilityService. Oczywiście skoro PHP nie ogranicza typów wartości zwracanych z metod, to w metodzie UtilityService::calculateTax() nie wiadomo, czy wartość otrzymana z wywołania getTaxRate() jest wartością sensowną. Można temu zaradzić, pisząc rozmaite sprawdziany wartości zwracanych, ale to trochę mija się z celem. Wystarczy chyba zasygnalizować użytkownikowi cechy typowej, że powinien udostępnić potrzebne dane przez implementacje odpowiednich metod narzucanych przez cechę.

Zmiana dostępności metod cech typowych Metody cech typowych mogą być deklarowane jako publiczne, prywatne bądź zabezpieczone. Dostępność metod można jednak zmieniać w klasie włączającej cechę typową. Wiemy już, że operator as może posłużyć do zmiany nazwy metody włączonej z cechą typową. Jeśli po lewej stronie operatora as użyjemy również modyfikatora dostępu do metody, to zamiast nazwy zmienimy dostępność metody dla wywołujących. Wyobraźmy sobie, że chcemy użyć metody calculateTax() we wnętrzu klasy UtilityService, ale nie chcemy tej metody udostępniać na zewnątrz klasy. Odpowiednia deklaracja use wyglądałaby wtedy tak: trait PriceUtilities { function calculateTax($price) { return (($this->getTaxRate()/100) * $price); } abstract function getTaxRate(); // inne składowe } abstract class Service { // składowe i metody do obsługi klas hierarchii Service } class UtilityService extends Service { use PriceUtilities { PriceUtilities::calculateTax as private; } private $price; function __construct($price) { $this->price = $price; } function getTaxRate() { return 17; } function getFinalPrice() { return ($this->price + $this->calculateTax($this->price)); } } $u = new UtilityService(100); print $u->getFinalPrice()."\n";

Użyliśmy tu operatora as w połączeniu ze słowem private w celu zablokowania dostępu do metody calculateTax() dla użytkowników zewnętrznych. Mamy więc dostęp do tej metody we własnej metodzie getFinalPrice(), ale próba wywołania metody calculateTax() z zewnątrz: $u = new UtilityService(100); print $u->calculateTax()."\n";

doprowadzi do błędu wykonania: Fatal error: Call to private method UtilityService::calculateTax() from context '' in ...

70

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

Późne wiązanie statyczne: słowo static Skoro znamy już klasy abstrakcyjne, cechy typowe i interfejsy, wrócimy na chwilę do metod statycznych. Wiadomo już, że metoda statyczna może pełnić rolę „wytwórni”, czyli generatora instancji danej klasy. Leniwy programista będzie zapewne kręcił nosem na definiowanie statycznej wytwórni w każdej klasie wyprowadzonej z klasy abstrakcyjnej, jak tutaj: abstract class DomainObject { } class User extends DomainObject { public static function create() { return new User(); } } class Document extends DomainObject { public static function create() { return new Document(); } }

Klasą bazową jest tu klasa o nazwie DomainObject. Normalnie byłaby w niej zaimplementowana funkcjonalność wspólna dla wszystkich klas pochodnych. Następnie utworzyliśmy dwie klasy pochodne User i Document. W każdej z nich chcielibyśmy mieć statyczne metody create().  Uwaga Po co stosować statyczne metody „wytwórcze”, skoro do tworzenia obiektów z powodzeniem może służyć konstruktor? W rozdziale 13. poznasz wzorzec projektowy o nazwie Identity Map („mapa tożsamości”). Realizacja tego wzorca zarządza obiektami danej klasy i generuje nowy obiekt tylko wtedy, kiedy w zbiorze już utworzonych obiektów nie istnieje obiekt o identycznych cechach (decydujących o jego „tożsamości”). Jeśli szukany obiekt już istnieje, jest zwracany. Metoda wytwórcza create() byłaby modelowym klientem takiego komponentu (i innych, podobnych komponentów zarządzających obiektami).

Powyższy kod jest w porządku, ale prowadzi do irytującego powielania podobnych metod. Nie chcemy sztucznie rozdmuchiwać kodu źródłowego podobnym kodem powtarzanym w każdej klasie pochodnej implementującej klasę DomainObject. Może lepiej umieścić metodę create() w klasie bazowej? abstract class DomainObject { public static function create() { return new self(); } } class User extends DomainObject { } class Document extends DomainObject { } Document::create();

Cóż, wygląda nieźle. Mamy teraz kod wspólny dla klas pochodnych w jednym miejscu w klasie bazowej; dla rozróżnienia typu tworzonego obiektu stosujemy słowo self. Zakładamy jednak, że słowo self działa dla klas tak samo jak $this dla obiektów — a to nie jest do końca prawda. Słowo self nie odnosi się do kontekstu wywołania; odnosi się do kontekstu realizacji odwołania. Innymi słowy, wykonanie poprzedniego przykładu sprowokowałoby taki błąd: PHP Fatal error: Cannot instantiate abstract class DomainObject in ....

71

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Jak widać, self odnosi się do klasy DomainObject, czyli do miejsca, w którym zdefiniowaliśmy metodę create(), a nie do miejsca, z którego ją wywołujemy (klasa Document). Aż do wersji PHP 5.3 było to poważnym ograniczeniem, które prowokowało powstawanie rozmaitych udanych i mniej udanych obejść. W PHP 5.3 pojawił się natomiast mechanizm późnego wiązania składowych statycznych. Najważniejszym objawem działania tego mechanizmu jest nowe słowo kluczowe języka: static. Słowo to ma znaczenie podobne do self, ale odnosi się do kontekstu użycia, a nie do kontekstu klasy zawierającej użycie. W naszym przypadku użycie słowa static oznaczałoby, że wywołanie metody Document::create() utworzyłoby nowy obiekt klasy Document, ale nie stanowiłoby próby utworzenia obiektu klasy DomainObject. Możemy więc wykorzystać zależności dziedziczenia ze słowem static: abstract class DomainObject { public static function create() { return new static(); } } class User extends DomainObject { } class Document extends DomainObject { } print_r(Document::create()); Document Object ( )

Słowo static może być stosowane również poza kontekstem tworzenia obiektów klas. Możemy go używać tak samo jak słów kluczowych self i parent — w roli identyfikatora przy wywołaniach metod statycznych, nawet w kontekście niestatycznym. Załóżmy, że w klasie DomainObject chcemy wyróżniać grupy (czy też kategorie) obiektów: w nowej klasyfikacji wszystkie obiekty klas mają być zaliczane do kategorii „domyślne”, ale w niektórych gałęziach hierarchii dziedziczenia chcielibyśmy nadawać obiektom inne grupowanie: abstract class DomainObject { private $group; public function __construct() { $this->group = static::getGroup(); } public static function create() { return new static(); } static function getGroup() { return "default"; } } class User extends DomainObject { } class Document extends DomainObject { static function getGroup() { return "document"; } }

72

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

class SpreadSheet extends Document { } print_r(User::create()); print_r(SpreadSheet::create());

Do klasy DomainObject wprowadziliśmy konstruktor. Wykorzystujemy w nim słowo static do wywołania metody statycznej getGroup(). Klasa DomainObject zawiera domyślną implementację tej metody, ale w klasie Document zostaje ona przesłonięta inną wersją. Utworzyliśmy też nową klasę SpreadSheet, która rozszerza klasę Document. Oto wynik wykonania powyższego kodu: User Object ( [group:DomainObject:private] => default ) SpreadSheet Object ( [group:DomainObject:private] => document )

W przypadku klasy User rzecz jest oczywista: konstruktor klasy bazowej DomainObject wywołuje metodę statyczną; wywołanie jest kierowane do lokalnej wersji tej metody w klasie DomainObject. Ale w przypadku klasy SpreadSheet poszukiwanie metody do wywołania rozpoczyna się od klasy wywołującej, czyli od klasy SpreadSheet. Skoro nie zawiera ona implementacji wywoływanej metody, poszukiwanie trwa dalej w klasach nadrzędnych, aż do realizacji wywołania za pomocą metody getGroup() w klasie Document. Przed PHP 5.3 i mechanizmem późnego wiązania składowych statycznych moglibyśmy co najwyżej użyć słowa self, a więc ograniczylibyśmy poszukiwanie metody getGroup() wyłącznie do klasy DomainObject.

Obsługa błędów Nic nie jest doskonałe. Pliki są mylone, bazy danych pozostają niezainicjalizowane, adresy URL się zmieniają, pliki XML psują, uprawnienia są źle ustawiane, a limity dyskowe przekraczane — to normalne. W walce z niespodziankami i w przewidywaniu przyszłych problemów prosta z początku metoda może się załamać pod ciężarem kodu obsługi błędów. Oto prosta klasa Conf, która przechowuje, czyta i zapisuje plik konfiguracyjny XML: class Conf { private $file; private $xml; private $lastmatch; function __construct($file) { $this->file; $this->xml = simplexml_load_file($file); } function write() { file_put_contents($this->file, $this->xml->asXML()); } function get($str) { $matches = $this->xml ->xpath("/conf/item[@name=\"$str\"]"); if (count($matches)) { $this->lastmatch = $matches[0];

73

PHP. OBIEKTY, WZORCE, NARZĘDZIA

return (string)$matches[0]; } return null; } function set($key, $value) { if (! is_null($this->get($key))) { $this->lastmatch[0]=$value; return; } $conf = $this->xml->conf; $this->xml->addChild('item', $value) ->addAttribute('name', $key); } }

Klasa Conf używa rozszerzenia SimpleXml i odwołuje się do par klucz i wartość w pliku XML. Ma ona przetwarzać pliki następującego formatu:

bob newpass localhost

Konstruktor klasy Conf przyjmuje w wywołaniu ścieżkę do pliku, którą przekazuje do funkcji simplexml_load_file(). Wynikowy obiekt klasy SimpleXmlElement przechowuje we właściwości $xml. Metoda get() na bazie XPath lokalizuje element o podanej nazwie i zwraca jego wartość. Metoda set() zmienia wartość istniejącego elementu, ewentualnie dodaje do dokumentu nowy element. Wreszcie metoda write() wypisuje dokument konfiguracyjny z powrotem do pliku. Jak w większości przykładowego kodu klasa Conf jest mocno uproszczona. W szczególności nie ma możliwości obsługi nieistniejących albo niemodyfikowalnych plików. Jest też wielce „optymistyczna” co do otoczenia programu — „zakłada”, że odczytany z pliku dokument XML będzie prawidłowo zbudowany i że będzie zawierał oczekiwane elementy. Wykrywanie tego rodzaju błędów jest dość proste, ale trzeba jeszcze decydować o strategii postępowania w przypadku ich stwierdzenia. Ogólnie mamy w tym zakresie dwie możliwości. Pierwszą z nich jest przerwanie wykonywania programu. To metoda prosta i skuteczna, ale wyjątkowo drastyczna. Nie należy zostawiać decyzji o kontynuowaniu albo przerwaniu wykonania programu pierwszej lepszej klasie. Choć metody __construct() i write() znakomicie sprawdzają się w wykrywaniu błędów, nie ma w nich dostępu do informacji niezbędnych do podjęcia decyzji co do sposobu obsługi błędów. Zamiast obsługiwać błąd wewnątrz klasy, moglibyśmy spróbować zasygnalizować problem otoczeniu zewnętrznemu, ustawiając jakiś znacznik. Może być nim wartość logiczna albo liczba całkowita ustawiana na umowną wartość, jak 0 czy –1. Niektóre klasy mogą również ustawiać dodatkowo ciąg znaków prezentowany użytkownikowi zainteresowanemu bliższymi szczegółami okoliczności wystąpienia błędu. W wielu pakietach PEAR stosowana jest kombinacja obu tych metod, zakładająca zwracanie wywołującemu obiektu błędu (egzemplarz klasy PEAR_Error); obiekt ten występuje w roli sygnalizatora błędu, jak i nośnika rozszerzonych informacji o jego okolicznościach. Takie podejście obecnie zarzucono, ale wiele klas nie zostało unowocześnionych również dlatego, że wymagałoby to z kolei uaktualnienia mnóstwa kodu klienckiego tych klas, polegającego na takim sygnalizowaniu błędów. Problem polega tu na zaśmiecaniu dziedziny wartości zwracanych. W języku PHP nie można narzucić metodzie konkretnego typu wartości zwracanej. W czasie przygotowywania tej publikacji nie było jeszcze w PHP możliwości wymuszania typu zwracanego — na wzór wymuszania typu argumentu. Tym samym nie można mieć pewności, że metoda zwróci oczekiwany obiekt czy wartość skalarną, a nie znacznik czy obiekt błędu. Twórca metody musi zaś polegać na sumienności użytkownika wywołującego metodę — powinien on bowiem na własną rękę sprawdzić otrzymaną wartość i jej typ. Niestety, nie zawsze można ufać użytkownikowi.

74

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

Kiedy zwracamy wywołującemu kod błędu, nie mamy żadnej gwarancji, że jego odbiorca będzie w choć trochę większym stopniu kompetentny do obsługi wykrytego błędu. Jeśli nie będzie, będziemy mieli powtórkę z rozrywki — wywołujący będzie musiał określić własną strategię reakcji na błędy, a nie musi ona wcale być zgodna ze stosowaną w naszej metodzie.

Wyjątki Wersja 5 wprowadziła do języka PHP wyjątki, które zasadniczo zmieniają sposób obsługi błędów, czyniąc go zupełnie nietypowym — nietypowym dla tradycyjnego PHP. Wyjątki PHP przypominają bardzo te znane z C++ czy Javy i pozwalają na wyeliminowanie wszystkich opisywanych wyżej kłopotów. Wyjątek jest specjalnym obiektem, egzemplarzem wbudowanej klasy Exception (albo jej klasy pochodnej). Obiekty klasy Exception służą do przechowywania i zgłaszania informacji o błędach. Konstruktor klasy Exception przyjmuje dwa opcjonalne argumenty: ciąg komunikatu o błędzie oraz kod błędu. Pozostałe metody klasy dają zaś możliwość analizy okoliczności wystąpienia błędu. Metody te wymieniono w tabeli 4.1. Tabela 4.1. Metody publiczne klasy wyjątku Metoda

Opis

getMessage()

Zwraca ciąg komunikatu przekazany do konstruktora obiektu wyjątku.

getCode()

Zwraca kod błędu przekazany do konstruktora obiektu wyjątku.

getFile()

Zwraca nazwę pliku, który był wykonywany, gdy doszło do wygenerowania wyjątku.

getLine()

Zwraca numer wiersza kodu, w którym wygenerowano wyjątek.

getPrevious()

Zwraca zagnieżdżony obiekt wyjątku (klasy Exception).

getTrace()

Zwraca wielowymiarową tablicę ze śladem wywołań metod prowadzących do wyjątku; ślad obejmuje metodę, klasę, plik i argumenty wywołania.

getTraceAsString()

Zwraca dane zwracane przez getTrace() w postaci ciągu znaków.

__toString()

Wywoływana automatycznie, jeśli obiekt klasy Exception jest wykorzystywany w kontekście ciągu znaków (zwraca ciąg opisujący szczegóły wyjątku).

Klasa Exception świetnie sprawdza się w sygnalizowaniu błędów i ich diagnostyce (szczególnie przydatne są w niej metody getTrace() i getTraceAsString()). Klasa ta cechuje się zresztą znacznym podobieństwem do wspominanej już klasy PEAR_Error. Jednak wyjątek to nie tylko klasa opisująca okoliczności wystąpienia błędu.

Zrzucanie wyjątku Zrzucenie wyjątku realizuje się instrukcją zawierającą słowo kluczowe throw i obiekt klasy Exception w roli jego operandu. Instrukcja throw powoduje przerwanie wykonania bieżącej metody oraz przekazanie sterowania wraz z odpowiedzialnością za obsługę błędu do wywołującego. Uzupełnijmy metodę __construct() o instrukcję zrzucającą wyjątek: function __construct($file) { $this->file = $file; if (! file_exists($file)) { throw new Exception("plik '$file' nie istnieje"); } $this->xml = simplexml_load_file($file); }

75

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Podobnej konstrukcji można użyć w metodzie write(): function write() { if (! is_writeable($this->file)) { throw new Exception( "plik '{$this->file}' nie da się zapisać"); } file_put_contents($this->file, $this->xml->asXML()); }

Nasze metody __construct() i write() mogą teraz kontrolować wystąpienia błędów, ale nie podejmują zadania ich obsługi, pozostawiając ją wywołującemu, który najprawdopodobniej ma do tej obsługi więcej kompetencji. W jaki sposób wywołujący może przechwycić i obsłużyć zrzucony wyjątek? Otóż powinien wywołanie metody potencjalnie zrzucającej wyjątki umieścić w bloku kodu chronionego, czyli wewnątrz klauzuli try. Klauzula try składa się ze słowa kluczowego try oraz nawiasów klamrowych ograniczających blok kodu chronionego. Klauzula try musi być uzupełniona przynajmniej jedną klauzulą przechwytującą wyjątki — klauzulą catch, w ciele której umieszcza się kod obsługi wyjątków: try { $conf = new Conf(dirname(__FILE__)."/conf01.xml"); print "user: ".$conf->get('user')."\n"; print "host: ".$conf->get('host')."\n"; $conf->set("pass", "newpass"); $conf->write(); } catch(Exception $e) { die($e->__toString() ); }

Jak widać, klauzula catch przypomina nieco deklarację metody. W momencie zrzucania wyjątku wewnątrz bloku kodu chronionego wywoływana jest klauzula catch uzupełniająca ten blok. Do ciała klauzuli catch automatycznie przekazywany jest obiekt zrzuconego wyjątku. Wykonanie kodu bloku chronionego jest przerywane natychmiast po zrzuceniu wyjątku (podobnie jak wewnątrz metody zrzucającej wyjątek) — sterowanie jest przekazywane wprost do klauzuli przechwytującej wyjątek.

Specjalizowanie klasy wyjątku Klasę wyjątku można specjalizować tak jak każdą inną klasę — wyprowadzając jej pochodną. Specjalizacje są uzasadnione w dwóch przypadkach. Po pierwsze, można w ten sposób rozszerzać funkcjonalność klasy wyjątku. Po drugie, fakt definiowania przez klasę pochodną nowego typu może zwiększyć efektywność obsługi błędów. Na potrzeby klauzuli try można definiować dowolną liczbę klauzul catch. Przekazanie sterowania do jednej z nich uzależnione będzie wtedy od dopasowania typu obiektu wyjątku do deklaracji tegoż typu na liście argumentów klauzuli catch. Zdefiniujmy na własny użytek kilka klas pochodnych klasy Exception: class XmlException extends Exception { private $error; function __construct(LibXmlError $error) { $shortfile = basename($error->file); $msg = "[{$shortfile}, wiersz {$error->line}, ". "kolumna {$error->column}] {$error->message}"; $this->error = $error; parent::__construct($msg, $error->code); }

}

function getLibXmlError() { return $this->error; }

class FileException extends Exception { } class ConfException extends Exception { }

76

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

Kiedy rozszerzenie SimpleXml zostanie użyte wobec popsutego pliku XML, w tle zostanie wygenerowany obiekt klasy LibXmlError. Obiekt ten zawiera składowe message oraz code i bardzo przypomina ogólną klasę wyjątków Exception. Z tego podobieństwa można skorzystać i wygodnie użyć obiektu LibXmlError w klasie XmlException. Klasy FileException i ConfException nie robią nic ponad utworzenie klasy pochodnej dla Exception. Teraz możemy wszystkich tych klas użyć w kodzie i wzmocnić nimi metody __construct() i write(): // klasa Conf function __construct($file) { $this->file = $file; if (! file_exists($file)) { throw new FileException("plik '$file' nie istnieje"); } $this->xml = simplexml_load_file($file, null, LIBXML_NOERROR); if (! is_object($this->xml)) { throw new XmlException(libxml_get_last_error()); } print gettype( $this->xml ); $matches = $this->xml->xpath("/conf"); if ( ! count( $matches ) ) { throw new ConfException( "nie można odnaleźć elementu: conf" ); } } function write() { if (! is_writeable($this->file)) { throw new FileException( "plik '{$this->file}' nie da się zapisać"); } file_put_contents($this->file, $this->xml->asXML()); }

Metoda __construct() zrzuca wyjątek XmlException, FileException lub ConfException, zależnie od rodzaju wykrytego błędu. Zauważmy, że do wywołania funkcji simplexml_load_file() przekazaliśmy znacznik LIBXML_NOERROR. W ten sposób tłumimy ostrzeżenia generowane w funkcji, pozwalając sobie obsłużyć je później w klasie XmlException. W przypadku napotkania źle zbudowanego pliku XML będziemy o tym wiedzieć, ponieważ funkcja simplexml_load_file() nie zwróci obiektu. Wtedy możemy dokonać inspekcji okoliczności błędu za pomocą funkcji libxml_get_last_error(). Metoda write() zrzuca wyjątek FileException, jeśli składowa $file odnosi się do pliku niemodyfikowalnego. Wiadomo więc, że wywołanie konstruktora __construct() może doprowadzić do zrzucenia jednego z trzech możliwych wyjątków. Jak z tej wiedzy skorzystać? Oto kod tworzący obiekt klasy Conf: class Runner { static function init() { try { $conf = new Conf(dirname(__FILE__)."/conf01.xml"); print "user: ".$conf->get('user')."\n"; print "host: ".$conf->get('host')."\n"; $conf->set("pass", "newpass"); $conf->write(); } catch ( FileException $e ) { // problem z dostępem do pliku } catch ( XmlException $e ) { // popsuty dokument XML } catch ( ConfException $e ) { // zły rodzaj pliku XML

77

PHP. OBIEKTY, WZORCE, NARZĘDZIA

} catch ( Exception $e ) { // nie powinno dojść do tego wywołania } } }

Dla każdego typu wyjątku stosujemy osobną klauzulę catch. To, która z nich zostanie wywołana w obliczu wystąpienia wyjątku w bloku kodu chronionego, zależy od kolejności klauzul i dopasowania wymuszanych w nich typów wyjątków do typu wyjątku zrzuconego. Ostatecznie wykonywany jest kod pierwszej pasującej klauzuli. Trzeba więc pamiętać o umieszczaniu klauzul przechwytujących i obsługujących wyjątki najbardziej specjalizowane na początku; dopiero za nimi należałoby umieszczać klauzule wyjątków ogólniejszych. Gdyby bowiem w powyższym kodzie przestawić kolejność klauzul tak, aby klauzula przechwytująca wyjątek Exception wyprzedzała klauzule dla wyjątków klas XmlException i ConfException, żadna z tych dwóch klauzul nie zostałaby nigdy wywołana. Każda klasa wyjątku jest bowiem specjalizacją klasy Exception, a typ każdego wyjątku jest zgodny z typem Exception, więc klauzula catch (Exception $e) zostanie dopasowana do dosłownie każdego wyjątku. Pierwsza klauzula catch (dla wyjątków typu FileException) zostanie wywołana w przypadku problemów z plikiem konfiguracyjnym (plik nie istnieje albo jest niemodyfikowalny). Druga klauzula (dla XmlException) zostanie uruchomiona w przypadku błędu parsowania pliku XML (np. w przypadku braku zamknięcia elementu). Trzecia klauzula (ConfException) zostanie wywołana dla takich (poprawnie zbudowanych) plików XML, które nie zawierają oczekiwanego elementu głównego conf. Wreszcie ostatnia klauzula catch (dla wyjątków typu Exception) nie powinna nigdy zostać wykonana, ponieważ z wywołanych w bloku try metod zrzucane są wyjątki tylko trzech typów, wszystkie jawnie obsłużone w poprzednich klauzulach catch. Warto często umieszczać takie zabezpieczenie na końcu listy klauzul przechwytujących, na wypadek przyszłego rozszerzenia zestawu zrzucanych wyjątków. Zaletą celowanych klauzul przechwytywania wyjątków jest możliwość zastosowania w nich odmiennych, specjalizowanych procedur obsługi dla różnych błędów. W zależności od wyjątku możemy na przykład zdecydować o zakończeniu wykonania programu, zapisaniu błędu do dziennika aplikacji i kontynuowaniu wykonania albo wręcz jawnie przerzucić wyjątek do kodu klienckiego: try { // ... } catch ( FileException $e ) { throw $e; }

Możemy też zastosować sztuczkę ze zrzuceniem nowego wyjątku, zawierającego w sobie wyjątek przechwycony. Pozwala to na częściowe zaimplementowanie obsługi błędu, uzupełnienie informacji diagnostycznych przy zachowaniu kompletu danych zawartych w oryginalnym wyjątku. Więcej o tej technice dowiesz się z rozdziału 15. Co się stanie, kiedy wyjątek nie zostanie przechwycony jawnie? Zostanie niejawnie przerzucony w górę stosu wywołań, co da szansę obsłużenia błędu w kodzie klienckim. Procedura ta będzie stosowana do momentu przechwycenia wyjątku albo braku możliwości dalszego przerzucania w górę stosu wywołań. W tym drugim przypadku program zostanie zakończony z błędem. Gdybyśmy nie przechwycili jednego z wyjątków z naszego przykładu, to w przypadku błędu doczekalibyśmy się następującego komunikatu: PHP Fatal error: Uncaught exception 'FileException' with message 'plik 'nonexistent/not_there.xml' nie istnieje' in ...

Zrzucając wyjątek, zmuszamy użytkownika naszego kodu do wzięcia na siebie odpowiedzialności za jego obsługę. Owego przerzucenia odpowiedzialności nie należy interpretować jako lenistwa — twórca metody nie ma niekiedy innego wyjścia, jak tylko przerzucić wyjątek do wywołującego, ponieważ nie dysponuje informacjami niezbędnymi do właściwej z punktu widzenia wywołującego obsługi błędu. We wnętrzu metody write() naszej klasy możemy jedynie wykryć błąd i określić przyczynę (tutaj: błędy przetwarzania pliku XML), nie wiadomo jednak, jak taki błąd obsłużyć. I nie może dziwić przekazanie decyzji do wywołującego — to zupełnie zasadne. Z kolei gdybyśmy wyposażyli klasę Conf w większą „wiedzę” konieczną do kompletnego obsłużenia potencjalnych sytuacji wyjątkowych, stałaby się mniej poręczna i mniej elastyczna.

78

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

Finalizacja obsługi wyjątków: słowo finally Sposób, w jaki wyjątki wpływają na przebieg wykonania programu, może prowadzić do nieoczekiwanych problemów. Po zrzuceniu wyjątku może na przykład nie dojść do wykonania kodu porządkującego stan programu, umieszczonego w bloku try. Wiemy już, że jeżeli wyjątek zostanie zrzucony w obrębie bloku try, sterowanie jest natychmiast przekazywane do odpowiedniej klauzuli catch. Niekoniecznie znajduje się tam jednak kod zamykający połączenie z bazą danych czy otwarte pliki, niekoniecznie wykona się też umieszczony dalej w bloku try kod aktualizujący informacje o stanie przetwarzania. Wyobraźmy sobie na przykład, że metoda Runner::init() prowadzi rejestr wykonanych operacji. Rejestruje w nim początek procesu inicjalizacji, wszelkie napotkane błędy i wreszcie zakończenie procesu inicjalizacji. Oto typowy, uproszczony przykład tego rodzaju dziennika operacji: class Runner { static function init() { try { $fh = fopen("./log.txt","a"); fputs( $fh, "start\n" ); $conf = new Conf( dirname(__FILE__)."/conf.broken.xml" ); print "user: ".$conf->get('user')."\n"; print "host: ".$conf->get('host')."\n"; $conf->set("pass", "newpass"); $conf->write(); fputs( $fh, "end\n" ); fclose( $fh ); } catch ( FileException $e ) { fputs( $fh, "file exception\n" ); //...

Metoda otwiera plik log.txt, zapisuje do niego wstępne dane, a następnie wywołuje kod konfigurujący. Jeśli wykonanie tego kodu zostanie przerwane przez wyjątek, zostanie to odnotowane w odpowiedniej klauzuli catch. Rejestracja kończy się jeszcze w bloku try poprzez zapis danych do pliku i zamknięcie uchwytu pliku. Rzecz jasna ten ostatni krok rejestracji operacji nie zostanie wykonany, jeśli wyjątek zostanie zgłoszony wcześniej. Sterowanie zostanie przekazane wprost do klauzuli catch i cała niewykonana jeszcze część bloku try nie zostanie wcale wykonana. Jeśli dojdzie do wyjątku operacji na pliku, plik dziennika będzie wyglądał tak: start file exception

Jak widać, metoda rozpoczęła rejestrowanie operacji i sam wyjątek obsługi pliku został odnotowany, ale już fragment kodu sygnalizujący koniec rejestracji nie został wykonany i w logu brakuje wpisu o tym zdarzeniu. Mogłoby się wydawać, że rozwiązaniem jest przeniesienie ostatnich faz procesu rejestrowania operacji poza blok try-catch. Byłoby to jednak rozwiązanie niestabilne. Niby w przypadku pojawienia się wyjątku po wykonaniu kodu z klauzuli catch sterowanie jest przenoszone poza blok try-catch, ale przecież w klauzuli catch wyjątek może zostać przerzucony dalej; ba, obsługa wyjątku może polegać nawet na przerwaniu wykonywania programu. W PHP 5.5 wprowadzono mechanizmy pomagające w zarządzaniu przepływem sterowania w obliczu wyjątków, udostępniając nową klauzulę finally. Programiści znający język Java znają podobną klauzulę. Otóż klauzule catch są wykonywane warunkowo, to znaczy tylko kiedy z bloku try zostanie zrzucony pasujący wyjątek. Natomiast kod z klauzuli finally wykonywany jest zawsze, niezależnie od tego, czy do wyjątku doszło, czy nie. Możemy więc naprawić niedziałające logowanie zdarzeń, przenosząc operację zapisu dziennika i zamknięcia pliku do klauzuli finally:

79

PHP. OBIEKTY, WZORCE, NARZĘDZIA

class Runner { static function init() { $fh = fopen("./log.txt","w"); try { fputs( $fh, "start\n" ); $conf = new Conf( dirname(__FILE__)."/conf.broken.xml" ); print "user: ".$conf->get('user')."\n"; print "host: ".$conf->get('host')."\n"; $conf->set("pass", "newpass"); $conf->write(); } catch ( FileException $e ) { // problem uprawnień albo braku pliku fputs( $fh, "file exception\n" ); throw $e; } catch ( XmlException $e ) { fputs( $fh, "xml exception\n" ); // niepoprawny xml } catch ( ConfException $e ) { fputs( $fh, "conf exception\n" ); // niewłaściwy plik XML } catch ( Exception $e ) { fputs( $fh, "general exception\n" ); // inny błąd: nie powinno się to zdarzyć } finally { fputs( $fh, "end\n" ); fclose( $fh ); } } }

Ponieważ sam zapis do dziennika i zamknięcie pliku dziennika zostały przeniesione do bloku finally, zostaną wykonane nawet po zrzuceniu wyjątku FileException, który jest z klauzuli catch przerzucany dalej. Tym razem zawartość pliku dziennika będzie kompletna: start file exception end

 Uwaga Kod z klauzuli finally zostanie wykonany również wtedy, kiedy wyjątek chwycony do klauzuli catch zostanie przerzucony dalej albo zakończony zwróceniem wartości. Jeśli jednak w wykonanym bloku try albo catch wystąpi wywołanie die() albo exit(), wykonanie programu zostanie przerwane bezwarunkowo i kod z klauzuli finally w ogóle nie zostanie wykonany.

Klasy i metody finalne Dziedziczenie daje w ramach hierarchii klas niezrównaną elastyczność. Można dzięki niemu przesłaniać klasy i metody, tak aby efekty wywołania metody przez użytkownika na rzecz pewnego obiektu były uzależnione od konkretnej klasy tego obiektu. Niekiedy jednak owa elastyczność jest zbędna albo wręcz niepożądana i zachodzi konieczność zablokowania hierarchii dla dalszych rozszerzeń. Jeśli uznasz, że rozwój hierarchii dobiegł końca i dalsze jej rozbudowywanie byłoby szkodliwe, możesz uciec się do zablokowania rozrostu gałęzi hierarchii słowem kluczowym final. 80

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

Słowo final to dla hierarchii znak stop. Klasa finalna nie może uczestniczyć w dalszym dziedziczeniu. Mniej drastyczna blokada słowem final pojedynczej metody uniemożliwia jej przesłanianie w klasach pochodnych. Zadeklarujmy klasę finalną: final class Checkout { // … }

Próba wyprowadzenia klasy pochodnej z klasy Checkout: class IllegalCheckout extends Checkout { // ... }

Doprowadzi do błędu wykonania programu: PHP Fatal error: Class IllegalCheckout may not inherit from final class (Checkout) in ...

Ograniczenie rozwoju gałęzi hierarchii możemy nieco rozluźnić, nakładając je nie na całą klasę, ale na pojedyncze metody tej klasy. Słowo kluczowe final powinno w deklaracjach takich metod poprzedzać pozostałe modyfikatory (np. modyfikatory widoczności i statyczności), jak poniżej: class Checkout { final function totalize() { // Oblicz rachunek… } }

Teraz możemy wyprowadzić pochodną klasy Checkout, ale nie możemy w niej przesłonić metody totalize() — próba taka zakończy się krytycznym błędem programu (nie można przesłonić metody finalnej): class IllegalCheckout extends Checkout { final function totalize() { // Zmień sposób liczenia rachunku… } } Fatal error: Cannot override final method Checkout::totalize() in...

Prawidłowy obiektowy kod powinien kłaść nacisk na dobrze zdefiniowane interfejsy. Interfejsy te mogą jednak skrywać zmienne implementacje. Dopuszczalne są w takim układzie klasy czy zestawy klas odpowiadające wspólnemu interfejsowi, ale w różnych okolicznościach zachowujące się odmiennie. Deklaracja metody albo klasy jako finalnej ogranicza tę elastyczność. Niekiedy jest to całkiem uprawnione (w dalszej części książki prezentowane będą przykłady takich sytuacji), ale zazwyczaj przed zastosowaniem słowa kluczowego final należy się głęboko zastanowić nad jego zasadnością. Czy naprawdę nie da się wyobrazić sobie czy sprowokować okoliczności, w których należałoby wykorzystać przesłanianie? Zawsze też można oznaczyć klasy i metody jako finalne dopiero później, kiedy okaże się to zasadne „w praniu”. Nie sposób jednak przewidzieć wszystkich zastosowań tworzonych hierarchii, zwłaszcza tych wchodzących w skład bibliotek przeznaczonych do powszechnego użytku. Słowo final powinno być ostatecznością.

Przechwytywanie chybionych wywołań PHP udostępnia wbudowane metody przechwytujące (ang. interceptor methods) służące do przechwytywania wywołań niezdefiniowanych metod i składowych. Przechwytywanie to nosi również miano przeciążania, ale ponieważ znaczenie tego terminu jest w PHP odmienne od przyjętego w językach C++ i Java, będziemy mówić raczej o przechwytywaniu i tłumieniu wywołań i odwołań. 81

PHP. OBIEKTY, WZORCE, NARZĘDZIA

PHP5 udostępnia w klasach wbudowane metody przechwytujące chybione wywołania. Podobnie jak wbudowana metoda konstrukcji __construct(), tak i one wymagają spełnienia pewnych warunków. Zestawienie metod zawiera tabela 4.2. Tabela 4.2. Metody przechwytujące Metoda

Opis

__get($składowa)

Wywoływana w obliczu odwołania do niezdefiniowanej składowej klasy.

__set($składowa, $wartość)

Wywoływana w obliczu próby przypisania wartości do niezdefiniowanej składowej klasy.

__isset($składowa)

Wywoływana w przypadku wywołania isset() na rzecz niezdefiniowanej składowej.

__unset($składowa)

Wywoływana w przypadku wywołania unset() na rzecz niezdefiniowanej składowej.

__call($metoda, $argumenty)

Wywoływana w obliczu próby wywołania niezdefiniowanej niestatycznej metody klasy.

__callStatic($metoda, $argumenty)

Wywoływana w obliczu próby wywołania niezdefiniowanej statycznej metody klasy.

Metody __get() i __set() służą do wychwytywania prób odwołań do składowych, których nie zadeklarowano w klasie (ani w jej klasach bazowych). Metoda __get() wywoływana jest w obliczu próby odczytu wartości niezadeklarowanej składowej. Jej wywołanie następuje w pełni automatycznie, a towarzyszy mu pojedynczy argument w postaci ciągu znaków, zawierającego nazwę problematycznej składowej. To, co zostanie zwrócone z tej metody, zostanie przekazane do wywołującego, przy czym nie będzie on w stanie stwierdzić, czy jego odwołanie do składowej było skuteczne, czy sprowokowało wywołanie __get(). Oto przykład prostej obsługi takiego odwołania: class Person { function __get($property) { $method = "get{$property}"; if (method_exists($this->, $method)) { return $this->$method(); } } function getName() { return "Bob"; } function getAge() { return 44; } }

Wedle powyższego kodu, kiedy użytkownik spróbuje odwołać się do niezdefiniowanej składowej, nastąpi wywołanie metody __get(), w ramach której będzie miała miejsce próba skonstruowania (przez poprzedzenie nazw składowej ciągiem get) nazwy metody akcesora tejże składowej. Następnie tak skonstruowany ciąg jest przekazywany do funkcji method_exists(), która sprawdza obecność zadanej metody w klasie wskazanego obiektu. Jeśli okaże się, że taka metoda istnieje, wywołanie jest do niej delegowane, a użytkownikowi prezentowana jest jej wartość zwracana. Jeśli więc użytkownik odwoła się do składowej $name: $p = new Person(); print $p->name;

82

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

to w tle nastąpi wywołanie metody __get(), a z jej wnętrza — metody getName(). Bob

Gdyby jednak akcesor żądanej składowej nie istniał, odwołanie do niej dałoby wartość pustą (NULL). Metoda __isset() działa podobnie jak metoda __get(). Jest wywoływana w przypadku, kiedy kod kliencki odwoła się do niezdefiniowanej składowej w kontekście wywołania funkcji isset(). Moglibyśmy więc rozszerzyć klasę Person następująco: function __isset($property) { $method = "get{$property}"; return (method_exists($this, $method)); }

Teraz ostrożny użytkownik naszej klasy może zbadać dostępność składowej jeszcze przed próbą odwołania się do niej: if (isset($p->name)) { print $p->name; }

Metoda __set() wywoływana jest z kolei przy próbach przypisywania wartości do nieistniejącej składowej. W jej wywołaniu przekazywane są automatycznie dwa argumenty: nazwa składowej i wartość, która miałaby zostać do niej przypisana. O sposobie wykorzystania obu argumentów decyduje twórca implementacji metody __set(). Spójrzmy, jak mogłaby ona wyglądać w klasie Person: class Person { private $_name; private $_age; function __set($property, $value) { $method = "set{$property}"; if (method_exists($this, $method)) { return $this->$method($value); } } function setName($name) { $this->_name = $name; if (! is_null($name)) { $this->_name = strtoupper($this->_name); } } function setAge($age) { $this->_age = strtoupper($age); } }

Gdy użytkownik spróbuje przypisać wartość do niezdefiniowanej składowej obiektu klasy w przypadku takiej implementacji klasy, wywoływana jest metoda __set() z argumentami reprezentującymi nazwę składowej i z przypisywaną do niej wartością. W implementacji __set() sprawdzamy istnienie odpowiedniej metody akcesora i ewentualnie wywołujemy ją z odpowiednim parametrem. Przy okazji możemy też filtrować przypisywane wartości.  Uwaga Przypominam, że w dokumentacji języka PHP metody i składowe klas występują często w notacji właściwej dla elementów statycznych, choć notacja ta ma w tym kontekście jedynie powiązać składową czy metodę z klasą. Stąd zapis Person::$name nie oznacza bynajmniej, że składowa $name klasy Person jest składową statyczną — przeciwnie, odwołujemy się do niej w kontekście nie klasy, a obiektu.

83

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Gdybyśmy więc utworzyli egzemplarz klasy Person i próbowali za jego pośrednictwem wykonać przypisanie wartości do składowej Person::$name, wywołana zostanie metoda __set(), ponieważ klasa Person nie definiuje składowej o takiej nazwie. Metoda otrzymuje w wywołaniu ciąg „name” oraz wartość, którą klient próbuje przypisać do tej składowej. Sposób obsługi takiego przypisania jest wtedy całkowicie zależny od implementacji metody __set(). W jej przykładowej implementacji na podstawie pierwszego argumentu wywołania konstruujemy nazwę akcesora. Jeśli okaże się, że uzyskana w ten sposób metoda setName() istnieje w klasie, zostanie ona wywołana z przypisywaną wartością. Wartość ta podlega transformacji i dopiero potem jest przypisywana do odpowiedniej składowej obiektu. $p = new Person(); $p->name = "bob"; // Składowa $_name otrzyma wartość 'BOB'.

Łatwo się domyślić, że metoda __unset() jest prostym uzupełnieniem metody __set(). Kiedy w kodzie klienckim klasy dochodzi do wywołania funkcji unset() na rzecz niezdefiniowanej składowej obiektu, wywoływana jest metoda __unset() klasy tego obiektu. Można wtedy przeprowadzić dowolne operacje realizujące albo negujące operację żądaną przez klienta. W poniższym przykładzie implementacja __unset() sprowadza się do wywołania metody pasującej do nazwy przekazanej składowej, z wartością pustą (NULL). function __unset($property) { $method = "set{$property}"; if (method_exists($this, $method)) { $this->$method(null); } }

Ostatnia z metod przechwytujących, __call(), jest chyba metodą najbardziej użyteczną. Jest ona bowiem wywoływana wtedy, kiedy użytkownik wywoła na rzecz obiektu bądź klasy metodę niezdefiniowaną w klasie. Wywołanie __call() przenosi nazwę wywoływanej przez użytkownika metody i tablicę argumentów pierwotnego wywołania. Wartość zwracana z metody __call() jest zaś przekazywana do wywołującego, który interpretuje ją jako wartość zwracaną wywoływanej metody. Metodę __call() można wykorzystać do delegowania wywołań. Delegowanie jest mechanizmem, w ramach którego jeden z obiektów przekazuje wywołanie metody do innego obiektu. Przypomina to dziedziczenie, w ramach którego obiekt klasy pochodnej przekazuje wywołanie metody do implementacji klasy bazowej. Przy dziedziczeniu relacja pomiędzy klasą pochodną i bazową jest stała; możliwość wybierania obiektu faktycznie realizującego wywołanie w czasie wykonania programu, wynikająca z delegowania, stanowi o jego większej elastyczności. Całość najlepiej zilustrować przykładem. Oto prosta klasa, której rolą jest formatowanie informacji przechowywanych w obiektach klasy Person: class PersonWriter { function writeName(Person $p) { print $p->getName()."\n"; } function writeAge(Person $p) { print $p->getAge()."\n"; } }

Moglibyśmy rzecz jasna specjalizować klasę (wyprowadzać pochodne tej klasy), wyświetlając dane obiektów Person na najróżniejsze sposoby. Poniżej mamy implementację klasy Person czyniącą użytek z klasy PersonWriter i metody __call(): class Person { private $writer; function __construct(PersonWriter $writer) { $this->writer = $writer; }

84

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

function __call($methodname, $args) { if (method_exists($this->writer, $methodname)) { return $this->writer->$methodname($this); } } function getName() { return "Bob"; } function getAge() { return 44; } }

Klasa Person wymaga tutaj przekazania w wywołaniu konstruktora obiektu klasy PersonWriter; obiekt ten jest zachowywany przez konstruktor w specjalnej składowej. W ramach metody __call() sprawdzamy przekazany w wywołaniu argument $methodname, kontrolując obecność metody o przekazanej nazwie w klasie przechowywanego obiektu. Jeśli metoda istnieje, delegujemy pierwotne wywołanie do obiektu klasy PersonWriter, przekazując w wywołaniu bieżący egzemplarz obiektu (wartość pseudozmiennej $this). Jeśli więc użytkownik klasy Person zainicjuje wywołanie: $person = new Person(new PersonWriter()); $person->writeName();

wywołana zostanie niechybnie metoda __call(). Z kolei metoda __call() sprawdzi, czy w klasie PersonWriter jest dostępna metoda writeName(), i ewentualnie ją wywoła. Oszczędza nam to konieczności ręcznego delegowania wywołania: function writeName() { $this->writer->writeName($this); }

Klasa Person w magiczny sposób zyskała dwie nowe metody. Ale choć automatyzacja delegowania pozwala na zaoszczędzenie wysiłku (w przypadku, kiedy jest wiele procesów), odbywa się to kosztem przejrzystości. Otoczenie otrzymuje bowiem w ten sposób dynamiczny interfejs opierający się inżynierii wstecznej (analizie aspektów klas realizowanej w czasie wykonywania programu), niekoniecznie oczywisty dla użytkujących go programistów. Wynika to z faktu, że logika obejmująca interakcje pomiędzy klasą delegującą a klasą docelową niekoniecznie jest oczywista — zamiast jawnego rozprowadzenia wywołania wynikającego z hierarchii dziedziczenia otrzymujemy niejawną delegację wynikającą np. z logiki zaszytej w metodzie __call(). Metody przechwytujące chybione wywołania mają więc swoje zastosowania, ale należy je wdrażać z zachowaniem zdrowego rozsądku i ostrożności, a klasy, które je wykorzystują, powinny ów fakt bardzo jasno dokumentować. Do zagadnienia delegowania i wstecznej inżynierii interfejsów wrócimy później. Metody przechwytujące __get() i __set() mogą być stosowane również do zarządzania właściwościami złożonymi. Może to być z pożytkiem dla programistów użytkowników klasy. Wyobraźmy sobie na przykład klasę Address przechowującą numer domu i nazwę ulicy. Ostatecznie dane z tego obiektu mają trafić do osobnych pól bazy danych, więc wewnętrzna separacja numeru i nazwy ulicy jest uzasadniona i wygodna. Ale jeśli użytkownicy klasy stosują te dane łącznie, można im ułatwić życie i zwracać je również łącznie za pośrednictwem składowej złożonej, np. Address::$streetaddress. class Address { private $number; private $street; function __construct($maybenumber, $maybestreet=null) { if (is_null($maybestreet)) { $this->streetaddress = $maybenumber; } else { $this->number = $maybenumber; $this->street = $maybestreet; } } function __set($property, $value) { if ($property === "streetaddress") {

85

PHP. OBIEKTY, WZORCE, NARZĘDZIA

if (preg_match("/^(\d+.*?)[\s,]+(.+)$/", $value, $matches)) { $this->number = $matches[1]; $this->street = $matches[2]; } else { throw new Exception("unable to parse street address: '{$value}'"); } } } function __get($property) { if ($property === "streetaddress") { return $this->number." ".$this->street; } } } $address = new Address("441b Bakers Street"); print "street address: {$address->streetaddress}\n"; $address = new Address(15, "Albert Mews"); print "street address: {$address->streetaddress}\n"; $address->streetaddress = "34, West 24th Avenue"; print "street address: {$address->streetaddress}\n";

Kiedy użytkownik obiektu klasy spróbuje odwołać się do (nieistniejącej) właściwości Address::$streetaddress, dojdzie do wywołania metody przechwytującej __get(). W jej wnętrzu sprawdzamy, czy wywołanie odnosiło się do właściwości o nazwie streetaddress. Z kolei w metodzie przechwytującej __set() trzeba jeszcze przed ustawieniem wartości odpowiednich składowych sprawdzić, czy przekazana wartość nadaje się do przetworzenia, i wyłuskać z niej wartości składowe. W tym przykładzie sprawdzian jest prosty: przekazany adres jest akceptowany, jeśli zaczyna się od numeru, który jest oddzielony od reszty spacjami albo przecinkami. Dzięki odwołaniom wstecznym w wyrażeniu regularnym dopasowanie wyrażenia wyłuskuje równocześnie zawarte w dopasowaniach dane do tablicy $matches, z której można wyciągnąć wartości do ustawienia składowych $number i $street. Jeśli wyrażenia regularnego nie uda się dopasować, zrzucamy wyjątek. Tak więc próba przypisania do Address::$streetaddress ciągu w rodzaju 441b Bakers Street spowoduje faktycznie ustawienie składowych $number i $street obiektu. Można to sprawdzić za pomocą funkcji print_r(): $address = new Address("441b Bakers Street"); print_r($address); Address Object ( [number:Address:private] => 441b [street:Address:private] => Bakers Street )

Metoda __get() jest oczywiście znacznie prostsza w implementacji: jeśli użytkownik odwołał się do składowej Address::$streetaddress, wywołana metoda __get() zwyczajnie sprawdza nazwę składowej z odwołania i — jeśli pasuje do ciągu streetaddress — zwraca ciąg sklejający wartości składowych $number i $street.

Definiowanie destruktorów Wiemy już, że w ramach konkretyzacji następuje automatyczne wywołanie metody konstrukcji — __construct(). W języku PHP5 obok konstruktora obiektu definiowany jest też jego destruktor — metoda o nazwie __destruct(). Jest ona wywoływana tuż przed operacją zbierania nieużytków (ang. garbage collection), czyli bezpośrednio przed usunięciem obiektu z pamięci. W ramach tej metody można podejmować wszelkie niezbędne operacje porządkujące stan obiektu.

86

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

Wyobraźmy sobie choćby klasę, która na żądanie utrwala swoje egzemplarze w bazie danych. Naturalnym miejscem wymuszenia takiego utrwalenia jest destruktor klasy — będziemy mieć wtedy pewność, że każdy obiekt przed usunięciem go z pamięci zostanie utrwalony w bazie danych: class Person { private $name; private $age; private $id; function __construct($name, $age) { $this->name = $name; $this->age = $age; } function setId($id) } $this->id = $id; } function __destruct() { if (!empty($this->id)) { // Utrwal dane obiektu… print "Dane obiektu utrwalone\n"; } } }

Metoda __destruct() zostanie wywołana tuż przed usunięciem obiektu klasy Person z pamięci. To zaś następuje albo w wyniku jawnego wywołania funkcji unset(), albo kiedy w bieżącym procesie nie ostanie się już żadna referencja obiektu. O wywołaniu metody destruktora możemy się przekonać, tworząc i następnie usuwając obiekt: $person = new Person("bob", 44); $person->setId(34); unset($person); // wydruk: // Dane obiektu utrwalone

Tego rodzaju sztuczki są zabawne, ale warto przestrzec programistów: metody __call(), __destruct() i im podobne są niekiedy nazywane „magicznymi”. Każdy, kto czytał powieści fantastyczne, wie, że magia nie zawsze jest rzeczą dobrą. Magia jest dziwna i nieoczekiwana. Magia nagina reguły. Magia oznacza ukryte koszty. Weźmy za przykład metodę __destruct(); dłubanie w tej metodzie może sprawić klientom klasy przykre niespodzianki. Choćby w klasie Person — która realizuje utrwalanie danych w bazie danych właśnie w ramach metody __destruct(). Wyobraźmy sobie teraz początkującego programistę, który zamierza użyć klasy Person. Nie doczytał o działaniu metody __destruct() i przymierza się do próbnego użycia obiektów tej klasy. W wywołaniu pierwszego, testowego konstruktora obiektu klasy Person używa zabawnej ksywki prezesa (której prezes jakoś nie lubi). Wiek ustawia na — to przecież tylko test — 150 lat. Kilkukrotnie uruchamia przykład, próbując różnych zabawnych kombinacji ksywki i wieku. Następnego ranka czeka go być może dywanik u kierownika i tłumaczenie się z obraźliwych wpisów w bazie danych. Morał? Nie ufać do końca magii.

Wykonywanie kopii obiektów W języku PHP4 kopiowanie obiektu sprowadzało się do przypisania zmiennej do innej zmiennej: class CopyMe {} $first = new CopyMe(); $second = $first; // W PHP4 — $second i $first są dwoma różnymi obiektami. // W PHP5 i nowszych — $second i $first są referencjami tego samego obiektu.

87

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Owa prostota kopiowania była przyczyną mnóstwa błędów wynikających z nieumyślnego kopiowania obiektów w wyniku przekazywania ich pomiędzy jednostkami programu — przy okazji przypisań, przekazywania obiektów w wywołaniach funkcji i przekazywania obiektów jako wartości zwracanych funkcji. Sprawę pogarszał jeszcze brak możliwości sprawdzenia, czy dwie zmienne odnoszą się do tego samego obiektu. Testy równoważności mogły bowiem dawać odpowiedź na pytanie o zgodność wartości wszystkich pól (w przypadku operatora ==), ewentualnie odpowiadać na pytanie o to, czy obie zmienne są obiektami (w przypadku operatora ===), ale nie pozwalały stwierdzić, czy zmienne odnoszą się do jednego obiektu. W PHP przypisanie obiektów odbywa się zawsze przez referencje, natomiast przekazywanie do funkcji odbywa się przez przekazanie wartości tej referencji. Gdybyśmy uruchomili powyższy przykład w PHP5, zmienne $first i $second odnosiłyby się do tego samego egzemplarza klasy CopyMe. W manipulowaniu obiektami taka semantyka przypisania jest ogólnie pożądana, okazjonalnie pojawia się jednak potrzeba wykonania kopii obiektu, a nie tylko utworzenia jego nowej referencji. PHP przewiduje na tę okoliczność słowo kluczowe clone. Jego działanie sprowadza się do wygenerowania kopii wartości obiektu: class CopyMe {} $first = new CopyMe(); $second = clone $first; // W PHP5 i nowszych — $second i $first to 2 różne (choć mające identyczną wartość) obiekty.

To dopiero początek zagadnienia kopiowania obiektów. Weźmy choćby klasę Person według implementacji prezentowanej w poprzednim podrozdziale. Wykonana w sposób domyślny kopia obiektu tej klasy będzie posiadać identyczny jak pierwowzór identyfikator (składową $id), którego rolą jest przecież rozróżnianie obiektów i na podstawie którego określany jest numer wiersza tabeli bazy danych. Zezwolenie na proste kopiowanie obiektów mogłoby doprowadzić do istnienia dwóch niezależnych obiektów odwołujących się do wspólnego źródła danych, czego raczej należałoby uniknąć, bo aktualizacja jednego z obiektów będzie pośrednio (przez zapis w bazie danych) unieważniać drugi — i na odwrót. Na szczęście możemy przejąć kontrolę nad procesem kopiowania obiektów — służy do tego specjalna metoda o nazwie __clone() (zauważ dwa znaki podkreślenia rozpoczynające nazwę metody i charakterystyczne dla nazw metod wbudowanych). Metoda __clone() jest wywoływana automatycznie, w reakcji na obecność słowa clone zastosowanego w odniesieniu do obiektu. Poprawna implementacja metody __clone() wymaga zrozumienia kontekstu, w którym owa metoda jest wykonywana. Otóż __clone() uruchamiana jest na rzecz kopii, a nie oryginału. Spróbujmy uzupełnić o metodę __clone() kolejną wersję klasy Person: class Person { private $name; private $age; private $id; function __construct($name, $age) { $this->name = $name; $this->age = $age; } function setId($id) } $this->id = $id; } function __clone() { $this->id = 0; } }

Kiedy w odniesieniu do obiektu klasy Person stosowane jest słowo kluczowe clone, tworzona jest początkowo powierzchowna (ang. shallow) kopia tego obiektu, po czym na jej rzecz wywoływana jest metoda __clone(). Oznacza to, że wszelkie operacje wykonane w ramach __clone() operują na już wykonanej kopii domyślnej. W naszej implementacji metoda __clone() zeruje składową $id kopii. 88

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

$person = new Person("bob", 44); $person->setId(343); $person2 = clone $person; // Efekt: // $person2: // name: bob // age: 44 // id: 0

Kopia powierzchowna oznacza tutaj proste kopiowanie poszczególnych składowych pierwowzoru do nowo tworzonego obiektu. Składowe obiektowe są jednak kopiowane przez referencję, co nie zawsze jest pożądane. Załóżmy, że obiekty klasy Person przechowywałyby wewnętrznie obiekty klasy Account (konto) reprezentujące stan konta danej osoby. W momencie wykonania kopii powstałyby dwa obiekty Person odwołujące się do wspólnych informacji o koncie! Z pewnością lepiej byłoby, gdyby oba dysponowały własnymi kopiami obiektu konta: class Account { public $balance; function __construct($balance) { $this->balance = $balance; } } class Person { private $name; private $age; private $id; public $account; function __construct($name, $age, Account $account) { $this->name = $name; $this->age = $age; $this->account = $account; } function setId($id) } $this->id = $id; } function __clone() { $this->id = 0; } } $person = new Person("bob", 44, new Account(200)); $person->setId(343); $person2 = clone $person; // Dajmy $person trochę gotówki: $person->account->balance += 10; // To samo konto widzi $person2: print $person2->account->balance;

Otrzymamy: 210

89

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Obiekt $person przechowuje referencję obiektu Account — dla uproszczenia przykładu jest ona dostępna publicznie, ale nie ma to znaczenia (w praktyce należałoby zablokować dostęp do niej i ewentualnie udostępnić odpowiednie akcesory). Wykonana później kopia obiektu otrzymuje do swojej dyspozycji własną referencję obiektu klasy Account, tego samego, do którego odnosi się referencja z pierwowzoru! Udowodniliśmy to, zwiększając saldo konta obiektu $person, a następnie odczytując saldo obiektu $person2 — okazało się, że operacje na jednym obiekcie wpływają na stan innego, co jest w tym przypadku niedopuszczalne. Eliminacja niepożądanego współużytkowania składowych obiektu po wykonaniu kopii wymaga jawnej ingerencji w proces kopiowania, a więc odpowiedniej implementacji metody __clone(): function __clone() { $this->id = 0; $this->account = clone $this->account; }

Reprezentacja obiektu w ciągach znaków W PHP5 pojawiło się też kolejne zapożyczenie z Javy — w postaci metody __toString(). W wersjach PHP poprzedzających 5.2 przy wypisywaniu wartości obiektu jako ciągu znaków otrzymywaliśmy: class StringThing {} $st = new StringThing(); print $st; Object id #1

Od wersji PHP 5.2 powyższy kod doprowadzi do następującego błędu: PHP Catchable fatal error: Object of class StringThing could not be converted to string in ...

Na sposób reprezentacji obiektu w ciągach znaków możemy wpływać, implementując samodzielnie metodę __toString(). Metoda ta powinna zwracać ciąg znaków. Jest ona wywoływana automatycznie, kiedy obiekt występuje w instrukcji print albo echo — w takim układzie obiekt jest zastępowany wartością zwracaną jego metody __toString(). Zobaczmy, jak może wyglądać implementacja metody reprezentacji obiektu w ciągu znaków w minimalistycznej wersji klasy Person: class Person { function getName() { return "Bob"; } function getAge() { return 44; } function __toString() { $desc = $this->getName(); $desc .= " (wiek: ".$this->getAge().")"; return $desc; } }

Teraz przy próbie wypisania wartości obiektu klasy Person w postaci ciągu znaków efekt będzie zupełnie inny: $person = new Person(); print $person; Bob (wiek: 44)

Własne implementacje metody __toString() sprawdzają się zwłaszcza przy rejestrowaniu i powiadamianiu o błędach, są też nieocenione w klasach, których podstawowym zadaniem jest przenoszenie informacji. Klasą taką jest choćby klasa wyjątku Exception, która w ramach metody __toString() generuje ciąg podsumowujący informacje o wyjątku. 90

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

Wywołania zwrotne, funkcje anonimowe i domknięcia Nie jest to co prawda mechanizm ściśle obiektowy, ale funkcje anonimowe z racji ich przydatności warto omówić już teraz — można ich sporo spotkać w aplikacjach obiektowych stosujących wywołania zwrotne. Na początek utwórzmy kilka klas pomocniczych: class Product { public $name; public $price; function __construct($name, $price) { $this->name = $name; $this->price = $price; } } class ProcessSale { private $callbacks; function registerCallback($callback) { if (! is_callable($callback)) { throw new Exception("niepoprawne wywołanie zwrotne"); } $this->callbacks[] = $callback; } function sale($product) { print "{$product->name}: przetwarzanie sprzedaży \n"; foreach ($this->callbacks as $callback) { call_user_func($callback, $product); } } }

Powyższy zestaw klas jest przystosowany do wykonywania rozmaitych wywołań zwrotnych. Składa się z dwóch klas. Klasa Product zwyczajnie przechowuje składowe $name i $price. Obie składowe są publiczne, a to ze względu na zwięzłość kodu. W prawdziwym programie zostałyby najpewniej objęte ochroną i udostępnione metodami akcesorami. Klasa ProcessSale składa się z dwóch metod: registerCallback(), która przyjmuje nieokreślonego typu wartość skalarną, bada ją i ewentualnie dodaje do tablicy wywołań zwrotnych. Badanie polega na wywołaniu wbudowanej funkcji is_callable(), która mówi, czy argument wywołania nadaje się do realizacji wywołania w kontekście funkcji takich jak call_user_func() czy array_walk(). Metoda sale() przyjmuje w wywołaniu obiekt klasy Product, wypisuje informacje o produkcie, a następnie przegląda tablicę w składowej $callback. Każdy element tablicy przekazuje do funkcji call_user_func(), która uruchamia funkcję reprezentowaną w tablicy, przekazując do niej obiekt produktu poprzez referencję. Wszystkie poniższe przykłady będziemy uruchamiać w takim „frameworku”. Czy wywołania zwrotne są przydatne? Tak, bo pozwalają na łatwe włączanie do komponentów funkcjonalności niekoniecznie ściśle związanych z głównymi zadaniami realizowanymi przez komponent. Dzięki udostępnieniu w komponencie mechanizmu wywołań zwrotnych dajemy klientom możliwość łatwego rozbudowania komponentu. Wyobraźmy sobie, że przyszły użytkownik klasy ProcessSale zechce utworzyć dziennik sprzedaży. Gdyby miał dostęp do kodu klasy, mógłby dodać logowanie bezpośrednio do metody sale(). Ale nie zawsze jest to pożądane; jeśli ów użytkownik nie jest osobą odpowiedzialną za pakiet zawierający klasę ProcessSale, wprowadzone przez niego zmiany zostaną najpewniej nadpisane w kolejnych wersjach pakietu. A nawet jeśli użytkownik ma pełną kontrolę nad pakietem, to dodawanie rozmaitych zadań bezpośrednio do metody sale() szybko sprawi, że stanie się ona nieczytelna i nieelastyczna. Tymi zagadnieniami zajmiemy się zresztą w następnym podrozdziale.

91

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Na szczęście klasa ProcessSale została wyposażona w mechanizm wywołań zwrotnych. Oto wywołanie zwrotne symulujące dziennik sprzedaży: $logger = create_function('$product', 'print " zapisano ({$product->name})\n";' ); $processor = new ProcessSale(); $processor->registerCallback($logger); $processor->sale(new Product("buty", 6)); print "\n"; $processor->sale(new Product("kawa", 6));

Do zbudowania wywołania zwrotnego użyliśmy funkcji create_function(). Jak widać, funkcja ta przyjmuje dwa argumenty (ciągi znaków). Pierwszy z nich określa listę parametrów montowanej funkcji, a drugi to ciało funkcji. Wynikiem wykonania create_function() jest tak zwana funkcja anonimowa, a więc nieposiadająca nazwy tak jak klasyczna funkcja PHP. Może jednak być reprezentowana w zmiennej i przekazywana do funkcji w roli argumentu wywołania. I właśnie tak jej używamy: umieszczamy ją w składowej $logger i przekazujemy do metody ProcessSale::registerCallback(). Na koniec tworzymy kilka obiektów produktów i przekazujemy je do metody sale(). Od razu widać, co się dzieje: dochodzi do wykonania właściwej części metody sale() (u nas jest to wypisanie prostego komunikatu o produkcie) oraz do wywołania wszelkich zarejestrowanych wywołań zwrotnych. Nasz kod działa tak: buty: przetwarzanie sprzedaży zapisano (buty) kawa: przetwarzanie sprzedaży zapisano (kawa)

Spójrzmy raz jeszcze na przykład z create_function(). Nie jest zbyt ładny, prawda? Umieszczanie kodu do wykonania w ciągu znaków zawsze jest kłopotliwe. Zmienne wykorzystywane w kodzie trzeba specjalnie sygnalizować, odpowiednio sygnalizować trzeba też znaki cudzysłowu… jeśli kod funkcji anonimowej się rozrośnie, jego czytelność będzie żadna. Czy nie istnieje lepszy sposób tworzenia funkcji anonimowych? Cóż, w PHP 5.3 mamy taki sposób. Sprowadza się on do deklarowania funkcji połączonego z przypisaniem do zmiennej. Za pomocą nowej składni nasz przykład moglibyśmy zrealizować tak: $logger2 = function($product) { print " logging ({$product->name})\n"; }; $processor = new ProcessSale(); $processor->registerCallback( $logger2 ); $processor->sale(new Product("buty", 6)); print "\n"; $processor->sale(new Product("kawa", 6));

Różnica tkwi w tworzeniu funkcji anonimowej. Jak widać, jest to znacznie przyjemniejsze. Wystarczy użyć słowa kluczowego function i nie określać nazwy funkcji. Zauważmy, że skoro deklaracja jest osadzona w przypisaniu, to na końcu bloku kodu funkcji konieczny jest średnik. Wynik wykonania powyższego kodu jest identyczny jak poprzednio. Wywołania zwrotne nie muszą być koniecznie anonimowe. W roli wywołania zwrotnego można przecież użyć funkcji nazwanej albo nawet referencji do obiektu i nazwy metody, jak tutaj: class Mailer { function doMail( $product ) { print " wysyłka dla ({$product->name})\n"; }

92

ROZDZIAŁ 4.  ZAAWANSOWANA OBSŁUGA OBIEKTÓW

} $processor = new ProcessSale(); $processor->registerCallback(array(new Mailer(), "doMail")); $processor->sale(new Product("buty", 6)); print "\n"; $processor->sale(new Product("kawa", 6));

Utworzyliśmy klasę Mailer. Jej jedyna metoda — doMail() — przyjmuje obiekt $product i wypisuje komunikat o produkcie. W wywołaniu registerCallback() przekazujemy tym razem tablicę. Pierwszym elementem tablicy jest obiekt Mailer, a drugim jest ciąg pasujący do nazwy metody, którą chcemy wywołać na rzecz obiektu w roli wywołania zwrotnego. Pamiętajmy, że metoda registerCallback() bada argument pod kątem zdatności do wywołania. Użyta tam funkcja is_callable() jest na tyle sprytna, że potrafi zbadać podobne tablice. Poprawne wywołanie zwrotne dane jako tablica powinno zawierać obiekt w pierwszym elemencie i nazwę metody w drugim elemencie tablicy. Dokładnie jak u nas, więc możemy na wyjściu programu oczekiwać takiego efektu: buty: przetwarzanie sprzedaży wysyłka (buty) kawa: przetwarzanie sprzedaży wysyłka (kawa)

W funkcji anonimowej można zwracać wartości, jak tutaj: class Totalizer { static function warnAmount() { return function($product) { if ($product->price > 5) { print " drogo: {$product->price}\n"; } }; } } $processor = new ProcessSale(); $processor->registerCallback(Totalizer::warnAmount()); ...

Poza wygodą stosowania metody warnAmount() w roli wytwórni funkcji anonimowych nie ma tu nic ciekawego. Ale taka struktura pozwala na znacznie więcej niż tylko generowanie pojedynczej funkcji anonimowej. Możemy mianowicie skorzystać z dopełnień (ang. closures). Otóż owe dopełnienia, jako rozbudowane funkcje anonimowe, mogą odwoływać się do zmiennych deklarowanych w zasięgu zawierającym deklarację funkcji anonimowej. Nie jest to zupełnie oczywisty mechanizm. To jakby funkcja anonimowa zapamiętywała kontekst, w którym została utworzona. Wyobraźmy sobie, że w ramach wywołania Totalizer::warnAmount() chcemy wykonać dwie rzeczy. Po pierwsze, chcemy przyjmować dowolną kwotę progową. Po drugie, chcemy pamiętać kwotę sprzedaży. Kiedy suma sprzedaży przekroczy wartość progową, funkcja powinna wykonać jakąś akcję (u nas będzie to jedynie wypisanie komunikatu). Aby zmusić funkcję anonimową do śledzenia zmiennych z zewnętrznego zasięgu, możemy użyć klauzuli use: class Totalizer { static function warnAmount($amt) { $count=0; return function($product) use ($amt, &$count) { $count += $product->price; print " suma: $count\n"; if ($count > $amt) { print " drogo: {$count}\n";

93

PHP. OBIEKTY, WZORCE, NARZĘDZIA

} }; } } $processor = new ProcessSale(); $processor->registerCallback( Totalizer::warnAmount( 8) ); $processor->sale(new Product("buty", 6)); print "\n"; $processor->sale(new Product("kawa", 6));

Funkcja anonimowa zwracana przez Totalizer::warnAmount() wymienia w klauzuli use dwie zmienne. Pierwsza z nich to $amt. Jest to argument wywołania metody warnAmount(). Druga zmienna domknięcia to $count, zmienna zadeklarowana w ciele metody warnAmount() i zainicjalizowana zerem. Zauważmy, że przed nazwą zmiennej $count w klauzuli use znajduje się znak &. Oznacza to, że w funkcji anonimowej zmienna będzie dostępna przez referencję, a nie poprzez kopię wartości. W ciele funkcji anonimowej zwiększamy wartość $count o cenę bieżącego produktu i sprawdzamy sumę pod kątem wartości progowej $amt. Jeśli wartość została przekroczona, wypisujemy powiadomienie. Oto powyższy kod w akcji: buty: przetwarzanie sprzedaży suma: 6 kawa: przetwarzanie sprzedaży suma: 12 drogo: 12

Jak widać, wywołanie zwrotne potrafi teraz akumulować wartość $count pomiędzy wywołaniami. Zmienne $count i $amt są związane z kontekstem funkcji wywołania zwrotnego, bo były obecne w jej kontekście w miejscu deklaracji i zostały wymienione w klauzuli use.

Podsumowanie W rozdziale wzięliśmy się za bary z bardziej zaawansowanymi elementami obsługi obiektów w języku PHP. Niektóre z nich będą intensywnie wykorzystywane w pozostałych rozdziałach książki. Szczególnie często wracać będziemy do klas abstrakcyjnych i metod statycznych. Następny rozdział poświęcony będzie już nie tyle wbudowanym elementom obsługi obiektów, co klasom i funkcjom pełniącym w manipulowaniu obiektami rolę pomocniczą.

94

ROZDZIAŁ 5 

Narzędzia obiektowe

Poprzednie rozdziały zaznajamiały Czytelnika z programowaniem obiektowym, przybliżając mu podstawowe konstrukcje obiektowe, takie jak klasy i metody. Udogodnienia obiektowe nie kończą się na tych konstrukcjach i mechanizmach — sam język udostępnia też mechanizmy pomocnicze, ułatwiające pracę z obiektami. Niniejszy rozdział poświęcony będzie prezentacji niektórych z tych narzędzi oraz technikom wykorzystywanym do organizowania, testowania i korzystania z klas i obiektów. W rozdziale omawiam:  Pakiety — czyli organizowanie kodu w kategorie logiczne.  Przestrzenie nazw — od wersji 5.3 można osadzać elementy kodu w odrębnych przestrzeniach nazw.  Włączanie kodu — z naciskiem na ustanowienie centralnie dostępnej lokalizacji kodu bibliotecznego.  Funkcje pomocnicze względem klas i obiektów — służące do testowania obiektów, klas, składowych i metod.  Interfejs Reflection API — bezprecedensowy zestaw wbudowanych klas pozwalających na retrospekcję: realizację dynamicznych odwołań do informacji o klasach.

PHP a pakiety Pakiet to zbiór powiązanych ze sobą klas. Pakiety służą do wyodrębniania i rozdzielania poszczególnych części systemu. W niektórych językach programowania obsługa pakietów (modułów) jest sformalizowana — jak w Javie, gdzie pakiety dysponują własnymi przestrzeniami nazw. W PHP koncepcja pakietu jest cokolwiek obca, ale od wersji 5.3 wprowadzono przestrzenie nazw, o których napiszę więcej w następnym podrozdziale. Skoro i tak przez jakiś czas będziemy musieli pracować również ze starym kodem, nie może tu zabraknąć klasycznego sposobu organizowania klasy w struktury pakietopodobne.

Pakiety i przestrzenie nazw w PHP PHP nie posiada mechanizmów obsługi pakietów jako takich, ale programiści od zawsze radzili sobie z kategoryzacją kodu poprzez odrębne konwencje nazewnicze i separację kodu w systemie plików. Niebawem zajmiemy się zalecanymi technikami organizowania kodu na bazie katalogów i plików, na początek jednak weźmiemy na warsztat konwencje nazewnicze oraz nowy mechanizm przestrzeni nazw. Aż do powstania wersji 5.3 programiści byli zmuszeni do dobierania nazw plików w kontekście globalnym. Innymi słowy, jeśli klasa nosiła miano ShoppingBasket, była pod tą nazwą dostępna w całym systemie. Prowadziło to do dwóch problemów. Przede wszystkim wprowadzało niemałe ryzyko kolizji nazw. Niby jest

PHP. OBIEKTY, WZORCE, NARZĘDZIA

to mało prawdopodobne, bo wystarczy zapamiętać wszystkie nazwy klas, prawda? Kłopot w tym, że każdy z programistów używa mnóstwa kodu bibliotecznego, zewnętrznego. To oczywiście pożądane, ale w kontekście kolizji nazw bardzo ryzykowne. Co jeśli nasz projekt robi tak: // plik my.php require_once "useful/Outputter1.php" class Outputter { // wypisywanie danych }

A plik włączany do projektu robi tak: // plik useful/Outputter1.php class Outputter { // ... }

Chyba już wiemy, prawda? Oto co się stanie: PHP Fatal error: Cannot redeclare class Outputter in ...Outputter1.php on line 2

Oczywiście, istniało konwencjonalne obejście tego problemu. Należało poprzedzać nazwy klas nazwami pakietów, co gwarantowało (w pewnym stopniu) unikatowość nazw klas: // plik my.php require_once "useful/Outputter2.php"; class my_Outputter { // wypisywanie danych } // plik useful/Outputter2.php class useful_Outputter { // ... }

Sęk w tym, że w miarę rozbudowywania projektów nazwy klas wydłużały się niemiłosiernie. Nie jest to może bardzo problematyczne, ale zmniejsza czytelność kodu i utrudnia zapamiętanie nazw klas przez programistów, a także przyczynia się do utraty wielu roboczogodzin potrzebnych na poprawianie pomyłek w coraz to dłuższych nazwach. Jeszcze przez lata będziemy skazani na tę konwencję, bo każdy z nas korzysta z jakichś starszych bibliotek. Z tego względu do zagadnienia klasycznego sposobu zarządzania pakietami wrócimy jeszcze w dalszej części rozdziału.

Ratunek — przestrzenie nazw W PHP 5.3 pojawiła się obsługa przestrzeni nazw. Zasadniczo przestrzeń nazw to pojemnik, w którym można umieszczać klasy, funkcje i zmienne. W obrębie przestrzeni nazw można się do tych elementów odwoływać bez kwalifikowania odwołań. Z zewnątrz należy albo zaimportować przestrzeń nazw, albo odwoływać się do jej elementów za pomocą nazw kwalifikowanych. Skomplikowane? Przykład powinien rozjaśnić problem. Oto przykład kolidujących klas przepisany na przestrzenie nazw: namespace my; require_once "useful/Outputter3.php"; class Outputter { // wypisywanie danych }

96

ROZDZIAŁ 5.  NARZĘDZIA OBIEKTOWE

// plik useful/Outputter3.php namespace useful; class Outputter { // }

Zauważmy słowo kluczowe namespace. Łatwo się domyślić, że ustanawia ono nową przestrzeń nazw. Użytkownicy tego mechanizmu powinni pamiętać, że deklaracja przestrzeni nazw musi być pierwszą instrukcją pliku. Powyżej utworzyliśmy dwie przestrzenie nazw: my oraz useful. Typowo jednak przestrzenie nazw tworzą głębszą hierarchię. Na samym szczycie definiuje się zazwyczaj przestrzeń z nazwą projektu albo organizacji. Następnie kwalifikuje się tę nazwę nazwą pakietu — PHP pozwala na deklarowanie zagnieżdżonych przestrzeni nazw. Poziomy w hierarchii przestrzeni nazw oddziela się znakami lewego ukośnika. namespace com\getinstance\util; class Debug { static function helloWorld() { print "hello from Debug\n"; } }

Gdybyśmy udostępniali repozytorium z kodem, w naturalny sposób moglibyśmy użyć członów nazwy domeny jako początkowych członów przestrzeni nazw. Sztuczkę tę stosują programiści Javy w nazwach pakietów: odwracają nazwy domen organizacji czy projektów, od członu najbardziej ogólnego do członu najbardziej szczegółowego. A po zidentyfikowaniu repozytorium można zacząć definiować pojedyncze pakiety — w tym przypadku pakiet util. Jak wywołać metodę klasy z takiego pakietu? Zależy, skąd ta metoda ma być wywołana. Jeśli wywołanie odbywa się w obrębie przestrzeni nazw, w której metoda jest zadeklarowana, można ją wywołać wprost: Debug::helloWorld();

Takie wywołanie nazwiemy niekwalifikowanym. W przestrzeni nazw com\getinstance\util nazwy klas i metod są dostępne bez żadnych członów poprzedzających. Ale spoza przestrzeni nazw należy używać nazwy klasy (metody) kwalifikowanej nazwą przestrzeni nazw: com\getinstance\util\Debug::helloWorld();

Jaki będzie więc efekt wykonania poniższego kodu? namespace main; com\getinstance\util\Debug::helloWorld();

Pytanie było podchwytliwe. Oczywiście pojawi się błąd: PHP Fatal error: Class 'main\com\getinstance\util\Debug' not found in …

A to dlatego, że użyliśmy względnej przestrzeni nazw. PHP przy rozwiązywaniu nazw szuka przestrzeni nazw com\getinstance\util w obrębie przestrzeni nazw main i rzecz jasna — nie znajduje jej. Tak samo, jak można stosować bezwzględne ścieżki plików i URL-e, tak samo można konstruować bezwzględne nazwy przestrzeni nazw. Błąd poprzedniego programu można więc naprawić tak: namespace main; \com\getinstance\util\Debug::helloWorld();

Znak lewego ukośnika na początku identyfikatora przestrzeni nazw mówi, że poszukiwanie przestrzeni nazw należy zacząć od samego szczytu hierarchii, a nie od bieżącej przestrzeni nazw. Ale czy przestrzenie nazw nie miały przypadkiem oszczędzić programistom długich nazw? Deklaracja klasy Debug jest co prawda krótsza, ale jej wywołania wcale się nie skróciły — są równie rozwlekłe jak w klasycznym modelu „pakietów” bez przestrzeni nazw. Do wyeliminowania tej rozwlekłości przewidziano osobne słowo kluczowe języka PHP: use. Pozwala ono na aliasowanie nazw innych przestrzeni nazw w bieżącej przestrzeni nazw. Oto przykład: 97

PHP. OBIEKTY, WZORCE, NARZĘDZIA

namespace main; use com\getinstance\util; util\Debug::helloWorld();

Przestrzeń nazw com\getinstance\util została tu skrócona do krótkiej nazwy util. Zauważmy, że nie rozpoczęto jej od znaku ukośnika: argument dla słowa kluczowego use jest rozpatrywany w globalnej, a nie w bieżącej przestrzeni nazw. Dalej, jeśli w ogóle chcemy się pozbyć kwalifikacji nazw, możemy zaimportować klasę Debug do bieżącej przestrzeni nazw: namespace main; use com\getinstance\util\Debug; Debug::helloWorld();

A co się stanie, jeśli w bieżącej przestrzeni nazw (main) znajduje się już deklaracja klasy Debug? Łatwo zgadnąć. Oto stosowny kod i efekt jego wykonania: namespace main; use com\getinstance\util\Debug; class Debug { static function helloWorld() { print "hello from main\Debug"; } } Debug::helloWorld(); PHP Fatal error: Cannot declare class main\Debug because the name is already in use in …

Zatoczyliśmy więc koło, wracając ponownie do kolizji nazw klas, nieprawdaż? Na szczęście nasz problem ma rozwiązanie w postaci jawnych aliasów dla używanych nazw: namespace main; use com\getinstance\util\Debug as uDebug; class Debug { static function helloWorld() { print "hello from main\Debug"; } } uDebug::helloWorld();

Użycie słowa as w klauzuli use pozwala na zmianę aliasu nazwy Debug na uDebug. Kiedy programista pisze kod w jakiejś przestrzeni nazw i zamierza odwołać się do klasy z globalnej (nienazwanej) przestrzeni nazw, może po prostu poprzedzić nazwę klasy pojedynczym znakiem ukośnika. Oto deklaracja metody w globalnej przestrzeni nazw: // plik global.php: bez przestrzeni nazw class Lister { public static function helloWorld() { print "ahoj z modułu głównego\n"; } }

98

ROZDZIAŁ 5.  NARZĘDZIA OBIEKTOWE

A oto kod zamknięty w przestrzeni nazw, odwołujący się do owej metody: namespace com\getinstance\util; require_once 'global.php'; class Lister { public static function helloWorld() { print "ahoj z modułu ".__NAMESPACE__."\n"; } } Lister::helloWorld(); // odwołanie lokalne \Lister::helloWorld(); // odwołanie globalne

Kod z przestrzeni nazw deklaruje własną wersję klasy Lister. Odwołanie z nazwą niekwalifikowaną to odwołanie do wersji lokalnej; odwołanie z nazwą kwalifikowaną pojedynczym znakiem ukośnika to odwołanie do klasy z globalnej przestrzeni nazw. Oto efekt wykonania poprzedniego fragmentu kodu. ahoj z modułu com\getinstance\util ahoj z modułu głównego

Warto go pokazać, bo przy okazji ilustruje działanie stałej __NAMESPACE__. Otóż przechowuje ona nazwę bieżącej przestrzeni nazw i bardzo przydaje się w diagnostyce błędów. W pojedynczym pliku można deklarować więcej niż jedną przestrzeń nazw — składnia pozostaje bez zmian. Można też stosować składnię alternatywną, z użyciem nawiasów klamrowych ujmujących ciało deklaracji przestrzeni nazw. namespace com\getinstance\util { class Debug { static function helloWorld() { print "ahoj, tu Debug\n"; } } } namespace main { \com\getinstance\util\Debug::helloWorld(); }

Jeśli zachodzi konieczność użycia wielu przestrzeni nazw w pojedynczym pliku, składnia z nawiasami klamrowymi jest wręcz zalecana. Ogólnie jednak zaleca się, aby przestrzenie nazw były definiowane w osobnych plikach. Unikatową cechą składni z nawiasami klamrowymi jest możliwość przełączenia się do globalnej przestrzeni nazw wewnątrz pliku. Wcześniej do pozyskania kodu z globalnej przestrzeni nazw użyliśmy dyrektywy require_once. Mogliśmy jednak użyć alternatywnej składni przestrzeni nazw i zamknąć wszystko w jednym pliku. namespace { class Lister { //... } } namespace com\getinstance\util { class Lister { //... } Lister::helloWorld(); // odwołanie lokalne \Lister::helloWorld(); // odwołanie globalne }

99

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Do globalnej przestrzeni nazw weszliśmy, otwierając blok przestrzeni nazw bez określenia nazwy.  Uwaga Nie można mieszać składni wierszowej ze składnią klamrową w jednym pliku — w obrębie pliku trzeba wybrać jedną składnię i konsekwentnie się jej trzymać.

Symulowanie systemu pakietów na bazie systemu plików Niezależnie od wykorzystywanej wersji PHP możemy na własną rękę organizować klasy w pakiety, wykorzystując struktury charakterystyczne dla systemu plików. Możemy na przykład wydzielić dla dwóch grup klasy osobne katalogi (np. util i business) i włączać przechowywane w nich pliki implementujące klasy za pośrednictwem funkcji require_once(), jak poniżej: require_once("business/Customer.php"); require_once("util/WebTools.php");

Z podobnym efektem można zastosować funkcję include_once(). Różnica pomiędzy instrukcjami include() i require() tkwi w obsłudze błędów. Otóż plik wywołany za pomocą require() w przypadku błędu zatrzyma przetwarzanie całego programu. Taki sam błąd w pliku włączanym instrukcją include() zaledwie sprowokuje ostrzeżenie i przerwie wykonanie kodu z wciąganego pliku, ale nie przerwie wykonania całego programu. Dzięki temu require() i require_once() stanowią bezpieczniejsze sposoby włączania plików bibliotecznych, a include() i include_once() są bardziej przydatne przy szablonach.  Uwaga require() i require_once() to w istocie instrukcje, a nie funkcje. Oznacza to, że można przy nich zrezygnować z nawiasów. Osobiście i tak stosuję nawiasy, ale zdarzają się pedanci zanudzający wyjaśnieniami różnicy pomiędzy funkcją a instrukcją.

Rysunek 5.1 prezentuje tak uzyskany podział kodu w przeglądarce plików Nautilus.

Rysunek 5.1. Organizacja pakietów PHP w konwencji systemu plików  Uwaga Argumentem wywołania funkcji require_once() jest ścieżka do pliku; funkcja wstawia plik do bieżącego skryptu po jego uprzednim przetworzeniu. Nastąpi to jednak jedynie wtedy, kiedy plik określony przez argument wywołania nie został jeszcze włączony do procesu w innym miejscu. Tego rodzaju zabezpieczenie przed wielokrotnym włączaniem kodu jest użyteczne zwłaszcza w kodzie bibliotecznym, zapobiega bowiem przypadkowemu ponownemu definiowaniu klas i funkcji, do czego mogłoby dojść, gdyby plik kodu bibliotecznego był włączany do skryptu w kilku różnych miejscach za pośrednictwem funkcji require() czy include(). Programista ma swobodę wyboru pomiędzy funkcjami require() i require_once() a podobnymi (ale nie identycznymi) w działaniu include() i include_once(), zalecałbym jednak korzystanie z tych pierwszych, a to dlatego, że błąd w pliku odczytywanym za pomocą funkcji require() przerywa wykonywanie skryptu. Taki sam błąd występujący w pliku włączanym do skryptu wywołaniem include() powoduje zaś jedynie wygenerowanie ostrzeżenia w skrypcie wywołującym, ale nie przerywa jego działania. W tym przypadku za bezpieczniejsze należy zaś uznać środki drastyczniejsze. Z zastosowaniem require_once() w porównaniu z require() związany jest pewien narzut. Otóż tam, gdzie ważne są nawet milisekundy działania programu, warto rozważyć użycie require().

100

ROZDZIAŁ 5.  NARZĘDZIA OBIEKTOWE

Jeśli chodzi o PHP, struktura ta nie ma w sobie niczego szczególnego: różne skrypty biblioteczne umieszczamy po prostu w różnych katalogach. Wymusza to porządek w organizacji kodu i nie wyklucza używania przestrzeni nazw bądź klasycznych konwencji nazewniczych.

Nazwy à la PEAR W praktyce nie zawsze można skorzystać z dobrodziejstw przestrzeni nazw; w zastanym oprogramowaniu modernizacja kodu może się okazać przedsięwzięciem nieopłacalnym — mimo że oprogramowanie wciąż jest używane i rozwijane. A nawet jeśli dany projekt w całości oparty jest na najnowszej wersji PHP, nie obędzie się pewnie bez wykorzystania jakiegoś starszego kodu. Jeśli możemy pozwolić sobie na przepisanie go na nazwy klas — świetnie. W większości przypadków będzie to jednak nieosiągalny luksus. Jak więc poradzić sobie z ryzykiem kolizji nazw, jeśli nie można zdać się w całości na przestrzenie nazw? Jeden sposób już zaznaczyliśmy — mowa o wykorzystaniu konwencji nazewniczej typowej dla pakietów PEAR.  Uwaga PEAR to skrót od PHP Extenstion and Application Repository (repozytorium rozszerzeń i aplikacji PHP). To oficjalne archiwum pakietów i narzędzi rozszerzających możliwości i zakres zastosowań języka PHP. Podstawowe pakiety z tego repozytorium wchodzą w skład dystrybucji PHP, inne mogą być do niej dodawane za pośrednictwem prostego narzędzia wywoływanego z wiersza polecenia. Pod adresem http://pear.php.net dostępna jest przeglądarka pakietów repozytorium. Do aspektów korzystania z PEAR wrócimy w rozdziale 15.

W PEAR stosuje się strukturę pakietów bazującą właśnie na systemie plików. Nazwa każdej z klas jest więc odzwierciedleniem ścieżki dostępu — nazwy poszczególnych podkatalogów są w nazwie klasy rozdzielane znakiem podkreślenia. Repozytorium PEAR obejmuje na przykład pakiet o nazwie XML, zawierający pakiet RPC. Pakiet RPC zawiera z kolei plik o nazwie Server.php. Klasa definiowana wewnątrz tego pliku nie nosi bynajmniej prostej nazwy Server. Prędzej czy później stosowanie tak oczywistej nazwy doprowadziłoby bowiem do kolizji z kodem użytkującym pakiet RPC. Rzeczona klasa nosi więc nazwę XML_RPC_Server. Nie czyni to nazwy klasy atrakcyjniejszą, zwiększa jednak łatwość czytania kodu, bo nazwa klasy zawsze opisuje swój własny kontekst.

Ścieżki przeszukiwania Przy organizowaniu komponentów warto pamiętać o dwóch perspektywach. Pierwszą mamy omówioną: chodzi o położenie plików i katalogów w systemie plików. Trzeba też jednak uwzględnić sposób realizacji odwołań pomiędzy komponentami. Jak dotąd zignorowałem niemal całkowicie tematykę ścieżek dostępu występujących w wywołaniach funkcji włączających kod do skryptu. Tymczasem, włączając plik kodu, możemy określać ów plik za pośrednictwem ścieżki względnej, odnoszącej się do bieżącego katalogu roboczego, albo ścieżki bezwzględnej, zakorzenionej w katalogu głównym systemu plików. W prezentowanych dotychczas przykładach stosowaliśmy wyłącznie ścieżki względne: require_once("business/User.php");

Ale to oznacza konieczność obecności w bieżącym katalogu roboczym podkatalogu business, a prędzej czy później taki wymóg stanie się niepraktyczny. Jeśli już chce się stosować w wywołaniach włączających kod biblioteczny ścieżki względne, to lepiej, aby miały one postać: require_once("../../projectlib/business/User.php");

Można by też stosować ścieżki bezwzględne: require_once("/home/john/projectlib/business/User.php");

Żadne rozwiązanie nie jest jednak idealne, bo określając ścieżkę zbyt szczegółowo, zamrażamy niejako położenie pliku bibliotecznego.

101

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Przy stosowaniu ścieżek bezwzględnych wiążemy połączenie bibliotek z konkretnym systemem plików. Instalacja projektu na nowym serwerze wymaga wtedy aktualizacji wszystkich wywołań funkcji włączających pliki biblioteczne. Stosując ścieżki względne, ustalamy położenie plików bibliotecznych względem bieżącego katalogu roboczego, przez co utrudniamy przenoszenie plików bibliotecznych. W ten sposób utrudnia się przeniesienie biblioteki w systemie plików bez koniecznej zmiany instrukcji require(), co sprawia, że w projektach innych niż macierzysty biblioteki nie da się łatwo używać. W obu zaś przypadkach tracimy perspektywę pakietu na rzecz perspektywy systemu plików — nie bardzo wiadomo bowiem, czy mamy pakiet business, czy może projectlib/business. Aby ułatwić sobie odwoływanie się do plików bibliotecznych, musimy więc oddzielić kod wywołujący od konkretnego położenia plików bibliotecznych, tak aby ścieżkę: business/User.php

można było wykorzystać w dowolnym miejscu systemu i aby w każdym z nich odnosiła się ona do tego samego pakietu. Można to osiągnąć, umieszczając pakiet w jednym z katalogów, do których odwołuje się parametr include_path. Parametr ten jest zwykle ustawiany w pliku php.ini — centralnym pliku konfiguracji PHP. Definiuje on listę ścieżek dostępu wymienianych po dwukropkach (w systemach uniksowych) albo średnikach (w systemach z rodziny Windows). include_path = ".:/usr/local/lib/php-libraries"

Użytkownicy serwera Apache mogą też ustawić dyrektywę include_path w pliku konfiguracyjnym serwera (zazwyczaj jest to plik httpd.conf) albo w plikach konfiguracji poszczególnych katalogów (zazwyczaj pod nazwą .htaccess). Odbywa się to za pomocą składni: php_wartość include_path wartość .:/usr/local/lib/php-libraries

 Uwaga Pliki .htaccess są przydatne zwłaszcza w przestrzeni WWW udostępnianej przez firmy hostingowe, w których do środowiska konfiguracji samych serwerów mamy bardzo ograniczony dostęp.

W wywołaniach funkcji systemowych, jak fopen() czy require(), z względnymi ścieżkami dostępu, których nie uda się dopasować w kontekście bieżącego katalogu roboczego, inicjowane jest przeszukiwanie ścieżek wymienionych w ramach parametru include_path w kolejności zgodnej z kolejnością ich definiowania w ramach parametru (w przypadku funkcji fopen() włączenie automatycznego przeszukiwania ścieżek wymaga przekazania za pomocą argumentów odpowiedniego znacznika). Wyszukiwanie kończy się po odnalezieniu żądanego pliku w którymś z kolejnych katalogów wymienionych w ramach parametru include_path. Gdy umieścimy katalog pakietów w katalogach ścieżek przeszukiwania, możemy w wywołaniach włączających kod zrezygnować z samodzielnego określania ścieżek dostępu. W takim układzie listę ścieżek przeszukiwania należałoby uzupełnić o wyróżniony katalog przeznaczony wyłącznie na pliki biblioteczne. Wymaga to edycji pliku konfiguracji php.ini (oczywiście wprowadzone w nim zmiany zostaną uwzględnione przez moduł PHP serwera dopiero po przeładowaniu tego serwera). W przypadku nieposiadania uprawnień niezbędnych do modyfikowania pliku php.ini można uciec się do modyfikacji parametru include_path z poziomu samego skryptu. Służy do tego funkcja set_include_path(). Funkcja ta przyjmuje w wywołaniu ciąg reprezentujący ścieżkę przeszukiwania i ustawia ową ścieżkę, ale wyłącznie dla bieżącego procesu. Zazwyczaj parametr include_path zdefiniowany w pliku php.ini zawiera już przydatne ścieżki przeszukiwania, więc zamiast go zamazywać, można go uprzednio odczytać i jedynie dopisać swoje ścieżki do bieżącej wartości parametru. Odczyt parametru include_path możliwy jest za pośrednictwem funkcji get_include_path(). Opisywane uzupełnienie ścieżek przeszukiwania może wyglądać następująco: set_include_path(get_include_path() . PATH_SEPARATOR . "/home/john/phplib/");

Stała PATH_SEPARATOR będzie w systemach Unix zamieniana na znak dwukropka, a w systemach Windows na znak średnika; jej stosowanie przybliża nas więc do pożądanej wysokiej przenośności kodu aplikacji.

102

ROZDZIAŁ 5.  NARZĘDZIA OBIEKTOWE

Automatyczne wczytywanie kodu W pewnych okolicznościach pożądana jest taka organizacja klas, aby każda z nich była trzymana w osobnym pliku. Taki model ma swoje wady (włączanie dużej liczby małych plików może zwiększać ogólny koszt włączania), ale jest użyteczny, zwłaszcza kiedy system ma być rozbudowywany i ma korzystać z nowych klas w czasie wykonania (zobacz wzorzec Command w rozdziałach 11. i 12.). W takich przypadkach nazwy plików poszczególnych klas mogą mieć powiązania z nazwami klas zapisanych w tych plikach: klasa ShopProduct wyląduje więc w pliku ShopProduct.php. Można pójść o krok dalej i użyć konwencji nazw pakietowych PEAR. W takim przypadku, jeśli zechcemy zdefiniować klasę ShopProduct w pakiecie o nazwie business, powinniśmy plik o nazwie ShopProduct.php umieścić w katalogu o nazwie business. Samą klasę należy natomiast zdefiniować przez nazwę pakietową, a mianowicie business_ShopProduct. Alternatywnie, jeśli można sobie pozwolić na stosowanie przestrzeni nazw, można stosować konwencję PEAR odnośnie do rozmieszczenia plików (business/ShopProduct.php), ale pakietowe adresowanie klas przenieść z nazwy klasy do nazwy przestrzeni nazw. W PHP5 zaimplementowano mechanizmy ładowania klas pomocne w automatyzacji włączania plików klas. Domyślne działanie tych mechanizmów jest dość ograniczone, niemniej jednak już przydatne. Można z niego skorzystać, wywołując funkcję o nazwie spl_autoload_register() (bez argumentów). Po aktywowaniu w ten sposób mechanizmu automatycznego ładowania klas za każdym razem, kiedy zechcemy utworzyć egzemplarz nieznanej jeszcze klasy, dojdzie do wywołania specjalnej funkcji o nazwie spl_autoload(). Funkcja spl_autoload() otrzyma w wywołaniu nazwę klasy i spróbuje użyć przekazanej nazwy (po konwersji na małe litery) uzupełnionej o rozszerzenie (domyślnie .php lub .inc) do znalezienia pliku klasy w systemie plików. Oto prosty przykład: spl_autoload_register(); $writer = new Writer();

Przy założeniu, że nie włączyliśmy jeszcze do aplikacji pliku zawierającego klasę Writer, powyższa próba utworzenia obiektu nie może się udać. Ale skoro wcześniej uruchomiliśmy mechanizm automatycznego ładowania klas, PHP spróbuje znaleźć i włączyć do aplikacji plik writer.php lub writer.inc, i ponownie przeprowadzić konkretyzację obiektu klasy Writer. Jeśli któryś z tych plików istnieje i zawiera klasę Writer, druga próba zakończy się sukcesem. Domyślny mechanizm ładowania klas obsługuje przestrzenie nazw, odwzorowując kolejne nazwy pakietowe na nazwy katalogów. Poniższy kod: spl_autoload_register(); $writer = new util\Writer();

sprowokuje wyszukanie pliku o nazwie writer.php (pamiętajmy o zamianie wielkości liter w nazwie klasy) w katalogu o nazwie util. A jeśli pliki z klasami będą miały nazwy zawierające wielkie litery? Jeśli klasa Writer zostanie umieszczona w pliku Writer.php, to domyślny mechanizm ładowania klas nie poradzi sobie z odszukaniem pliku klasy. Na szczęście możemy rejestrować własne funkcje obsługi ładowania klas, w których można implementować dowolne konwencje odwzorowania nazwy klasy na plik. Aby skorzystać z tego udogodnienia, należy do wywołania spl_autoload_register() przekazać referencję do własnej (może być anonimowa) funkcji ładującej. Funkcja ładująca powinna przyjmować pojedynczy argument. Jeśli wtedy PHP napotka próbę utworzenia egzemplarza niezaładowanej jeszcze klasy, zainicjuje wywołanie naszej funkcji z pojedynczym argumentem zawierającym nazwę klasy. Funkcja ładująca może zupełnie arbitralnie definiować strategię odwzorowania i włączania brakujących plików klas. Po zakończeniu wykonywania funkcji ładującej PHP ponownie spróbuje utworzyć egzemplarz klasy. Oto prosty przykład własnej funkcji ładującej: function straightIncludeWithCase($classname) { $file = "{$classname}.php"; if (file_exists($file)) { require_once($file); } } spl_autoload_register('straightIncludeWithCase'); $product = new ShopProduct('The Darkening', 'Harry', 'Hunter', 12.99);

103

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Po nieudanej pierwszej próbie utworzenia obiektu klasy ShopProduct PHP uwzględni funkcję ładującą zarejestrowaną wywołaniem spl_register_function() i przekaże do niej ciąg znaków "ShopProduct". Nasza implementacja tej funkcji ogranicza się jedynie do próby włączenia pliku o nazwie skonstruowanej na bazie przekazanego ciągu. Poprawne włączenie pliku jest uwarunkowane jego obecnością w bieżącym katalogu roboczym albo w jednym z katalogów wymienionych w ramach parametru include_path (czy to zgodnie ze starą konwencją nazewniczą PEAR, czy to zgodnie z konwencją przestrzeni nazw). Bardzo łatwo zaimplementować też obsługę nazw pakietowych PEAR: function replaceUnderscores($classname) { $path = str_replace('_', DIRECTORY_SEPARATOR, $classname); if (file_exists("{$path}.php")) { require_once("{$path}.php"); } } spl_autoload_register('replaceUnderscores'); $x = new ShopProduct(); $y = new business_ShopProduct();

W powyższej implementacji w ciele funkcji replaceUnderscores() następuje dopasowanie znaków podkreślenia występujących w argumencie wywołania $classname i ich zamiana na znaki separatora katalogów (w systemach uniksowych rolę tę pełnią znaki ukośnika — /). Ostatecznie więc do skryptu włączany jest plik business/ShopProduct.php. Jeśli taki plik istnieje, a zawarta w nim klasa ma odpowiednią nazwę, uda się skutecznie skonkretyzować obiekt klasy. To znaczne ułatwienie, pod warunkiem że programiści dostosują się i będą się konsekwentnie trzymać raz przyjętej konwencji nazewniczej klas i konwencji rozmieszczania plików definicji (i unikać stosowania znaków podkreśleń, jeśli nie reprezentują one katalogowego rozmieszczenia pakietów). A co z przestrzeniami nazw? Wiemy, że domyślny mechanizm ładowania klas obsługuje przestrzenie nazw, odwzorowując je na podkatalogi. Ale jeśli przesłaniamy mechanizm ładowania własną funkcją, musimy ten przypadek również obsłużyć samodzielnie. Jest to zresztą jedynie kwestia dopasowania i zastąpienia znaków lewego ukośnika: function myNamespaceAutoload($path) { if (preg_match('/\\\\/', $path)) { $path = str_replace('\\', DIRECTORY_SEPARATOR, $path); } if ( file_exists("{$path}.php")) { require_once("{$path}.php"); } }

Wartość przekazywana do funkcji ładującej jest zawsze znormalizowana do postaci pełnej kwalifikowanej nazwy klasy, z pominięciem pierwszego ukośnika, nie ma więc potrzeby martwienia się o rozpoznanie przestrzeni nazw czy uwzględnianie aliasów klas. A jak zrealizować rozpoznawanie nazw klas w konwencji PEAR i nazw klas używających przestrzeni nazw? Cóż, wystarczy połączyć dwie pokazywane implementacje funkcji ładujących i przekształcić je w jedną funkcję uniwersalną. Można też zarejestrować więcej niż jedną funkcję ładującą, bo funkcje rejestrowane przez spl_register_autoload() są zachowywane w kolejności rejestracji: spl_autoload_register('replaceUnderscores'); spl_autoload_register('myNamespaceAutoload'); $x $y $z $a

= = = =

104

new new new new

ShopProduct(); business_ShopProduct(); business\ShopProduct2(); \business\ShopProduct3();

ROZDZIAŁ 5.  NARZĘDZIA OBIEKTOWE

Kiedy PHP napotka nieznaną klasę, będzie wywoływać kolejno funkcje replaceUnderscores() i myNamespaceAutoload() do momentu, kiedy kolejna próba utworzenia obiektu zakończy się powodzeniem albo wyczerpie się lista funkcji ładujących. Oczywiście stosowanie kaskady funkcji ładujących oznacza pewien narzut czasowy wykonania, po co więc wprowadzono taką możliwość? Otóż w praktyce łączy się odwzorowanie klas według różnych konwencji w jednej funkcji ładującej. Ale w większych systemach, z dużym udziałem komponentów zewnętrznych, konieczność rejestrowania dodatkowych funkcji ładujących może się okazać nieunikniona — wiele bibliotek stosuje własne, unikatowe mechanizmy ładowania. Kaskada funkcji ładujących pozwala na realizowanie niezależnych metod ładowania klas w poszczególnych komponentach. Zresztą kiedy porządnie napisana biblioteka nie potrzebuje już stosować własnego mechanizmu ładowania, może swoją funkcję ładującą wyrejestrować wywołaniem metody spl_unregister_function()!  Uwaga PHP obsługuje funkcję __autoload(), realizującą znacznie mniej zaawansowany mechanizm zarządzania automatyzacją włączania plików; jeśli zaimplementujemy taką funkcję, PHP przekaże do niej kompetencję ładowania klas w przypadku nieudanej próby utworzenia obiektu. Jest to jednak podejście mniej uniwersalne, bo funkcja __autoload() może mieć tylko jedną implementację; jest też wielce prawdopodobne, że w przyszłych wersjach języka PHP zostanie ona wycofana z użycia.

Klasy i funkcje pomocnicze Programista języka PHP ma do swojej dyspozycji szereg klas i funkcji służących do analizowania obiektów. Jaka jest ich przydatność? W końcu większość klas wykorzystywanych w projekcie konstruujemy sami i mamy pełną wiedzę o strukturze ich obiektów. Często jednak nie posiadamy wystarczających informacji o obiektach wykorzystywanych w czasie wykonywania skryptu, niejednokrotnie bowiem własne projekty opieramy na transparentnym delegowaniu zadań do klas autorstwa osób trzecich. W takich przypadkach obiekt konkretyzuje się często jedynie na podstawie dynamicznie konstruowanej nazwy klasy. PHP pozwala na dynamiczne odwołania do klas za pośrednictwem ciągów znaków, jak tutaj: // plik Task.php namespace tasks; class Task { function doSpeak() { print "Ahoj\n"; } } // Plik TaskRunner.php $classname = "Task"; require_once( "tasks/{$classname}.php" ); $classname = "tasks\\$classname"; $myObj = new $classname(); $myObj->doSpeak();

Ciąg przypisywany powyżej do zmiennej $classname typowo odczytywany jest z pliku konfiguracyjnego albo określany na podstawie odebranego żądania z zawartością katalogu. Ciąg taki można wykorzystać do wczytania pliku definicji klasy i konkretyzacji jej obiektu. Zauważmy, że w tym fragmencie skonstruowaliśmy de facto kwalifikację przestrzeni nazw. Operacje tego rodzaju wykorzystywane są w takich systemach, które mają zapewniać możliwość uruchamiania dodatków i rozszerzeń definiowanych zewnętrznie. Zanim dopuścimy tego rodzaju rozwiązanie w prawdziwym (a nie tylko przykładowym) skrypcie, powinniśmy jeszcze upewnić się, że żądana klasa istnieje, a także sprawdzić, czy udostępnia oczekiwane metody itd.

105

PHP. OBIEKTY, WZORCE, NARZĘDZIA

 Uwaga Nawet w obliczu zabezpieczeń należy zachować szczególną ostrożność przy dynamicznym instalowaniu zewnętrznego kodu. Nie powinno się pod żadnym pozorem automatycznie ładować kodu dostarczanego przez użytkowników zewnętrznych: każdy tak zainstalowany dodatek może zazwyczaj wykonywać się z uprawnieniami właściwymi dla całej aplikacji, więc złośliwy kod może spowodować niemałe zamieszanie w systemie. Nie oznacza to, że dynamiczne ładowanie kodu nie jest w ogóle przydatne; możliwość rozszerzania podstawowej funkcjonalności systemu przez programistów trzecich może zaowocować znacznym zwiększeniem elastyczności systemu. Aby przy tym zachować jego bezpieczeństwo, można na przykład rozważyć zabezpieczenie w postaci wydzielenia katalogu kodu ładowanego dynamicznie, z uprawnieniami ograniczonymi do grona administratorów albo programistów zarejestrowanych i posiadających konto w specjalnie wydzielonym repozytorium; w takim układzie administrator systemu mógłby osobiście rewidować kod z repozytorium i wybiórczo instalować rozszerzenia. W ten sposób działa na przykład popularna platforma WordPress.

Niektóre z funkcji analizy klas zostały w PHP5 zdublowane w ramach znacznie rozbudowanego interfejsu Reflection API, któremu przyjrzymy się w dalszej części rozdziału. Jednak ich prostota i łatwość użycia czynią je bardzo wygodnymi narzędziami.

Szukanie klasy Funkcja class_exists() przyjmuje w wywołaniu ciąg reprezentujący klasę do zbadania i zwraca wartość logiczną: true, jeśli klasa istnieje, i false, jeśli nie istnieje (nie napotkano dotąd definicji klasy). Za pomocą tej funkcji możemy uczynić poprzedni fragment kodu odrobinę bezpieczniejszym: // plik TaskRunner.php $classname = "Task"; $path = "tasks/{$classname}.php"; if (!file_exists($path)) { throw new Exception("Brak pliku {$path}"); } require_once($path); $qclassname = "tasks\\$classname"; if (!class_exists($qclassname)) { throw new Exception("Brak klasy $qclassname"); } $myObj = new $qclassname(); $myObj->doSpeak();

Nie daje nam to jeszcze pewności co do wymaganych argumentów wywołania konstruktora. Aby mieć taką pewność i uczynić konkretyzację obiektu jeszcze bardziej niezawodną, musimy uciec się do interfejsu Reflection API opisywanego w dalszej części rozdziału. Tak czy inaczej wywołanie class_exists() pozwala na sprawdzenie obecności klasy przed próbą jej użycia.  Uwaga Pamiętajmy, że zawsze należy ostrożnie korzystać z danych pobieranych ze źródeł zewnętrznych. Każdorazowo trzeba je weryfikować przed właściwym użyciem. W przypadku ścieżki dostępu do pliku należy usunąć albo oznaczyć kropki oraz znaki separatora katalogów — w ten sposób zabezpiecza się kod przed niepożądaną zmianą katalogu i włączeniem do programu nieoczekiwanych plików. Natomiast w przypadku budowania rozszerzalnych systemów techniki te dotyczą generalnie właściciela systemu (posiadającego uprawnienia do zapisu plików w katalogach), a nie użytkowników zewnętrznych.

Programista może również uzyskać tablicę wszystkich zdefiniowanych dotąd klas — wystarczy, że wywoła funkcję get_declared_classes(): print_r(get_declared_classes());

106

ROZDZIAŁ 5.  NARZĘDZIA OBIEKTOWE

Po wykonaniu tej instrukcji na wyjściu skryptu pojawi się lista klas, obejmująca zarówno klasy definiowane przez użytkownika, jak i te wbudowane. Warto pamiętać, że lista obejmuje jedynie te klasy, których deklaracje zostały przetworzone przed momentem wywołania funkcji. Lista klas używanych w toku wykonania skryptu może przecież być później uzupełniana, choćby za pomocą wywołań require() czy require_once().

Badanie obiektów i klas Jak już Czytelnikowi wiadomo, obiektowe typy argumentów wywołania metod mogą być narzucane przez klasy. Mimo to nie zawsze możemy jednak mieć pewność co do konkretnego typu obiektu przetwarzanego w ramach klasy — w czasie przygotowywania tej publikacji język PHP nie pozwalał bowiem choćby na wymuszenie konkretnego typu obiektu zwracanego przez metody — taki mechanizm jest zapowiadany w następnych wydaniach PHP. Typ obiektu można sprawdzać za pośrednictwem rozmaitych narzędzi. Przede wszystkim programista może sprawdzać klasę obiektu — służy do tego funkcja get_class(). Funkcja ta przyjmuje w wywołaniu obiekt dowolnej klasy i zwraca ciąg znaków reprezentujący nazwę klasy: $product = getProduct(); if (get_class($product) === 'CdProduct') { print "\$product to obiekt klasy CdProduct\n"; }

W powyższym przykładzie pobieramy coś z funkcji getProduct(). Aby zyskać pewność, że zwrócona wartość jest oczekiwanym obiektem klasy CdProduct, korzystamy z wywołania funkcji get_class().  Uwaga Klasy CdProduct i BookProduct były prezentowane w rozdziale 3.

Oto kod funkcji getProduct(): function getProduct() { return new CdProduct("Exile on Coldharbour Lane", "The", "Alabama 3", 25.99, 60.33); }

Jak widać, funkcja getProduct() po prostu konkretyzuje obiekt klasy CdProduct. Przyda się nam ona w tym rozdziale jeszcze wielokrotnie. Funkcja get_class() jest narzędziem bardzo szczególnym, często potrzebujemy zaś bardziej ogólnego potwierdzenia typu klasy. Możemy na przykład próbować określić przynależność obiektu do hierarchii ShopProduct, ale bez rozróżniania pomiędzy poszczególnymi klasami tej hierarchii — nie interesuje nas bowiem, czy obiekt jest klasy BookProduct, czy CdProduct; ważne, że reprezentuje jakiś asortyment. Aby to stwierdzić, należy posłużyć się operatorem instanceof.  Uwaga W PHP4 brakowało operatora instanceof. Zamiast niego dostępna była funkcja is_a(), która jednak w wersji 5.0 została oznaczona jako zarzucona. Ponownie przywrócono ją w PHP 5.3.

Operator instanceof działa na dwóch operandach: lewym jest obiekt podlegający badaniu pod kątem przynależności do hierarchii klas, a prawy to nazwa klasy albo interfejsu. Jeśli obiekt jest egzemplarzem danej klasy (interfejsu), operator zwraca wartość logiczną true. $product = getProduct(); if ($product instanceof ShopProduct) { print "\$product jest obiektem klasy ShopProduct\n"; }

107

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Pozyskiwanie ciągu pełnej nazwy klasy Przestrzenie nazw umożliwiły wyeliminowanie wielu niedogodności obiektowej implementacji PHP. Nie musimy już tolerować niedorzecznie rozbudowanych nazw klas ani ryzykować kolizji nazw (to dotyczy już tylko zastanego, niezmodernizowanego kodu). Z drugiej strony, względne odwołania do przestrzeni nazw i aliasy utrudniają niekiedy określenie pełnej nazwy klasy, jak w poniższych przypadkach: namespace mypackage; use util as u; use util\db\Querier as q; class Local {} // Zagadki: // Przestrzeń nazw określana przez alias // u\Writer; // Klasa określana przez alias // q; // Klasa wymieniana w kontekście lokalnym // Local

Określenie właściwej nazwy klasy nie wydaje się bardzo trudne, ale implementacja kodu, który zadziała poprawnie we wszystkich możliwych kombinacjach, jest już kłopotliwa. Weźmy na przykład u\Writer. W przypadku takiej nazwy automat musiałby „wiedzieć”, że u jest aliasem przestrzeni nazw util, a nie właściwą nazwą przestrzeni nazw. Na szczęście w PHP 5.5 wprowadzono składnię odwołania NazwaKlasy::class. Innymi słowy, dowolną posiadaną referencję do klasy możemy uzupełnić o operator zasięgu i słowo kluczowe class w celu pozyskania pełnej kwalifikowanej nazwy klasy. Tak więc poniższy kod: print u\Writer::class."\n"; print q::class."\n"; print Local::class."\n";

wypisze na wyjściu: util\Writer util\db\Querier mypackage\Local

Badanie metod Za pośrednictwem funkcji get_class_methods() możemy pozyskać listę wszystkich metod udostępnianych przez klasę. Funkcja ta wymaga przekazania w wywołaniu nazwy klasy, a zwraca tablicę z nazwami wszystkich metod tejże klasy: print_r(get_class_methods('CdProduct'));

Jeśli założymy dostępność klasy CdProduct, na wyjściu powinno pojawić się coś takiego: Array ( [0] [1] [2] [3]

108

=> => => =>

__construct getPlayLength getSummaryLine getProducerFirstName

ROZDZIAŁ 5.  NARZĘDZIA OBIEKTOWE

[4] [5] [6] [7] [8] [9]

=> => => => => =>

getProducerMainName setDiscount getDiscount getTitle getPrice getProducer

)

W tym przykładzie przekazujemy w wywołaniu funkcji get_class_methods() nazwę klasy zdefiniowanej w poprzednich rozdziałach i wynik wywołania przekazujemy natychmiast do funkcji print_r(), wypisującej go na wyjście skryptu. Identyczny efekt osiągnęlibyśmy, przekazując w wywołaniu get_class_methods() nie nazwę klasy, a jej obiekt. Użytkownicy najwcześniejszych wersji PHP5 zobaczą na wykazie komplet metod — w nieco późniejszych wersjach wykaz introspekcji klasy obejmuje jedynie metody publiczne. Nazwy metod są reprezentowane jako ciągi znaków, co daje możliwość dynamicznego konstruowania ich wywoływań na rzecz obiektu, jak tutaj: $product = getProduct(); $method = "getTitle"; print $product->$method();

// pozyskanie obiektu… // konstrukcja nazwy metody… // wywołanie metody…

Takie konstrukcje mogą oczywiście być groźne. Co będzie w przypadku nieobecności metody w klasie? Oczywiście skrypt zostanie przerwany z powodu krytycznego błędu wykonania. Znamy już jeden sposób testowania klasy na obecność metody: if (in_array($method, get_class_methods($product))) { print $product->$method(); // wywołanie metody… }

Upewniamy się w ten sposób co do istnienia w klasie obiektu konkretnej metody. Jednak ten sam test możemy wykonać za pośrednictwem bardziej specjalizowanego narzędzia dostępnego w PHP. Nazwy metod możemy konfrontować z definicją klasy za pośrednictwem dwóch funkcji: is_callable() i method_exists(). Pierwsza z tych dwóch funkcji jest bardziej specjalizowana; przyjmuje ciąg znaków reprezentujący nazwę funkcji i zwraca true, jeśli funkcja istnieje i może zostać wywołana. W przypadku metod argument wywołania powinien mieć postać tablicy, której pierwszy element zawiera obiekt albo nazwę klasy, a drugi — nazwę metody do sprawdzenia. W tej wersji wywołania wartość true zwracana z funkcji oznacza obecność metody w klasie: if (is_callable(array($product, $method))) { print $product->$method(); // wywołanie metody… }

Funkcja is_callable() opcjonalnie przyjmuje drugi argument wywołania, którym powinna być zmienna logiczna. Jeśli ustawimy ją na true, funkcja będzie sprawdzać jedynie składnię danej nazwy, a nie faktyczną obecność metody czy funkcji o takiej nazwie. Funkcja method_exists() wymaga przekazania obiektu (albo nazwy klasy) oraz nazwy metody i zwraca true, jeśli w klasie danego obiektu występuje wskazana metoda: if (method_exists($product, $method)) { print $product->$method(); // wywołanie metody… }

 Ostrzeżenie Warto pamiętać, że obecność metody w klasie nie oznacza jeszcze możliwości jej wywołania w danym kontekście. Funkcja method_exists() zwraca bowiem true również dla metod oznaczonych jako prywatne i zabezpieczone, których nie da się wywołać spoza klasy obiektu.

109

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Badanie składowych Tak jak można wykrywać w klasie obecność metod, można też badać ją pod kątem obecności konkretnych składowych. Pełny wykaz składowych zwraca funkcja get_class_vars() przyjmująca w wywołaniu nazwę klasy. Zwracana przez nią tablica asocjacyjna zawiera nazwy składowych w roli kluczy i wartości składowych w roli wartości. Spróbujmy przetestować za jej pomocą zawartość składowych w klasie CdProduct. Dla lepszej ilustracji działania funkcji uzupełnimy tę klasę o publiczną składową CdProduct::$coverUrl: print_r(get_class_vars('CdProduct'));

Jako wynik ujawni się tylko publiczna składowa: Array ( [coverUrl] => )

Badanie relacji dziedziczenia Funkcje badania klas pozwalają również na rozpoznawanie relacji dziedziczenia. Możemy więc dla danej klasy znaleźć jej klasę bazową — służy do tego funkcja get_parent_class(). Funkcja ta wymaga przekazania albo obiektu, albo nazwy klasy, a zwraca nazwę klasy nadrzędnej (bazowej), jeśli taka istnieje. W przeciwnym przypadku — czyli kiedy badana klasa nie posiada klasy bazowej — funkcja zwraca wartość false: print get_parent_class('CdProduct');

Łatwo się domyślić, że w wyniku otrzymamy nazwę klasy nadrzędnej: ShopProduct. Relację dziedziczenia możemy też analizować za pośrednictwem funkcji is_subclass_of(). Wymaga ona przekazania obiektu klasy pochodnej i nazwy klasy bazowej. Jeśli relacja dziedziczenia faktycznie zachodzi, tzn. jeśli klasa przekazanego obiektu faktycznie jest pochodną klasy określonej za pomocą drugiego argumentu domniemanej klasy bazowej, funkcja zwraca true: $product = getProduct(); // pozyskanie obiektu if (is_subclass_of($product, 'ShopProduct')) { print "CdProduct to klasa pochodna klasy ShopProduct\n"; }

Funkcja is_subclass_of() informuje jedynie o relacjach w obrębie drzewa dziedziczenia klas. Nie zwraca natomiast informacji o tym, że dana klasa implementuje interfejs. Do tego celu należy użyć operatora instanceof, ewentualnie funkcji wchodzącej w skład standardowej biblioteki języka PHP SPL (Standard PHP Library), a mianowicie funkcji class_implements(), która przyjmuje nazwę klasy bądź referencję obiektu i zwraca tablicę interfejsów implementowanych przez daną klasę (obiekt). if (in_array('jakisInterfejs', class_implements($product))) { print "CdProduct jest interfejsem jakisInterfejs\n"; }

Badanie wywołań metod Prezentowałem już przykład próby wywołania metody, której nazwa była określona poprzez dynamicznie konstruowany ciąg znaków: $product = getProduct(); $method = "getTitle"; print $product->$method();

110

// pozyskanie obiektu… // konstrukcja nazwy metody… // wywołanie metody…

ROZDZIAŁ 5.  NARZĘDZIA OBIEKTOWE

Programista PHP może podobny efekt uzyskać za pośrednictwem wywołania funkcji call_user_func(). Funkcja ta nadaje się tak do wywoływania zwykłych funkcji, jak i metod klas. Wywołanie funkcji wymaga przekazania pojedynczego argumentu — ciągu znaków zawierającego nazwę funkcji: $returnVal = call_user_func("myFunction");

Wywołanie metody wymaga już przekazania tablicy. Pierwszym jej elementem powinien być obiekt, drugim zaś metoda, którą funkcja ma na rzecz owego obiektu wywołać: $returnVal = call_user_func(array($myObj, "methodName"));

Argumenty wywołania docelowej funkcji czy metody, realizowanego za pośrednictwem funkcji call_user_func(), należy przekazywać za pośrednictwem kolejnych (to jest za pośrednictwem trzeciego i następnych) argumentów wywołania call_user_func(): $product = getProduct(); // pozyskanie obiektu call_user_func(array($product, 'setDiscount'), 20);

Powyższe dynamicznie skonstruowane wywołanie jest rzecz jasna równoznaczne poniższemu wywołaniu statycznemu: $product->setDiscount(20);

Przydatność funkcji call_user_func() jest o tyle ograniczona, że dynamiczne wywołanie metody możemy skonstruować również samodzielnie: $method = "setDiscount"; $product->$method(20);

Znacznie większe wrażenie robi już funkcja call_user_func_array(). Działa ona podobnie jak call_user_func(), przynajmniej jeśli chodzi o sposób określania docelowej funkcji czy metody wywołania. Tyle że wszelkie argumenty przekazywane do owego wywołania przyjmuje za pośrednictwem tablicy. Cóż w tym niezwykłego? Otóż niekiedy otrzymujemy zestaw argumentów właśnie w postaci tablicy. Jeśli nie znamy z góry liczby jej elementów, przekazanie argumentów w wywołaniu może się skomplikować. Przykład mieliśmy choćby w rozdziale 4., przy okazji implementowania klas delegujących chybione wywołania do innych klas. Oto uproszczony przykład takiej metody przechwytującej: function __call($method, $args) { if (method_exists($this->thirdpartyShop, $method)) { return $this->thirdpartyShop->$method(); } }

Powyższa metoda jest wywoływana w obliczu próby wywołania na rzecz obiektu klasy niezdefiniowanej w tej klasie metody. W tym przykładzie owo chybione wywołanie delegujemy do obiektu przechowywanego za pośrednictwem składowej $thirdpartyShop. Jeśli w owym obiekcie wykryjemy obecność metody pasującej do argumentu $method, wywołujemy ją na rzecz obiektu $thirdpartyShop. Zakładamy przy tym, że docelowa metoda nie przyjmuje żadnych argumentów — założenie takie może zaś okazać się chybione. Pisząc kod metody __call(), nie mamy przecież możliwości określenia z góry rozmiaru tablicy argumentów $args. Gdybyśmy zaś po prostu przekazali tablicę $args wprost do metody docelowej, naruszylibyśmy być może składnię jej wywołania — wynikiem może być wiele, a nie tylko jeden (choćby i tablicowy) argument. Problem rozwiązuje właśnie funkcja call_user_func_array(): function __call($method, $args) { if (method_exists($this->thirdpartyShop, $method)) { return call_user_func_array( array($this->thirdpartyShop, $method), $args); } }

111

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Interfejs retrospekcji — Reflection API Interfejs retrospekcji Reflection API jest dla PHP tym, czym dla Javy jest pakiet java.lang.reflect. Interfejs Reflection API składa się z wbudowanych klas umożliwiających badanie metod, składowych i klas. W pewnych aspektach dubluje dostępne już wcześniej funkcje, jak choćby get_class_vars(), jest jednak nieporównywalnie bardziej elastyczny i szczegółowy. Do tego uwzględnia najnowsze obiektowe elementy PHP, jak kontrolę widoczności i dostępu do składowych, interfejsy i ich implementacje czy klasy abstrakcyjne — próżno tych udogodnień szukać w starszych funkcjach opisujących cechy klas.

Zaczynamy Interfejs retrospekcji nie służy wyłącznie do analizy klas. Na przykład klasa ReflectionFunction wykorzystywana jest do pozyskiwania informacji o zwykłych funkcjach, a klasa ReflectionExtension określa szczegóły rozszerzeń kompilowanych do języka. Wybrane klasy tego rozbudowanego interfejsu wymienia tabela 5.1. Tabela 5.1. Wybrane klasy interfejsu Reflection API Klasa

Opis

Reflection

Udostępnia statyczną metodę export() generującą zestawienia informacji o klasach.

ReflectionClass

Informacje i narzędzia badania klas.

ReflectionMethod

Informacje i narzędzia badania metod.

ReflectionParameter

Informacje i narzędzia badania argumentów metod.

ReflectionProperty

Informacje i narzędzia badania składowych.

ReflectionFunction

Informacje i narzędzia badania funkcji.

ReflectionExtension

Informacje o rozszerzeniach PHP.

ReflectionException

Klasa wyjątku.

ReflectionZendExtension

Informacje o rozszerzeniach PHP Zend.

Klasy interfejsu Reflection API dają bezprecedensową możliwość dynamicznego odwoływania się do informacji o obiektach, funkcjach i wyjątkach przetwarzanych w skrypcie. Z racji możliwości i zakresu zastosowań owego interfejsu należy go preferować wobec realizujących podobne zadania funkcji. Wkrótce Czytelnik przekona się, że interfejs ten jest wprost nieocenionym narzędziem badania klas. Można za jego pomocą generować diagramy na potrzeby dokumentacji albo utrwalać informacje o obiektach w bazach danych, czy też wreszcie analizować metody akcesory dostępne w obiekcie celem ustalenia nazw jego składowych. Jeszcze jednym zastosowaniem interfejsu Reflection jest konstruowanie szkieletu wywołań metod w klasach wedle pewnej konwencji nazewniczej.

Pora zakasać rękawy Wiemy już, że atrybuty klas można analizować za pośrednictwem zestawu specjalnych funkcji. Wiele z tych funkcji nie spełnia jednak wszystkich naszych wymagań, zwłaszcza w odniesieniu do rozszerzeń obiektowych wprowadzonych w PHP5. Pora więc przyjrzeć się narzędziu, które takich wad nie posiada. Klasa ReflectionClass pozwala na pozyskanie informacji o każdym dosłownie aspekcie danej klasy — a działa równie skutecznie wobec klas definiowanych przez użytkownika, jak i wobec klas wbudowanych. Jedynym argumentem wywołania konstruktora klasy ReflectionClass jest nazwa klasy wyznaczonej do analizy: $prod_class = new ReflectionClass('CdProduct'); Reflection::export($prod_class);

112

ROZDZIAŁ 5.  NARZĘDZIA OBIEKTOWE

Po utworzeniu obiektu klasy ReflectionClass można za pośrednictwem klasy narzędziowej Reflection wypisać informacje o klasie CdProduct na wyjście skryptu. Klasa Reflection udostępnia statyczną metodę export(), która formatuje i wypisuje informacje zebrane w obiekcie retrospekcji Reflection (ściśle mówiąc, w dowolnym obiekcie dowolnego obiektu implementującym interfejs Reflection). Oto fragment wydruku generowanego przez metodę Reflection::export(): Class [ class CdProduct extends ShopProduct ] { @@ fullshop.php 53-73 - Constants [0] { } - Static properties [0] { } - Static methods [0] { } - Properties [2] { Property [ private $playLength ] Property [ protected $price ] } - Methods [10] { Method [ public method __construct ] { @@ fullshop.php 56 - 61 - Parameters [5] Parameter #0 [ Parameter #1 [ Parameter #2 [ Parameter #3 [ Parameter #4 [ }

{



$title ] $firstName ] $mainName ] $price ] $playLength ]

} Method [ public method getPlayLength ] { @@ fullshop.php 63 - 65 } Method [ public method getSummaryLine ] { @@ fullshop.php 67 - 71 } } }

Jak widać, metoda Reflection::export() daje dostęp do znacznej ilości informacji o klasie. Reflection::export() w generowanym zestawieniu uwzględnia każdy niemal aspekt klasy CdProduct, w tym informacje o widoczności i dostępie do metod i składowych, o argumentach poszczególnych metod i położeniu kodu każdej metody w pliku kodu definiującego klasę. Szczegółowość informacji jest zdecydowanie wyższa niż w tradycyjnie wykorzystywanej w podobnych zastosowaniach funkcji diagnostycznej var_dump(). Wprawdzie funkcja ta wymaga konkretyzacji obiektu, dla którego ma wygenerować zestawienie diagnostyczne, ale mimo to nie dorównuje szczegółowością diagnozy metodzie Reflection::export(): $cd = new CdProduct("cd1", "bob", "bobbleson", 4, 50 ); var_dump( $cd );

113

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Jako wynik tego programu zobaczymy: object(CdProduct)#1 (6) { ["playLength:private"]=> int(50) ["title:private"]=> string(3) "cd1" ["producerMainName:private"]=> string(9) "bobbleson" ["producerFirstName:private"]=> string(3) "bob" ["price:protected"]=> int(4) ["discount:private"]=> int(0) }

Funkcja var_dump() i spokrewniona z nią print_r() są niezwykle wygodne, jeśli celem jest ekspozycja danych w skryptach. Jednak w przypadku klas i funkcji interfejs Reflection API przenosi diagnostykę i analizę na nowy poziom.

Badanie klasy Metoda Reflection::export() jest znakomitym źródłem informacji diagnostycznych, ale interfejs Reflection da się też wykorzystywać w sposób bardziej specjalizowany — za pośrednictwem jego specjalizowanych klas. Wiemy już, jak konkretyzować obiekt klasy ReflectionClass: $prod_class = new ReflectionClass('CdProduct');

Możemy teraz spróbować wykorzystać powołany do życia obiekt klasy RefectionClass do dynamicznej analizy klasy CdProduct. Jakiego rodzaju jest klasą? Czy da się utworzyć jej egzemplarz? Na pytania te odpowie następująca funkcja: function classData(ReflectionClass $class) { $details = ""; $name = $class->getName(); if ($class->isUserDefined()) { $details .= "$name to klasa definiowana przez użytkownika\n"; } if ($class->isInternal()) { $details .= "$name to klasa wbudowana\n"; } if ($class->isInterface()) { $details .= "$name definiuje interfejs\n"; } if ($class->isAbstract()) { $details .= "$name to klasa abstrakcyjna\n"; } if ($class->isFinal()) { $details .= "$name to klasa finalna\n"; } if ($class->isInstantiable()) { $details .= "Można tworzyć obiekty klasy $name\n"; } else { $details .= "Nie można tworzyć obiektów klasy $name\n"; } if ( $class->isCloneable() ) {

114

ROZDZIAŁ 5.  NARZĘDZIA OBIEKTOWE

$details .= "Można klonować obiekty klasy $name\n"; } else { $details .= "Nie można klonować obiektów klasy $name\n"; } return $details; } $prod_class = new ReflectionClass('CdProduct'); print classData($prod_class);

Tworzymy tu obiekt klasy ReflectionClass kojarzony z klasą CdProduct (której nazwa przekazywana jest w wywołaniu konstruktora klasy ReflectionClass). Następnie tak powołany do życia obiekt przekazujemy do funkcji classData(), która ilustruje sposób pozyskiwania niektórych informacji o klasie. Wywoływane w jej wnętrzu metody klasy ReflectionClass nie wymagają chyba komentarza — ograniczę się więc do króciutkiego opisu każdej z nich:  ReflectionClass::getName() zwraca nazwę badanej klasy.  ReflectionClass::isUserDefined() zwraca true, jeśli badana klasa jest klasą definiowaną przez użytkownika w kodzie skryptu PHP; analogicznie metoda ReflectionClass::isInternal() zwraca true, jeśli badana klasa jest klasą wbudowaną.  ReflectionClass::isAbstract() sprawdza, czy badana klasa jest klasą abstrakcyjną; bytność klasy jako interfejsu można zaś sprawdzić wywołaniem metody ReflectionClass::isInterface().  Metoda ReflectionClass::isInstantiable() informuje, czy klasa nadaje się do konkretyzacji, czyli czy można tworzyć jej egzemplarze. Wreszcie metoda ReflectionClass::isCloneable() pozwala na określenie, czy obiekty klasy implementują mechanizm klonowania. Diagnostyka może sięgać nawet do kodu źródłowego klas definiowanych przez użytkownika. Obiekt klasy ReflectionClass daje bowiem dostęp do informacji o nazwie pliku definicji klasy, podaje też początkowy i końcowy wiersz kodu źródłowego definicji klasy w tym pliku. Oto szybki sposób użycia klasy ReflectionClass do uzyskania dostępu do źródła klasy: class ReflectionUtil { static function getClassSource(ReflectionClass $class) { $path = $class->getFileName(); $lines = @file($path); $from = $class->getStartLine(); $to = $class->getEndLine(); $len = $to - $from + 1; return implode(array_slice($lines, $from - 1, $len)); } } print ReflectionUtil::getClassSource( new ReflectionClass('CdProduct')); ReflectionUtil to prosta klasa definiująca zaledwie jedną metodę statyczną — ReflectionUtil::getClassSource(). Jedynym argumentem jej wywołania jest obiekt klasy ReflectionClass, metoda zwraca zaś kod źródłowy wskazanej klasy. Nazwę pliku definicji klasy udostępnia wywołanie metody ReflectionClass::getFileName(); zwrócona nazwa jest ścieżką bezwzględną, więc można od razu otworzyć plik kodu. Listę wierszy kodu źródłowego z tego pliku pozyskuje się przez wywołanie funkcji file(). Numer pierwszego wiersza definicji klasy określa wywołanie ReflectionClass::getStartLine(), numer wiersza końcowego — wywołanie ReflectionClass::getEndLine(). Po uzyskaniu tych danych pozostaje już jedynie wyciąć z tablicy interesujące nas wiersze, wywołując funkcję array_slice(). Dla uproszczenia i gwoli zwięzłości w powyższym kodzie pominięto wszelką obsługę błędów. W prawdziwych aplikacjach należałoby oczywiście uzupełnić kod o stosowną kontrolę argumentów i wartości zwracanych.

115

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Badanie metod Tak jak klasa ReflectionClass pośredniczy w analizie klas, tak obiekt klasy ReflectionMethod pozwala na pozyskiwanie informacji o metodach klas. Obiekt klasy ReflectionMethod pozyskuje się na dwa sposoby: można bowiem albo pozyskać tablicę obiektów ReflectionMethod zwracaną przez wywołanie ReflectionClass::getMethods(), albo — jeśli interesuje nas pojedyncza metoda — skorzystać z wywołania ReflectionClass::getMethod() przyjmującego nazwę metody i zwracającego opisujący ją obiekt ReflectionMethod. Poniżej prezentowany jest sposób pierwszy: $prod_class = new ReflectionClass('CdProduct'); $methods = $prod_class->getMethods(); foreach($methods as $method) { print methodData($method); print "\n----\n"; } function methodData(ReflectionMethod $method) { $details = ""; $name = $method->getName(); if ($method->isUserDefined()) { $details .= "$name to metoda definiowana przez użytkownika\n"; } if ($method->isInternal()) { $details .= "$name to metoda wbudowana\n"; } if ($method->isAbstract()) { $details .= "$name to metoda abstrakcyjna\n"; } if ($method->isPublic()) { $details .= "$name jest metodą publiczną\n"; } if ($method->isProtected()) { $details .= "$name jest metodą zabezpieczoną\n"; } if ($method->isPrivate()) { $details .= "$name jest metodą prywatną\n"; } if ($method->isStatic()) { $details .= "$name to metoda statyczna\n"; } if ($method->isFinal()) { $details .= "$name to metoda finalna\n"; } if ($method->isConstructor()) { $details .= "$name to konstruktor\n"; } if ($method->isreturnsReference()) { $details .= "$name zwraca referencję (nie wartość)\n"; } return $details; }

Powyższy kod za pośrednictwem wywołania ReflectionClass::getMethods() pozyskuje tablicę obiektów opisujących metody klasy CdProduct, a następnie dokonuje przeglądu zawartości tablicy, wywołując dla każdego zawartego w niej obiektu ReflectionMethod funkcję methodData().

116

ROZDZIAŁ 5.  NARZĘDZIA OBIEKTOWE

Poszczególne wywołania w ciele funkcji methodData() nie wymagają raczej komentarza — funkcja sprawdza, czy bieżąca metoda jest definiowana przez użytkownika, czy może jest metodą wbudowaną, czy jest abstrakcyjna, czy jest publiczna, chroniona czy prywatna, czy jest statyczna, a może finalna. Dodatkowo funkcja sprawdza, czy metoda nie jest przypadkiem konstruktorem i czy zwraca wartości, czy referencje. Słowo komentarza: metoda ReflectionMethod::returnsReference() nie zwraca true, jeśli badana metoda zwraca obiekty, mimo że obiekty są w PHP5 przekazywane przez referencje, a nie wartości. Wywołanie ReflectionMethod::returnsReference() zwraca true jedynie wtedy, kiedy dana metoda została jawnie zadeklarowana jako zwracająca referencje (deklaracja taka polega na poprzedzeniu nazwy metody znakiem &). Jak można się spodziewać, i tym razem możemy spróbować odwołać się do kodu źródłowego metody, stosując zresztą technikę bardzo przypominającą tę stosowaną dla całych klas: class ReflectionUtil { static function getMethodSource(ReflectionMethod $method) { $path = $method->getFileName(); $lines = @file($path); $from = $method->getStartLine(); $to = $method->getEndLine(); $len = $to - $from + 1; return implode(array_slice($lines, $from - 1, $len)); } } $class = new ReflectionClass('CdProduct'); $method = $class->getMethod('getSummaryLine'); print ReflectionUtil::getMethodSource($method);

Wyodrębnienie kodu źródłowego jest bardzo proste, ponieważ klasa ReflectionMethod udostępnia komplet potrzebnych do tego informacji za pośrednictwem metod getFileName(), getStartLine() i getEndLine().

Badanie argumentów metod W PHP5 sygnatury metod mogą ograniczać typy argumentów obiektowych, przydatna więc byłaby możliwość analizowania tych deklaracji. Interfejs Reflection API udostępnia do tego celu klasę ReflectionParameter. Aby pozyskać obiekt tej klasy, należy odwołać się do obiektu ReflectionMethod, wywołując jego metodę ReflectionMethod::getParameters() — zwraca ona tablicę obiektów klasy ReflectionParameter. Obiekt klasy ReflectionParameter może dawać wywołującemu informacje o nazwie argumentu, o tym, czy argument jest przekazywany przez referencję (czyli czy został zadeklarowany w sygnaturze metody ze znakiem &), jak również o wymuszanej deklaracją klasie argumentu i o akceptacji w jego miejsce wartości pustej. Oto jedno z zastosowań metod klasy ReflectionParameter: $prod_class = new ReflectionClass(CdProduct); $method = $prod_class->getMethod("__construct"); $params = $method->getParameters(); foreach ($params as $param) { print argData($param)."\n"; } function argData(ReflectionParameter $arg) { $details = ""; $declaringclass = $arg->getDeclaringClass(); $name = $arg->getName(); $class = $arg->getClass(); $position = $arg->getPosition(); $details .= "\$$name na pozycji $position\n"; if (!empty($class)) { $classname = $class->getName(); $details .= "\$$name musi być obiektem klasy $classname\n";

117

PHP. OBIEKTY, WZORCE, NARZĘDZIA

} if ($arg->isPassedByReference()) { $details .= "\$$name jest przekazywany przez referencję\n"; } if ( $arg->isDefaultValueAvailable() ) { $def = $arg->getDefaultValue(); $details .= "\$$name has default: $def\n"; } return $details; }

W powyższym kodzie metoda ReflectionClass::getMethod() służy nam do pozyskania obiektu klasy ReflectionMethod opisującego wybraną metodę. Następnie za pośrednictwem zainicjowanego na rzecz tego obiektu wywołania metody getParameters() pobierana jest tablica obiektów ReflectionParameter. Są one kolejno wyodrębniane z tablicy i przekazywane do funkcji argData(). Ta z kolei w pierwszej kolejności sprawdza przez wywołanie ReflectionParameter:: getName() nazwę parametru. Wywoływana później metoda getClass() zwraca obiekt klasy ReflectionClass opisujący wymuszaną w sygnaturze metody klasę argumentu. Wreszcie kod sprawdza (za pomocą isPassedByReference), czy argument jest dany referencją i czy posiada wartość domyślną, którą ewentualnie dopisuje do ciągu zwracanego.

Korzystanie z retrospekcji Uzbrojeni w umiejętność korzystania (przynajmniej w podstawowym zakresie) z interfejsu Reflection API możemy zaprząc go do pracy. Załóżmy, że tworzymy klasę, która w sposób dynamiczny wywołuje obiekty klasy Module. Chodzi o to, aby kod mógł akceptować rozszerzenia i wtyczki autorstwa osób trzecich, możliwe do wywoływania z aplikacji bez potrzeby ciągłego zmieniania jej kodu. W tym celu można by zdefiniować w interfejsie albo w klasie bazowej Module metodę execute(), zmuszając wszystkie klasy pochodne Module do implementacji tej metody. Zakładamy też, że użytkownicy systemu będą mieć możliwość prowadzenia listy dostępnych modułów w zewnętrznym pliku konfiguracyjnym zapisanym w formacie XML. System powinien na podstawie tej listy zgromadzić odpowiednią liczbę obiektów Module i wywołać na rzecz każdego z nich metodę execute(). Jak jednak obsłużyć sytuację, w której każdy z modułów (obiektów Module) wymaga do wykonania swoich zadań odmiennego zestawu informacji? W takim przypadku stosowne klucze i wartości składowych powinny zostać zapisane w pliku XML, a twórca każdego obiektu Module powinien udostępnić zestaw stosownych akcesorów. Na takim fundamencie musimy sami już zapewnić prawidłowe wywołania akcesorów dla odpowiednich składowych. Oto pierwszy zarys interfejsu Module i kilku implementujących go klas: class Person { public $name; function __construct($name) { $this->name = $name; } } interface Module { function execute(); } class FtpModule implements Module { function setHost($host) { print "FtpModule::setHost(): $host\n"; } function setUser($user) {

118

ROZDZIAŁ 5.  NARZĘDZIA OBIEKTOWE

print "FtpModule::setUser(): $user\n"; } function execute() { // właściwe operacje obiektu } } class PersonModule implements Module { function setPerson(Person $person) { print "PersonModule::setPerson(): {$person->name}\n"; } function execute() { // właściwe operacje obiektu } }

Prezentowane tu klasy FtpModule i PersonModule udostępniają (na razie puste) implementacje metody execute(). Ponadto każda klasa implementuje pewne metody akcesory, których działanie ogranicza się chwilowo do sygnalizowania faktu wywołania. W naszym systemie przyjęliśmy konwencję, że wszystkie akcesory ustawiające przyjmują dokładnie jeden argument, którym jest albo ciąg znaków, albo obiekt dający się konkretyzować na podstawie pojedynczego ciągu znaków. Metoda PersonModule::setPerson() oczekuje przekazania obiektu klasy Person, więc uzupełniliśmy przykład o definicję tej klasy. Aby zacząć pracę z wykorzystaniem obiektów klas PersonModule i FtpModule, musimy jeszcze utworzyć klasę, która będzie te obiekty wywoływała. Nazwiemy ją ModuleRunner. Informacje odczytane z pliku konfiguracyjnego XML będą w niej reprezentowane wielowymiarową tablicą indeksowaną nazwą modułu. Oto kod klasy: class ModuleRunner { private $configData = array( "PersonModule" => array('person'=>'bob'), "FtpModule" => array('host' => 'przyklad.com', 'user' => 'anon') ); private $modules = array(); // … }

Składowa ModuleRunner::$configData przechowuje odwołania do dwóch klas implementujących interfejs Module. Każde takie odwołanie reprezentowane jest podtablicą gromadzącą zestaw składowych. Za tworzenie obiektów Module odpowiedzialna jest metoda init() klasy ModuleRuner zdefiniowana jak poniżej: class ModuleRunner { // … function init() { $interface = new ReflectionClass('Module'); foreach($this->configData as $modulename => $params) { $module_class = new ReflectionClass($modulename); if (!$module_class->isSubclassOf($interface)) { throw new Exception("nieznany typ modułu: $modulename"); } $module = $module_class->newInstance(); foreach ($module_class->getMethods() as $method) { $this->handleMethod($module, $method, $params); // metoda handleMethod() prezentowana na następnym listingu… } array_push($this->modules, $module); }

119

PHP. OBIEKTY, WZORCE, NARZĘDZIA

} // … } $test = new ModuleRunner(); $test->init();

Metoda init() przegląda tablicę ModuleRunner::$configData i dla każdego jej elementu opisującego moduł podejmuje próbę utworzenia obiektu klasy ReflectionClass. Jeśli konstruktor tej klasy zostanie wywołany z nazwą klasy nieistniejącej, generowany jest wyjątek — w praktycznych zastosowaniach trzeba by uzupełnić kod o obsługę tegoż wyjątku. Dalej za pośrednictwem wywołania ReflectionClass::isSubclassOf() sprawdzana jest przynależność klasy modułu do typu Module. Przed próbą wywołania metody execute() każdego z modułów należy najpierw skonkretyzować ich obiekty. To zadanie składamy na barki metody ReflectionClass::newInstance(). Metoda ta przyjmuje dowolną liczbę argumentów, które przekazuje do konstruktora odpowiedniej klasy (dla której skonkretyzowano uprzednio obiekt klasy ReflectionClass). Jeśli wszystko się powiedzie, wywołanie zwróci referencję nowego obiektu (w kodzie produkcyjnym należałoby zadbać o większą zachowawczość — wypadałoby choćby sprawdzić przed konkretyzacją obiektów Module, czy ich konstruktory faktycznie obchodzą się bez argumentów). Inicjowane potem wywołanie ReferenceClass::getMethods() zwraca tablicę obiektów ReflectionMethod reprezentujących wszystkie dostępne w danej klasie metody. Dla każdego elementu tej tablicy kod ten wywołuje metodę ModuleRunner::handleMethod(), przekazuje do niej egzemplarz obiektu Module, obiekt ReflectionMethod oraz tablicę składowych skojarzonych z obiektem Module. Metoda handleMethod() weryfikuje dostępność i ostatecznie wywołuje odpowiednie metody akcesory obiektu Module. class ModuleRunner { // … function handleMethod(Module $module, ReflectionMethod $method, $params) { $name = $method->getName(); $args = $method->getParameters(); if (count($args) != 1 || substr($name, 0, 3) != "set") { return false; } $property = strtolower(substr($name, 3)); if (!isset($params[$property])) { return false; }

}

}

$arg_class = $args[0]->getClass(); if (empty($arg_class)) { $method->invoke($module, $params[$property]); } else { $method->invoke($module, $arg_class->newInstance($params[$property])); }

Metoda handleMethod()sprawdza najpierw, czy wytypowana do wywołania metoda jest aby odpowiednim akcesorem ustawiającym. Akcesory takie rozpoznawane są tu na podstawie wzorca nazwy, która musi rozpoczynać się od ciągu set i zawierać nazwę składowej; poza tym musi deklarować dokładnie jeden argument. Jeśli argument się zgadza, kod wyodrębnia z nazwy metody nazwę składowej, usuwając z nazwy przedrostek set i konwertując resztę na ciąg zawierający wyłącznie małe litery. Wynik konwersji jest następnie wykorzystywany w analizie tablicy argumentów $params. Tablica ta zawiera przekazane przez użytkownika składowe skojarzone z obiektem Module. Jeśli tablica $params nie zawiera szukanej składowej, kod zwraca false.

120

ROZDZIAŁ 5.  NARZĘDZIA OBIEKTOWE

Jeśli wyodrębniona z nazwy akcesora nazwa składowej pasuje do elementu tablicy $params, możemy pójść dalej i wywołać właściwy akcesor. Wcześniej jednak należy sprawdzić typ pierwszego (i jedynego) argumentu wywołania akcesora ustawiającego. Informację tę zwraca metoda ReflectionParameter::getClass(). Jeśli wywołanie zwróci wartość pustą, akcesor oczekuje przekazania wartości elementarnej — w przeciwnym razie wymaga przekazania obiektu. Wywołanie metody akcesora wymaga pośrednictwa nieomawianej jeszcze metody klasy ReflectionMethod::invoke(), wymagającej przekazania obiektu i dowolnej liczby argumentów, które przekazywane są dalej do metody docelowej. Kiedy przekazany obiekt nie pasuje do metody, wywołanie ReflectionMethod::invoke() zgłasza wyjątek. Metoda invoke() wywoływana jest na dwa sposoby: jeśli akcesor nie wymaga przekazania obiektu konkretnego typu, wywołanie invoke() jest inicjowane z podsuniętym przez użytkownika ciągiem znaków. Jeśli metoda wymaga argumentu obiektowego, ów ciąg jest wykorzystywany do konkretyzacji obiektu odpowiedniego typu, który jest następnie przekazywany do invoke(). Przykład ten bazuje na założeniu, że wymagany obiekt da się konkretyzować wywołaniem konstruktora z pojedynczym argumentem w postaci ciągu znaków. Najlepiej oczywiście byłoby jednak sprawdzić wymagania konstruktora jeszcze przed wywołaniem ReflectionClass::newInstance(). W miarę postępu wykonywania metody ModuleRunner::init() obiekt klasy ModuleRunner wypełnia się obiektami Module zawierającymi stosowne dane. Klasa mogłaby zostać teraz uzupełniona o metodę przeglądającą owe obiekty i inicjującą na ich rzecz wywołanie metody execute().

Podsumowanie W rozdziale zajmowaliśmy się narzędziami i technikami pomocnymi w zarządzaniu bibliotekami i klasami. Czytelnik mógł poznać nowy w PHP mechanizm przestrzeni nazw. Wyjaśniono, jak organizować kod, uciekając się do odpowiednich ustawień ścieżek wyszukiwania, odpowiedniej (tu zaczerpniętej z repozytorium PEAR) konwencji nazewniczej i cech systemu plików. Przyjrzeliśmy się ponadto funkcjom dynamicznej analizy klas i obiektów, a następnie realizującym podobne zadania elementom interfejsu retrospekcji Reflection API. Na koniec zaś na bazie klas hierarchii Reflection skonstruowaliśmy prosty przykład ilustrujący potencjał tkwiący w interfejsie dynamicznej analizy klas.

121

PHP. OBIEKTY, WZORCE, NARZĘDZIA

122

ROZDZIAŁ 6 

Obiekty a projektowanie obiektowe Znamy już dość szczegółowo mechanizmy obsługi obiektów w języku PHP, wypadałoby więc zostawić na boku szczegóły i zastanowić się nad najlepszymi możliwymi zastosowaniami poznanych narzędzi. W niniejszym rozdziale wprowadzę Cię w kwestie oddalone nieco od obiektów, a bliższe projektowaniu. Przyjrzymy się między innymi UML, czyli efektywnemu graficznemu językowi opisu systemów obiektowych. Rozdział będzie traktował o:  Podstawach projektowania — co rozumieć pod pojęciem projektowania i w czym projektowanie obiektowe różni się od proceduralnego.  Zasięgu klas — jak decydować o zawartości i odpowiedzialności klas.  Hermetyzacji — czyli ukrywaniu implementacji i danych za interfejsami.  Polimorfizmie — czyli stosowaniu wspólnych typów bazowych dla uzyskania transparentnej podmiany specjalizowanych typów pochodnych. 

Języku UML — zastosowaniach diagramów w opisach architektur obiektowych.

Czym jest projektowanie? Jedno ze znaczeń pojęcia „projektowanie kodu” to definiowanie systemu — określanie dla systemu wymagań i zakresu jego zadań. Co system powinien robić? Czego potrzebuje do realizacji swoich zadań? Jakie dane system generuje? Czy spełniają one wyrażone uprzednio wymagania? Na niższym poziomie projektowanie oznacza proces definiowania uczestników systemu i rozpoznawania zachodzących pomiędzy nimi relacji. W tym rozdziale zajmiemy się właśnie projektowaniem w tym drugim ujęciu, a więc klasami i obiektami oraz ich powiązaniami. Jak rozpoznać elementy systemu? System obiektowy składa się z klas. Należy zdecydować o naturze poszczególnych klas uczestniczących w systemie. Klasy składają się po części z metod, więc definiując klasy, trzeba zdecydować o grupowaniu metod. Klasy często uczestniczą też w relacjach dziedziczenia, mających im zapewnić spełnianie wymogów wspólnych dla poszczególnych części systemu interfejsów. Pierwsze wyzwanie w projektowaniu systemu tkwi właśnie w rozpoznaniu i wytypowaniu tych interfejsów. Klasy mogą jednak wchodzić również w inne relacje. Można bowiem tworzyć klasy składające się z innych klas i typów albo utrzymujące listy egzemplarzy innych typów. Klasy mogą korzystać z obiektów zewnętrznych. Klasy dysponują więc wbudowanym potencjałem do realizowania relacji kompozycji i użycia (na przykład za pośrednictwem narzucania typów obiektowych w sygnaturach metod), ale właściwe relacje zawiązują się dopiero w czasie wykonania, co zwiększa elastyczność projektu. W rozdziale zaprezentowane zostaną sposoby modelowania tego rodzaju zależności; będą one podstawą do omówienia zawartego w kolejnej części książki.

PHP. OBIEKTY, WZORCE, NARZĘDZIA

W ramach procesu projektowego należy również decydować, kiedy dana operacja należy do danego typu, a kiedy powinna należeć do innego typu, wykorzystywanego przez dany. Każdy etap projektowania oznacza nowe wybory i decyzje; jedne z nich prowadzą do elegancji i przejrzystości, inne mogą się na decydencie zemścić. Rozdział ten będzie w części poświęcony pewnym kwestiom, których rozpoznanie jest pomocne w podejmowaniu właściwych decyzji.

Programowanie obiektowe i proceduralne Czym różni się kod obiektowy od tradycyjnego kodu proceduralnego? Najłatwiej powiedzieć, że główna różnica tkwi w obecności obiektów. Nie jest to jednak stwierdzenie ani odkrywcze, ani prawdziwe. Przecież w języku PHP obiekty mogą być z powodzeniem wykorzystywane w kodzie proceduralnym. Na porządku dziennym jest również definiowanie klas opartych na kodzie proceduralnym. Obecność klas i obiektów nie jest więc równoznaczna z obiektowością — nawet w językach takich jak Java, gdzie większość elementów programu to obiekty. Jedną z kluczowych różnic pomiędzy kodem obiektowym a proceduralnym odnajdujemy w podziale odpowiedzialności. Kod proceduralny przyjmuje postać sekwencji poleceń i wywołań metod. Do obsługi różnych stanów programu wydziela się kod kontrolujący. Taki model odpowiedzialności prowokuje powielanie kodu i uściślanie zależności pomiędzy elementami projektu. W kodzie obiektowym mamy zaś do czynienia z próbą minimalizacji owych zależności przez przekładanie odpowiedzialności za różne zadania na obiekty rezydujące w systemie. W tym rozdziale przedstawię przykładowy, uproszczony problem i przeanalizuję jego proceduralne i obiektowe rozwiązania. Załóżmy, że zadanie polega na skonstruowaniu narzędzia odczytu i zapisu plików konfiguracyjnych. Ponieważ najbardziej interesuje nas ogólna struktura kodu, nie będziemy zagłębiać się w żadnym z przypadków w szczegóły implementacyjne. Zacznijmy od podejścia proceduralnego. Odczytywać i zapisywać będziemy dane tekstowe w formacie: klucz:wartość

Wystarczą nam do tego dwie funkcje: function readParams($sourceFile) { $params = array(); // wczytaj parametry z pliku $sourceFile… return $params; } function writeParams($params, $destFile) { // zapisz parametry do pliku $destFile… }

Funkcja readParams wymaga przekazania jako argumentu wywołania nazwy pliku źródłowego. W jej ciele następuje próba otwarcia pliku, a potem odczyt kolejnych wierszy tekstu. Na podstawie wyszukiwanych w poszczególnych wierszach par kluczy i wartości konstruowana jest asocjacyjna tablica parametrów zwracana następnie do wywołującego. Funkcja writeParams() przyjmuje z kolei na wejście tablicę asocjacyjną i ścieżkę dostępu do pliku docelowego. W implementowanej w ciele funkcji pętli przegląda tablicę, zapisując wyodrębniane z niej pary klucz i wartość w pliku docelowym. Oto kod używający obu funkcji: $file = "./param.txt"; $array['klucz1'] = "wartość1"; $array['klucz2'] = "wartość2"; $array['klucz3'] = "wartość3"; writeParams($array, $file); // zapis tablicy parametrów do pliku $output = readParams($file); // odczyt tablicy parametrów z pliku print_r($output);

124

ROZDZIAŁ 6.  OBIEKTY A PROJEKTOWANIE OBIEKTOWE

Kod jest, jak widać, stosunkowo zwięzły i nie powinien sprawiać problemów konserwatorskich. Do utworzenia i zapisania pliku param.txt użyjemy wywołania writeParams(), którego zadaniem jest utrwalenie par klucz – wartość:

klucz wartość

klucz1:wartość1 klucz2:wartość2 klucz3:wartość3

Rozpoznanie formatu zapisu pliku powinno się odbywać na podstawie rozszerzenia pliku — dla plików z rozszerzeniem .xml należałoby wszcząć procedurę odczytu w formacie XML. Choć i tym razem poradzimy sobie z zadaniem, zagraża nam komplikacja kodu i zwiększenie uciążliwości utrzymania (konserwacji). Mamy teraz dwie możliwości. Albo będziemy sprawdzać rozszerzenie pliku parametrów w kodzie zewnętrznym, albo wewnątrz funkcji odczytujących i zapisujących. Spróbujmy oprogramować drugą z opcji: function readParams($source) { $params = array(); if (preg_match( "/\.xml$/i", $source)) { // odczyt parametrów z pliku XML } else { // odczyt parametrów z pliku tekstowego } return $params; } function writeParams($params, $source) { if (preg_match("/\.xml$/i", $source)) { // zapis parametrów do pliku XML } else { // zapis parametrów do pliku tekstowego } }

 Uwaga Kod przykładowy to zawsze sztuka kompromisu. Musi być dostatecznie czytelny, aby ilustrował konkretną koncepcję bądź problem, co często oznacza konieczność rezygnacji z kontroli błędów i elastyczności. Innymi słowy, prezentowany tu przykład jest jedynie ilustracją kwestii projektowania i powielania kodu, w żadnym razie nie będąc wzorcową implementacją parsowania i zapisywania danych w plikach. Z tego względu wszędzie tam, gdzie nie jest to konieczne do omówienia, implementacja fragmentów kodu została zwyczajnie pominięta.

Jak widać, w każdej z funkcji musieliśmy uwzględnić test rozszerzenia pliku parametrów. Tego rodzaju zwielokrotnienie kodu może być w przyszłości przyczyną problemów. Gdybyśmy bowiem stanęli w obliczu zadania obsługi kolejnego formatu pliku parametrów, musielibyśmy pamiętać o synchronizacji kodu sprawdzającego rozszerzenie w obu funkcjach. Spróbujmy to samo zadanie zrealizować za pomocą prostych klas. Na początek zdefiniujemy abstrakcyjną klasę bazową wyznaczającą interfejs typu: abstract class ParamHandler { protected $source; protected $params = array();

125

PHP. OBIEKTY, WZORCE, NARZĘDZIA

function __construct($source) { $this->source = $source; } function addParam($key, $val) { $this->params[$key] = $val; } function getAllParams() { return $this->params; } static function getInstance($filename) { if ( preg_match("/\.xml$/i", $filename)) return new XmlParamHandler($filename); } return new TextParamHandler($filename); } abstract function write(); abstract function read(); }

W klasie tej definiujemy metodę addParam() służącą do uzupełniania tablicy parametrów i metodę getAllParams() dającą użytkownikom dostęp do kopii tablicy parametrów. Tworzymy też statyczną metodę getInstance(), której zadaniem jest analiza rozszerzenia nazwy pliku parametrów i zwrócenie użytkownikowi klasy specjalizowanej do obsługi pliku odpowiedniego formatu. Wreszcie definiujemy dwie abstrakcyjne metody: write() i read(), wymuszając ich implementację w klasach pochodnych i tym samym narzucając im wspólny interfejs obsługi plików.  Uwaga Użycie metody statycznej do generowania obiektów klas pochodnych w klasie nadrzędnej jest bardzo wygodne. Taka decyzja projektowa ma jednak również wady. Typ ParamHandler jest teraz zasadniczo w swoich głównych instrukcjach warunkowych ograniczony do pracy z konkretnymi klasami. A jeśli zechcemy obsłużyć inny format danych? Oczywiście właściciel klasy ParamHandler może zawsze uzupełnić metodę getInstance(). Ale już programista kodu klienckiego nie ma łatwej możliwości zmieniania klasy bibliotecznej (sama zmiana nie jest może specjalnie trudna, ale pojawia się problem ponownego aplikowania zmian w kolejnych wersjach bibliotek). Zagadnienia tworzenia obiektów omówię bardziej szczegółowo w rozdziale 9.

Zdefiniujmy teraz owe klasy specjalizowane (znów gwoli przejrzystości przykładu pomijając szczegóły implementacyjne): class XmlParamHandler extends ParamHandler { function write() { // zapis tablicy parametrów $this->params w pliku XML } function read() { // odczyt pliku XML i wypełnienie tablicy parametrów $this->params } } class TextParamHandler extends ParamHandler { function write() { // zapis tablicy parametrów $this->params w pliku tekstowym } function read() { // odczyt pliku tekstowego i wypełnienie tablicy parametrów $this->params } }

126

ROZDZIAŁ 6.  OBIEKTY A PROJEKTOWANIE OBIEKTOWE

Obie klasy ograniczają się do implementowania metod write() i read(). Każda z klas zapisuje i odczytuje parametry w odpowiednim dla siebie formacie. Użytkownik takiego zestawu klas będzie mógł zapisywać i odczytywać pliki parametrów niezależnie od ich formatu, całkowicie ignorując (i nie mając nawet tego świadomości) znaczenie rozszerzenia nazwy pliku: $test = ParamHandler::getInstance("./params.xml"); $test->addParam("klucz1", "wartość1"); $test->addParam("klucz2", "wartość2"); $test->addParam("klucz3", "wartość3"); $test->write(); // zapis w formacie XML

Równie łatwo można odczytywać parametry z pliku niezależnie od jego formatu: $test = ParamHandler::getInstance("./params.txt"); $test->read(); // odczyt z pliku tekstowego

Spróbujmy podsumować naukę płynącą z ilustracji tych dwóch metod projektowych.

Odpowiedzialność Odpowiedzialność za decyzję co do formatu pliku w podejściu proceduralnym bierze na siebie użytkownik (kod kontrolujący) i czyni to nie raz, a dwa razy. Co prawda kod sprawdzający rozszerzenie został przeniesiony do wnętrza funkcji, ale nie przesłania to faktycznego przepływu sterowania. Wywołanie funkcji readParams() musi zawsze występować w kontekście innym od kontekstu wywołania writeParams(), więc test rozszerzenia pliku musi być powtarzany w każdej z tych funkcji, niezależnie od historii ich wywołań. W wersji obiektowej wybór formatu pliku dokonywany jest w ramach statycznej metody getInstance(), więc test rozszerzenia jest wykonywany tylko jednokrotnie, a jego wynik wpływa na wybór i konkretyzację odpowiedniej klasy pochodnej. Użytkownik nie bierze odpowiedzialności za implementację — korzysta po prostu z otrzymanego obiektu, nie wnikając w szczegóły implementacji klasy tego obiektu. Wie jedynie tyle, że korzysta z obiektu typu ParamHandler i że obiekt ten obsługuje operacje write() i read(). Kod proceduralny musiał wciąż zajmować się szczegółami swojej implementacji, a w kodzie obiektowym mamy przeniesienie zainteresowania z implementacji na interfejs. Przeniesienie odpowiedzialności za implementację z kodu użytkownika na kod klasy powoduje, że użytkownik nie cierpi w żaden sposób wskutek zmian decyzji i włączania do hierarchii klas obsługujących nowe formaty plików parametrów — dla niego te rozszerzenia są transparentne.

Spójność Spójność to bliski stopień powiązania zależnych od siebie procedur. W idealnym przypadku mamy do czynienia z komponentami w jasny sposób dzielącymi odpowiedzialność. Jeśli kod rozprasza powiązane ze sobą procedury, jego konserwacja staje się utrudniona, ponieważ wprowadzanie zmian wiąże się z identyfikacją i wyszukiwaniem rozległych zależności. Nasza klasa ParamHandler zbiera wszystkie procedury związane z obsługą plików parametrów we wspólnym kontekście. Metody operujące na plikach XML dzielą kontekst, w ramach którego mogą dzielić również dane i w ramach którego zmiany jednej metody (np. zmiana nazw elementów formatu XML) mogą być w razie konieczności łatwo odzwierciedlone w pozostałych metodach. Klasy hierarchii ParamHandler cechują się więc wysoką spójnością. Z kolei podejście proceduralne rozdziela powiązane procedury. Kod obsługi XML jest rozproszony pomiędzy słabo powiązanymi funkcjami.

Sprzęganie O ścisłym sprzęganiu mówimy wtedy, kiedy oddzielne części kodu systemu są ze sobą związane tak, że zmiany w jednej z nich wymuszają zmiany w pozostałych. Tego rodzaju sprzęganie jest charakterystyczne dla kodu proceduralnego z racji jego sekwencyjnej natury. 127

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Sprzęganie to widać też dobrze w naszym przykładzie z podejściem proceduralnym. Funkcje writeParams() i readParams() wykonują na nazwie pliku ten sam test mający na celu wykrycie formatu pliku parametrów i sposobu jego obsługi. Wszelkie zmiany w logice, jakie wprowadzilibyśmy w jednej z funkcji, musielibyśmy zaimplementować również w drugiej. Gdybyśmy, na przykład, zamierzali rozszerzyć obsługę plików parametrów o nowy format pliku, musielibyśmy zsynchronizować zmiany w obu funkcjach, tak aby obie w ten sam sposób realizowały test rozszerzenia pliku. Konieczność ta staje się jeszcze bardziej uciążliwa w miarę wzrostu liczby funkcji związanych z obsługą parametrów. W przykładzie podejścia obiektowego rozdzieliliśmy od siebie poszczególne klasy pochodne, izolując je również od kodu użytkującego. W obliczu potrzeby uzupełnienia obsługi plików parametrów o nowy format pliku dodalibyśmy po prostu do hierarchii nową klasę pochodną, a jedyną tego reperkusją byłaby konieczność zmiany logiki testu w pojedynczej metodzie klasy bazowej — getInstance().

Ortogonalność Ortogonalność będziemy tu rozumieć (za Andrew Huntem i Davidem Thomasem i ich publikacją The Pragmatic Programmer1, Addison-Wesley Professional, 1999) jako połączenie ściśle zdefiniowanej odpowiedzialności komponentów współzależnych z ich niezależnością od szerzej widzianego systemu. Ortogonalność promuje możliwość ponownego wykorzystywania komponentów przez łatwość ich włączania do nowych systemów bez konieczności specjalnego przystosowywania ich w tym celu. Takie komponenty mają ściśle i w sposób niezależny od szerszego kontekstu zdefiniowane zbiory danych wejściowych i wyjściowych. Kod ortogonalny ułatwia wprowadzanie zmian, bo ogranicza oddźwięk zmian wprowadzanych w implementacji komponentów. Wreszcie kod ortogonalny jest bezpieczniejszy, ponieważ tak samo jak zakres oddźwięku zmian ograniczony jest zakres oddźwięku ewentualnych błędów. Dla porównania błąd w kodzie cechującym się wysoką współzależnością komponentów może obejmować swoimi negatywnymi efektami znaczne obszary systemu. Nie istnieje coś takiego jak automatyzm wiążący osłabienie sprzęgania i wysoką spójność z zastosowaniem klasy. W końcu równie dobrze moglibyśmy w analizowanym przykładzie ująć całość proceduralnego kodu w pewnej klasie i nie zyskać na takim pseudoobiektowym podejściu żadnej z typowych dla niego zalet. W jaki więc sposób osiągnąć pożądaną równowagę w kodzie? Osobiście starania rozpoczynam od analizy klas, które miałyby uczestniczyć w systemie.

Zasięg klas Wyznaczanie granic odpowiedzialności i zakresu funkcji poszczególnych klas systemu okazuje się zaskakująco trudnym zadaniem, zwłaszcza w obliczu rozwoju systemu. Zadanie to wydaje się proste, kiedy system ma modelować świat materialny. Obiektowe systemy często wykorzystują programowe reprezentacje obiektów świata materialnego — w postaci klas Person (osoba), Invoice (faktura) czy Shop (sklep). Sugeruje to, że wyznaczanie zasięgu klas sprowadza się do rozpoznania i wytypowania w systemie jego „elementów” oraz wyposażenia ich w możliwość wzajemnego oddziaływania za pośrednictwem metod. Nie jest to spostrzeżenie zupełnie nieprawdziwe i stanowi znakomity punkt wyjścia w projektowaniu systemu, nie wolno jednak przyjmować go bezkrytycznie. Jeśli bowiem klasy postrzegać jako rzeczowniki, podmioty dowolnej liczby czynności (czasowników), może się okazać, że w miarę rozwoju projektu i zmian wymagań natłok „czasowników” i zależności pomiędzy „rzeczownikami” jest nie do opanowania. Wróćmy do pielęgnowanego w rozdziale 3. przykładu hierarchii ShopProduct. Nasz system ma prezentować klientom ofertę produktów, więc wydzielenie w nim klasy ShopProduct jest oczywiste, ale czy to jedyna decyzja, jaką należy podjąć? Klasę uzupełniliśmy o metody getTitle() i getPrice() udostępniające dane produktów. Poproszeni o mechanizm prezentacji zestawienia informacji o produkcie na potrzeby fakturowania i wysyłki moglibyśmy zdefiniować metodę write(). Gdyby okazało się, że zestawienia mają mieć różne formaty, moglibyśmy poza metodą write() wyposażyć naszą klasę również w metody writeXML() i writeXHTML() — albo uzupełnić write() o kod rozpoznający żądany format na podstawie dodatkowego znacznika i dostosowujący sposób prezentacji zestawienia. 1

Wydanie polskie: Pragmatyczny programista. Od czeladnika do mistrza, Helion, 2011 — przyp. tłum.

128

ROZDZIAŁ 6.  OBIEKTY A PROJEKTOWANIE OBIEKTOWE

Problem w tym, że w ten sposób obarczymy klasę ShopProduct nadmierną liczbą obowiązków — nagle okaże się, że klasa przeznaczona do przechowywania informacji o asortymencie będzie również odpowiadać za sposoby prezentacji tych informacji klientowi. Jak więc powinniśmy podchodzić do zadania definiowania klas? Najlepiej jest traktować klasy jako jednostki o ściśle ograniczonej odpowiedzialności, ograniczonej najlepiej do pojedynczego i dobrze zdefiniowanego zadania. Kiedy ograniczenie będzie odpowiednie? Kiedy uda się je zgrabnie wyrazić słowami. Przeznaczenie klasy powinno dać się opisać maksymalnie dwudziestoma pięcioma słowami, z rzadka przetykanymi spójnikami „i” czy „lub”. Jeśli opis się wydłuża albo zawiera zbyt wiele zdań podrzędnych, należałoby rozważyć wydzielenie dla części zadań odrębnych klas. Klasy hierarchii ShopProduct są więc odpowiedzialne za przechowywanie i zarządzanie danymi produktów. Jeśli uzupełnimy je o metody prezentujące zestawieniowe informacje o produkcie w różnych formatach, obciążymy hierarchię nowym zadaniem — odpowiedzialnością za prezentowanie informacji klientom. W rozdziale 3. uniknęliśmy przeładowania klasy, wydzielając do zadania prezentacji osobny typ. Typ ShopProduct pozostał odpowiedzialny jedynie za zarządzanie danymi produktów, zaś do zadania wyświetlania informacji o nich powołaliśmy klasę ShopProductWriter. Odpowiedzialność jest dalej zawężana w klasach pochodnych obu hierarchii.  Uwaga Niewiele reguł projektowych cechuje się stanowczością. Niekiedy widuje się więc w klasie kod zapisujący dane obiektowe w innej, zupełnie niepowiązanej z nią klasie. Zdaje się to naruszać regułę zawężania odpowiedzialności, ale niekiedy najłatwiej o taką implementację, ponieważ metoda zapisująca musi dysponować pełnym dostępem do składowych egzemplarza. Stosowanie lokalnych względem klasy metod do utrwalania danych pozwala też na uniknięcie definiowania równoległych hierarchii klas utrwalających odzwierciedlających hierarchię klas danych — takie zrównoleglenie oznaczałoby przecież tak niepożądane powiązanie elementów systemu. Innym strategiom utrwalania danych obiektów przyjrzymy się w rozdziale 12. Na razie chciałbym po prostu przestrzec przed fanatycznym trzymaniem się reguł projektowych — żaden zbiór wytycznych nie zastąpi analizy konkretnego problemu. Warto więc wymowę reguły projektowej konfrontować z wymową własnych wniosków co do jej zasadności w danym miejscu projektu.

Polimorfizm Polimorfizm, czyli przełączanie klas, to wspólna cecha systemów obiektowych. Zdążyliśmy już jej zresztą doświadczyć. Polimorfizm polega na utrzymywaniu wielu implementacji wspólnego interfejsu. Brzmi to może zawile, ale w istocie polimorfizm stosowaliśmy już z powodzeniem w przykładach. Potrzeba polimorfizmu jest często sygnalizowana w kodzie nadmierną liczbą instrukcji warunkowych. Tworząc w rozdziale 3. pierwsze wersje klasy ShopProduct, eksperymentowaliśmy z pojedynczą klasą, próbując pomieścić w niej funkcje pozwalające na zarządzanie nie tylko produktami pojmowanymi ogólnie, ale również całkiem konkretnym asortymentem — płytami CD i książkami. Doprowadziło to do naszpikowania kodu generującego zestawienie informacji o produkcie instrukcjami warunkowymi: function getSummaryLine() { $base = "{$this->title} ({$this->producerMainName}, "; $base .= "{$this->producerFirstName})"; if ($this->type === 'książka') { $base .= ": liczba stron - {$this->numPages}"; } else if ($this->type === 'cd') { $base .= ": czas nagrania - {$this->playLength}"; } return $base; }

Instrukcje warunkowe sugerują możliwość wydzielenia dwóch klas pochodnych: CdProduct i BookProduct.

129

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Podobnie w analizowanym niedawno proceduralnym kodzie obsługi plików parametrów obecność instrukcji warunkowych stanowiła pierwszy sygnał struktury obiektowej, do której ostatecznie doszliśmy. Mieliśmy tam powtórzenie tych samych testów w dwóch miejscach kodu: function readParams($source) { $params = array(); if (preg_match("/\.xml$/i", $source)) { // odczyt parametrów z pliku XML } else { // odczyt parametrów z pliku tekstowego } return $params; } function writeParams($params, $source) { if (preg_match("/\.xml$/i", $source)) { // zapis parametrów do pliku XML } else { // zapis parametrów do pliku tekstowego } }

Każda z instrukcji warunkowych sugerowała potrzebę zdefiniowania klas XmlParamHandler i TextParamHandler rozszerzających (czy raczej specjalizujących) klasę bazową ParamHandler i definiujących jej abstrakcyjne metody read() i write(): // Może zwrócić obiekt klasy XmlParamHandler bądź TextParamHandler: $test = ParamHandler::getInstance($file); $test->read(); // XmlParamHandler::read() albo TextParamHandler::read()… $test->addParam("klucz1", "wartość1"); $test->write(); // XmlParamHandler::write() albo TextParamHandler::write()…

Należy zauważyć, że polimorfizm nie delegalizuje instrukcji warunkowych. Wykorzystywane są one choćby w ramach metody ParamHandler::getInstance() celem wyboru odpowiedniej klasy obiektu. Chodzi o to, aby instrukcje decyzyjne były skupione w jednym miejscu kodu i nie musiały być powtarzane w różnych jego fragmentach. Wiemy, że PHP wymusza definiowanie interfejsów wyznaczanych abstrakcyjnymi klasami bazowymi. To korzystne, bo mamy dzięki temu pewność, że wszystkie konkretne (nie abstrakcyjne) klasy pochodne będą obsługiwały metody o dokładnie takich sygnaturach, jak w abstrakcyjnej klasie nadrzędnej. Dotyczy to również sygnalizacji (wymuszania) typów obiektowych oraz ochrony dostępu. W kodzie klienckim można więc wymiennie stosować wszystkie pochodne wspólnej klasy nadrzędnej (dopóty, dopóki kod kliencki odwołuje się wyłącznie do funkcjonalności zdefiniowanej w klasie bazowej). Od tej reguły jest jeden istotny wyjątek: nie ma możliwości ograniczenia i wymuszenia typu zwracanego przez metodę klasy, niezależnie od definicji metody w klasie nadrzędnej.  Uwaga W czasie przygotowywania tej książki mówiło się o włączeniu wymuszania typów zwracanych do przyszłych wydań PHP, ale nie było w tym względzie ostatecznego postanowienia.

Niemożność określenia typów zwracanych oznacza, że wymienność typów pochodnych może ulec zaburzeniu przez zmienność typów wartości zwracanych z metod, w zależności od implementacji klasy. Warto więc narzucić sobie samemu dyscyplinę polegającą na ujednolicaniu typów wartości zwracanych. Niektóre z metod mogą zresztą być tak definiowane, aby słabą kontrolę typów, charakterystyczną dla PHP, wykorzystać do zwracania różnych typów wartości w zależności od okoliczności wywołania. Reszta metod zawiera z użytkownikami hierarchii swego rodzaju kontrakt, obietnicę co do typu zwracanego. Jeśli kontrakt ten zostanie zawarty

130

ROZDZIAŁ 6.  OBIEKTY A PROJEKTOWANIE OBIEKTOWE

w abstrakcyjnej klasie bazowej, powinien być respektowany w implementacjach wszystkich jej konkretnych pochodnych, aby klienci byli pewni spójnego działania wywołań rozprowadzanych w hierarchii. Jeśli zgodzimy się na zwracanie obiektu pewnego typu, można będzie oczywiście zwrócić specjalizację tego typu w postaci obiektu jego klasy pochodnej. Choć więc interpreter nie może wymusić ujednolicenia typów wartości zwracanych z metod, nie powinno to być usprawiedliwieniem niekonsekwencji programisty. Typ wartości zwracanych z metod należałoby też określać w komentarzach dokumentujących kod.

Hermetyzacja Hermetyzacja (ang. encapsulation) oznacza proste ukrywanie danych i funkcji przed użytkownikiem. To kolejne z kluczowych pojęć podejścia obiektowego. Na najprostszym poziomie hermetyzacja danych polega na deklarowaniu składowych klas jako prywatnych bądź zabezpieczonych. Ukrywając składowe przed użytkownikami obiektów klas, wymuszamy na nich stosowanie pewnego interfejsu odwołań, zapobiegając tym samym przypadkowym naruszeniom spójności danych obiektów. Inną formą hermetyzacji jest polimorfizm. Skrywając za wspólnym interfejsem rozmaite jego implementacje, ukrywamy strategie implementacji przed użytkownikami tego interfejsu. Dzięki temu wszelkie zmiany wprowadzane za osłoną interfejsu są dla jego użytkowników transparentne. Oznacza to możliwość dodawania i uzupełniania implementacji interfejsu bez wymuszania zmian w jego stosowaniu po stronie użytkownika. Użytkownik posługuje się wyłącznie interfejsem i nie interesują go skrywające się za nim mechanizmy. Im większa zaś niezależność tych mechanizmów, tym mniejsze ryzyko, że wprowadzane w nich zmiany czy poprawki odbiją się na pozostałych częściach projektu. Hermetyzacja jest w pewnym sensie kluczem do programowania obiektowego. Naszym celem powinno być maksymalne uniezależnienie poszczególnych elementów systemu. Klasy i metody powinny otrzymywać tylko tyle informacji, ile im potrzeba do wykonywania ich — ściśle ograniczonych i odpowiednio zawężonych — zadań. Wprowadzenie do języka PHP słów kluczowych private, protected i public znakomicie ułatwia hermetyzację. Hermetyzacja jest jednak również swego rodzaju stanem umysłu projektanta. W PHP4 nie mieliśmy do dyspozycji żadnych formalnych środków ukrywania danych. Prywatność była sygnalizowana jedynie w dokumentacji i konwencji nazewniczej — symptomem zamierzonej prywatności składowej było na przykład rozpoczynanie jej nazwy od znaku podkreślenia: var $_niedotykac;

Wymuszało to staranną kontrolę kodu, gdyż respektowanie tak sygnalizowanej prywatności nie było nijak egzekwowane przez interpreter języka. Co ciekawe, błędy były stosunkowo rzadkie, ponieważ już sama struktura i styl kodu jasno wskazywały na to, które ze składowych powinny być omijane w kodzie użytkującym klasy. Również w PHP5 możemy złamać tę regułę i odkryć dokładny podtyp obiektu wykorzystywanego w kontekście przełączania klas — wystarczy użyć operatora instanceof. function workWithProducts(ShopProduct $prod) { if ($prod instanceof CdProduct) { // operacje właściwe dla obiektów CdProduct… } else if ($prod instanceof BookProduct) { // operacje właściwe dla obiektów BookProduct… } }

Być może istnieje niekiedy ważny powód do takiego postępowania, ale zazwyczaj nie jest ono dobrze widziane. Zapytując powyżej o konkretny podtyp, tworzymy zależność pomiędzy kodem implementacji interfejsu a kodem ów interfejs wykorzystującym. Jest to o tyle niebezpieczne, że hermetyzacja podtypów implementujących interfejs ma na celu ich separację od użytkownika między innymi po to, aby dać twórcom implementacji owych podtypów swobodę zmian i poprawek — tutaj konkretnie chodziło zaś o możliwość modyfikowania hierarchii ShopProduct bez propagowania zmian do kodu użytkującego tę hierarchię. Powyższy kod eliminuje tę możliwość. Gdybyśmy bowiem z jakichś powodów zdecydowali o zmianie implementacji klas CdProduct i BookProduct, moglibyśmy zaburzyć zamierzone działanie funkcji workWithProducts().

131

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Z powyższego przykładu wyciągamy dwa wnioski. Po pierwsze, hermetyzacja pomaga w tworzeniu ortogonalnego kodu. Po drugie zaś, stopień, do jakiego hermetyzacja daje się wymusić, a od jakiego może być utrzymana jedynie przy odpowiedniej dyscyplinie projektowej, jest zupełnie bez znaczenia. Hermetyzacja jest bowiem techniką, która powinna znaleźć poszanowanie tak twórców klas, jak i ich użytkowników.

Nieważne jak Jeśli Czytelnik myśli podobnie jak ja, to wzmianka o problemie wywołuje u niego intelektualny wyścig w poszukiwaniu mechanizmów dających rozwiązania. Zaczyna się wybór funkcji przydatnych w implementacji rozwiązania, przypominanie sobie co sprytniejszych wyrażeń regularnych i poszukiwanie w repozytorium PEAR tudzież powroty do kodu napisanego wcześniej, a nadającego się do wykorzystania w rozwiązaniu postawionego zadania. Jednak wszystko to należy na etapie projektowania odłożyć na bok. Trzeba oczyścić umysł z mechanizmów i procedur. Umysł powinny zaprzątać jedynie elementy uczestniczące w docelowym systemie: potrzebne w nim typy i ich interfejsy. Oczywiście wiedza odłożona na bok nie jest zupełnie ignorowana. Wiemy dzięki niej, że klasa otwierająca plik będzie potrzebować ścieżki dostępu, kod komunikujący się z bazą danych będzie musiał utrzymywać nazwy tabel oraz hasła i tak dalej. Główną rolę powinny jednak odgrywać struktury i zależności pomiędzy nimi. Łatwo się później przekonać, że implementacja elegancko wpasowuje się w wyznaczone interfejsy, a całość zyskuje elastyczność pozwalającą na łatwe wymienianie, ulepszanie i rozszerzanie implementacji bez zakłócania wzajemnych zależności komponentów systemu i zaburzania go jako całości. Gdy położy się nacisk na interfejs, należy myśleć kategoriami abstrakcyjnych klas bazowych, a nie ich konkretnych pochodnych. Przykład mamy w naszym kodzie odczytującym i zapisującym parametry — tutaj interfejs jest najważniejszym aspektem projektu. Potrzebujemy typu odczytującego i zapisującego pary klucz i wartość. I właśnie to jest podstawowym zadaniem owego typu, a nie faktycznie stosowany nośnik czy środki wykorzystywane w operacjach pozyskiwania i utrwalania danych. Projektujemy ten system na bazie abstrakcyjnej klasy ParamHandler, uzupełniając ją potem jedynie konkretnymi strategiami implementacji właściwych operacji odczytu i zapisu plików parametrów. W ten sposób uwzględniamy polimorfizm i hermetyzację od samego początku tworzenia systemu, zyskując ostatecznie możliwość przełączania klas implementacji. To powiedziawszy, należy przyznać, że od początku wiadomo było, że zaistnieją implementacje klasy ParamHandler dla XML-a i plików tekstowych, i że bez wątpienia wpłynęło to na kształt interfejsu. I dobrze, bo nie sposób definiować interfejsów w całkowitym oderwaniu od wiadomych sobie aspektów systemu — doprowadziłoby to najprawdopodobniej do zbytniej ich generalizacji. Sławna Banda Czworga (autorzy klasycznej pozycji Design Patterns2) podsumowała tę zasadę zdaniem: „Programuj pod kątem interfejsu, nie implementacji”. Warto je dodać do własnego zbioru złotych myśli.

Cztery drogowskazy Mało kto nie myli się wcale na etapie projektowania. Większość z nas akceptuje fakt konieczności wprowadzania w przyszłości poprawek, nieuniknionych w miarę zdobywania lepszego rozeznania w rozwiązywanym problemie. Jasny początkowo kurs poprawek łatwo zmienić w niekontrolowany dryf. Tu nowa metoda, tam dodatkowa klasa — i system chyli się ku upadkowi. Przekonaliśmy się już, że sugestie co do ulepszeń kodu widać często w nim samym. Owe tropy mogą wprost sugerować konkretne poprawki albo choćby skłaniać do weryfikacji projektu. W niniejszym podrozdziale spróbuję wyróżnić cztery oznaki mogące świadczyć o konieczności zmian projektowych.

2

Wydanie polskie: Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku, Helion, 2010 — przyp. tłum.

132

ROZDZIAŁ 6.  OBIEKTY A PROJEKTOWANIE OBIEKTOWE

Zwielokrotnianie kodu Zwielokrotnianie kodu jest jednym z cięższych grzechów programowania. Uczucie déjà vu przy programowaniu procedury może sygnalizować problem projektowy. Przyjrzyj się wtedy wystąpieniom powtórzonego kodu. Być może uda się je scalić. Zwielokrotnianie kodu oznacza zasadniczo ścisłe powiązanie elementów projektu. Czy zmiana czegoś w jednej procedurze wymaga powtórzenia zmian w podobnych procedurach? Jeśli tak, to może wszystkie je należałoby ująć we wspólnej klasie.

Przemądrzałe klasy Przekazywanie argumentów pomiędzy metodami może być uciążliwe. Dlaczego nie oszczędzić sobie kłopotu, wykorzystując zmienne globalne? Można wtedy zrezygnować z nużącego przekazywania… Zmienne globalne mają swoje zastosowania, ale nie należy do ich wykorzystywania pochodzić bezkrytycznie. Przeciwnie, każda zmienna globalna powinna być traktowana wyjątkowo podejrzliwie. Stosując zmienne globalne albo ujmując w klasie wiedzę wykraczającą poza dziedzinę odpowiedzialności tej klasy, kotwiczymy klasę w kontekście tej wiedzy i tym samym zmniejszamy jej uniwersalność — klasa jest uzależniona od kodu pozostającego poza jej kontrolą. A przecież chodzi nam o rozluźnianie, a nie zacieśnianie współzależności pomiędzy klasami a procedurami. Wiedzę używaną w klasie należałoby ograniczać do kontekstu tejże klasy — strategie umożliwiające osiągnięcie tego celu poznasz w dalszej części książki.

Złota rączka Czy nie każemy klasie wykonywać zbyt wielu zadań? Jeśli tak, spróbuj rozpisać listę tych zadań. Być może niektóre z nich dałoby się wyodrębnić do osobnej klasy. Obecność przeładowanych zadaniami klas utrudnia wyprowadzanie klas pochodnych. Które z zadań powinny być w ramach pochodnej specjalizowane? A jeśli potrzebna będzie pochodna specjalizująca więcej niż jedno zadanie? Skończy się albo na nadmiernej liczbie pochodnych, albo na dużej liczbie instrukcji warunkowych w hierarchii.

Za dużo warunków Stosowanie instrukcji if i switch w kodzie projektu to jeszcze nic złego. Niekiedy jednak obecność takich struktur warunkowych to niemy krzyk o polimorfizm. Jeśli zorientujesz się, że w ramach jednej klasy wciąż trzeba testować jakiś warunek, a zwłaszcza jeśli test ten trzeba powtarzać w wielu metodach klasy, najprawdopodobniej powinieneś rozdzielić klasę na dwie albo więcej klas. Sprawdź, czy struktura kodu warunkowego sugeruje rozróżnianie zadań i czy dałoby się nimi obarczyć osobne klasy. Owe klasy powinny implementować wspólną abstrakcyjną klasę bazową. Może też pojawić się wtedy kwestia przekazywania właściwej klasy do kodu użytkującego tak powstałą hierarchię. Można wtedy wykorzystać niektóre z wzorców projektowych z rozdziału 9., opisujących generowanie obiektów.

Język UML Jak dotąd projekt wyrażaliśmy jedynie kodem, ilustrując nim koncepcje dziedziczenia czy polimorfizmu. Miało to swoje zalety, ponieważ język PHP jest naszym — Czytelnika i moim — językiem wspólnym (musi tak być, skoro razem zabrnęliśmy aż tutaj). W miarę rozrastania się naszych przykładów pokazywanie kodu źródłowego przestanie wystarczać. Kilka wierszy kodu nie zawsze daje bowiem właściwy obraz koncepcji.

133

PHP. OBIEKTY, WZORCE, NARZĘDZIA

UML to skrót od Unified Modeling Language („ujednolicony język modelowania”). Według Martina Fowlera (autora książki UML Distilled, Addison-Wesley Professional, 1999), UML doczekał się rangi standardu dopiero po wieloletnich intelektualnych i biurokratycznych bataliach toczonych przez społeczność zwolenników projektowania obiektowego; stronami byli zwolennicy dobrego i zwolennicy lepszego. Z pobojowiska wyłoniła się niezwykle przydatna składnia graficznego opisu systemów obiektowych. W tym rozdziale zaledwie muśniemy zagadnienie, wkrótce jednak Czytelnik przekona się, że odrobina języka UML jest w tej książce jak najbardziej na miejscu. Przydatność UML-a przejawia się głównie w opisach struktur i wzorców, a to za sprawą diagramów klas. Uzyskiwana w tych diagramach przejrzystość intencji projektowych i podziału zadań rzadko daje się równie łatwo wyrazić w przykładowym kodzie.

Diagramy klas Choć diagramy klas to tylko jeden z wielu elementów języka UML, to właśnie im język zawdzięcza powszechność stosowania. Diagramy te są nieocenione w opisach relacji zachodzących w systemach obiektowych. I właśnie z nich najczęściej będziemy korzystać w niniejszej książce.

Reprezentowanie klas Łatwo się domyślić, że głównymi składnikami diagramów klas są same klasy. Klasa jest reprezentowana na diagramie prostokątem opatrzonym nazwą, jak na rysunku 6.1.

Rysunek 6.1. Klasa na diagramie klas UML Prostokąt klasy podzielony jest na trzy części; pierwszą z nich zajmuje nazwa klasy. Jeśli piktogram klasy na diagramie nie powinien zawierać niczego poza nazwą, wyróżnianie pozostałych pól w piktogramie klasy nie jest obowiązkowe. Projektując diagram klas, szybko spostrzeżesz, że szczegółowość opisu klasy z rysunku 6.1 jest dla wielu klas wystarczająca. Język UML nie wymaga bowiem wymieniania wszystkich składowych czy metod poszczególnych klas — ba, diagram klas nie musi zawierać kompletu klas projektu! Klasy abstrakcyjne są wyróżniane albo pochyleniem czcionki nazwy (jak na rysunku 6.2), albo umieszczonym poniżej nazwy oznaczeniem {abstrakcyjna} (jak na rysunku 6.3). Forma pierwsza jest popularniejsza, druga zaś lepiej nadaje się do odręcznych notatek.

Rysunek 6.2. Klasa abstrakcyjna na diagramie klas

Rysunek 6.3. Klasa abstrakcyjna na diagramie klas w notacji z ograniczeniem (metką)  Uwaga Notacja {abstrakcyjna} jest przykładem notacji charakterystycznej dla „ograniczeń” (ang. constraint). Ograniczenia służą na diagramach klas do opisu sposobów, w jakie należy wykorzystywać konkretne elementy diagramu. Nie istnieje przy tym żadna wyróżniona składnia dla tekstu umieszczanego pomiędzy nawiasami klamrowymi — powinien on jedynie wyjaśniać warunki wymagane dla elementu.

134

ROZDZIAŁ 6.  OBIEKTY A PROJEKTOWANIE OBIEKTOWE

Interfejsy obrazuje się na diagramie klas tak samo jak klasy, tyle że należy je uzupełnić o stereotyp (element zapewniający rozszerzalność języka UML), jak to zrobiono na rysunku 6.4.

Rysunek 6.4. Interfejs

Atrybuty Ogólnie rzecz ujmując, atrybuty odwzorowują składowe klas. Atrybuty klas wymieniane są w polu przylegającym bezpośrednio do pola nazwy klasy — patrz rysunek 6.5.

Rysunek 6.5. Atrybut Przyjrzyjmy się bliżej określeniu atrybutu z rysunku 6.5. Poprzedzający właściwy atrybut symbol odzwierciedla poziom widoczności, czyli dostępności atrybutu spoza klasy. Można tu zastosować jeden z trzech symboli, których interpretację opisuje tabela 6.1. Tabela 6.1. Symbole widoczności atrybutów Symbol

Widoczność

Znaczenie

+

Publiczna

Atrybut dostępny ogólnie.

-

Prywatna

Atrybut dostępny wyłącznie w ramach bieżącej klasy.

#

Chroniona

Atrybut dostępny wyłącznie w ramach bieżącej klasy i jej pochodnych.

Za symbolem widoczności podaje się nazwę atrybutu. W naszym przypadku opis dotyczy składowej ShopProduct::$price. Występujący za nią znak dwukropka oddziela nazwę atrybutu od jego typu (i opcjonalnie

podawanej wartości domyślnej). I znowu: na diagramie umieszczamy tylko to, co jest konieczne do czytelnego zilustrowania danej koncepcji.

Operacje Operacje reprezentują metody, a mówiąc ściślej, opisują wywołania, jakie można inicjować na rzecz klasy. Na rysunku 6.6 widać element reprezentujący klasę ShopProduct, uzupełniony o dwie operacje:

Rysunek 6.6. Operacje Jak widać, składnia operacji jest zbliżona do składni atrybutów. Nazwę metody poprzedza więc symbol jej widoczności. Operacje wyróżnia ujęta w nawiasy lista parametrów. Za listą parametrów, po znaku dwukropka, określany jest typ wartości zwracanej przez metodę (o ile jest zdefiniowany). Parametry na liście wymieniane są po przecinku, a ich składnia z grubsza odpowiada składni atrybutów — każdy parametr składa się z nazwy, dwukropka i typu.

135

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Jak można się spodziewać, składnia ta jest dość elastyczna. Można na przykład pominąć symbol widoczności czy typ zwracany. Parametry z kolei często reprezentuje się wyłącznie typami (bez nazw) — w większości języków programowania nazwy argumentów przekazywanych w wywołaniu nie mają bowiem żadnego znaczenia.

Relacje dziedziczenia i implementacji Język UML opisuje relację dziedziczenia jako relację uogólnienia, czyli „generalizacji” klas pochodnych w klasie bazowej3. Relacja ta jest reprezentowana na diagramie przez linię wiodącą od klasy pochodnej do klasy bazowej. Linia kończy się zarysem (niewypełnionym) strzałki. Relację dziedziczenia pomiędzy klasą ShopProduct a jej klasami pochodnymi ilustruje rysunek 6.7.

Rysunek 6.7. Opis relacji dziedziczenia Relacja pomiędzy interfejsem a klasami implementującymi ten interfejs to w języku UML tzw. relacja „realizacji”. Gdyby więc klasa ShopProduct implementowała interfejs Chargeable, w języku UML wyrazilibyśmy to tak jak na rysunku 6.8.

Rysunek 6.8. Opis relacji implementacji

Powiązania Dziedziczenie to tylko jedna z wielu możliwych relacji, w jakie mogą wchodzić klasy w systemie obiektowym. Kolejną jest na przykład powiązanie (ang. association), zachodzące, kiedy składowa klasy przechowuje referencję egzemplarza (albo egzemplarzy) innej klasy. Relację powiązania pomiędzy klasami Teacher (nauczyciel) i Pupil (uczeń) modelujemy na rysunku 6.9.

Rysunek 6.9. Powiązanie Taki opis nie informuje jednoznacznie o rodzaju powiązania. Wiadomo jedynie, że obiekt klasy Teacher będzie przechowywał referencję do jednego bądź wielu obiektów klasy Pupil albo odwrotnie — to obiekt klasy Pupil będzie przechowywał referencje obiektów klasy Teacher. Relacja powiązania może być również dwustronna. 3

Zatem trochę na opak, bo programiści mówią zazwyczaj o specjalizacji klasy bazowej w klasach pochodnych — przyp. tłum.

136

ROZDZIAŁ 6.  OBIEKTY A PROJEKTOWANIE OBIEKTOWE

Do określenia kierunku relacji powiązania służą strzałki. Gdyby to obiekt klasy Teacher miał przechowywać referencję obiektu klasy Pupil (ale nie odwrotnie), powinniśmy poprowadzić strzałkę od klasy Teacher, a w kierunku klasy Pupil. Takie powiązanie nosi nazwę jednokierunkowego (patrz rysunek 6.10).

Rysunek 6.10. Powiązanie jednokierunkowe Gdyby obiekty obu klas przechowywały referencję do obiektów drugiej klasy, relację taką, jako dwukierunkową, należałoby zasygnalizować na diagramie strzałkami w obu kierunkach, jak na rysunku 6.11.

Rysunek 6.11. Powiązanie dwukierunkowe Można też w relacji powiązania wyszczególnić liczbę egzemplarzy klasy, do której odwołuje się każdy obiekt klasy bieżącej. Czyni się to za pośrednictwem liczb albo zakresów umieszczanych przy prostokątach klas. Jeśli mowa o „dowolnej liczbie egzemplarzy”, należy w miejsce liczby czy zakresu zastosować znak gwiazdki (*). Wedle rysunku 6.12 jeden obiekt klasy Teacher przechowuje referencje do nieokreślonej z góry liczby obiektów klasy Pupil.

Rysunek 6.12. Definiowanie krotności relacji powiązania Na rysunku 6.13 widać z kolei, że pojedynczy obiekt klasy Teacher będzie powiązany z minimalnie pięcioma, a maksymalnie dziesięcioma obiektami klasy Pupil.

Rysunek 6.13. Definiowanie krotności relacji powiązania

Agregacja i kompozycja Agregacja i kompozycja to relacje o charakterze zbliżonym do powiązania. Wszystkie one opisują bowiem sytuację, w której klasa przechowuje trwałą referencję do jednego albo wielu egzemplarzy innej klasy. Przy agregacji i kompozycji owe obiekty wchodzą jednak w skład obiektu bieżącej klasy. W przypadku agregacji obiekt zawierający się w obiekcie danej klasy jest jego nieodłączną częścią, choć może równocześnie być zawierany w innych obiektach. Relacja agregacji jest obrazowana linią rozpoczynającą się symbolem pustego rombu. Rysunek 6.14 ilustruje dwie klasy: SchoolClass (grupa zajęciowa) i Pupil (uczeń). Klasa składa się tu z uczniów.

137

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Rysunek 6.14. Agregacja Uczniowie tworzą grupę zajęciową, równocześnie poszczególni uczniowie mogą należeć do więcej niż jednej grupy. Odwołanie zajęć grupy nie oznacza więc zwolnienia uczniów do domu — być może mają jeszcze zajęcia w innej grupie. Kompozycja to zależność jeszcze silniejsza. W kompozycji do obiektu zawieranego może odwoływać się wyłącznie obiekt go zawierający. Relacja kompozycji ilustrowana jest tak samo jak relacja agregacji, jedynie romb jest wypełniany. Relację kompozycji w języku UML ilustruje rysunek 6.15.

Rysunek 6.15. Kompozycja Klasa Person (osoba) zawiera referencję obiektu SocialSecurityData (dane ubezpieczenia społecznego). Jeden numer ubezpieczenia społecznego może przynależeć tylko do jednej osoby.

Relacja użycia Relacja użycia jest w języku UML opisywana jako „zależność”. To najsłabsza z relacji omawianych w tym podrozdziale — nie opisuje bowiem żadnego stałego, a tylko przejściowe powiązanie pomiędzy klasami. Obiekt klasy używanej może zostać przekazany do klasy używającej za pośrednictwem argumentu wywołania metody, może też zostać pozyskany jako wartość zwracana z wywołania metody. Z rysunku 6.16 wynika, że klasa Report używa obiektu klasy ShopProductWriter. Relacja użycia jest reprezentowana przerywaną linią i otwartą strzałką łączącą dwie klasy. Nie oznacza to jednak, że klasa Report przechowuje referencję obiektu (czy obiektów) klasy ShopProductWriter; z kolei obiekt klasy ShopProductWriter przechowuje trwale tablicę obiektów klasy ShopProduct.

Rysunek 6.16. Relacja zależności

138

ROZDZIAŁ 6.  OBIEKTY A PROJEKTOWANIE OBIEKTOWE

Notki Diagramy klas mogą dobrze odzwierciedlać strukturę systemu obiektowego, nie dają jednak poglądu na proces odbywający się w systemie. Rysunek 6.16 ilustruje klasy uczestniczące w naszym systemie. Widać na nim, że klasa Report używa obiektu klasy ShopProductWriter, nie wiadomo jednak, na czym owo użycie polega. Sens tego użycia możemy przybliżyć oglądającemu diagram, umieszczając na nim notki, jak na rysunku 6.17.

Rysunek 6.17. Notka wyjaśniająca charakter zależności użycia Jak widać, notka to prostokąt z zagiętym narożnikiem. Często zawiera fragmenty pseudokodu. Dzięki notce widać, że obiekt klasy Report używa obiektu klasy ShopProductWriter do wyprowadzania danych o produkcie. To żadne odkrycie, ale przecież relacje użycia nie zawsze są tak oczywiste, jak w tym przykładzie. Niekiedy nawet notka nie daje wystarczającej ilości informacji. Na szczęście poza modelowaniem samej struktury możemy w języku UML opisywać również interakcje zachodzące w systemie.

Diagramy sekwencji Diagram sekwencji operuje raczej obiektami niż klasami. Służy do modelowania poszczególnych etapów procesu przebiegającego w systemie. Spróbujmy skonstruować prosty diagram modelujący środki, za pomocą których obiekt klasy Report wypisuje dane o produktach. Diagram sekwencji wymienia w poziomie uczestników systemu, jak na rysunku 6.18.

Rysunek 6.18. Obiekty na diagramie sekwencji Obiekty oznaczyliśmy na diagramie nazwami ich klas. Gdyby w systemie działało niezależnie wiele egzemplarzy tej samej klasy, moglibyśmy umieścić na diagramie osobne bloki tych obiektów, stosując dla nich etykiety w formacie obiekt::klasa (np. product1::ShopProduct). Czas życia obiektów w modelowanym systemie prezentuje się w pionie, jak na rysunku 6.19. Przerywane linie pionowe reprezentują „linie życia” obiektów w systemie. Umieszczone na nich prostokąty reprezentują zaś fakt uczestnictwa obiektów w poszczególnych fazach procesu odbywającego się w systemie. Gdy analizuje się rysunek 6.19 od góry do dołu, widać, jak sterowanie w procesie jest przenoszone pomiędzy obiektami. Przebieg sterowania jest jednak nieczytelny, jeśli diagram nie zawiera komunikatów przekazywanych pomiędzy obiektami. Diagram został więc na rysunku 6.20 uzupełniony stosownymi komunikatami.

139

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Rysunek 6.19. „Linie życia” obiektów na diagramie sekwencyjnym

Rysunek 6.20. Kompletny diagram sekwencji Strzałki reprezentują kierunek przesyłania komunikatów pomiędzy obiektami. Wartości zwracane są często na diagramie sekwencji pomijane (choć można je reprezentować liniami przerywanymi prowadzącymi od wywołanego obiektu do inicjatora komunikatu). Każdy komunikat jest etykietowany wywołaniem metody. Etykiety można dobierać dość dowolnie, przyjęta jest jednak pewna prosta składnia. Otóż nawiasy prostokątne reprezentują warunki, więc: [okToPrint] write()

oznacza, że wywołanie metody write() jest uzależnione od spełnienia warunku okToPrint. Znak gwiazdki oznacza z kolei powtórzenie, którego charakter powinien zostać wyjaśniony w nawiasach prostokątnych: *[dla każdego obiektu ShopProduct] write()

Spróbujmy zatem dokonać analizy procesu implementowanego w systemie, analizując od góry do dołu diagram sekwencji. Na początku obiekt klasy Report pozyskuje listę obiektów ShopProduct, wywołując stosowną metodę obiektu klasy ProductStore. Listę tę przekazuje do obiektu ShopProductWriter, który najprawdopodobniej zachowuje referencję obiektów występujących na liście (choć trudno to wywnioskować

140

ROZDZIAŁ 6.  OBIEKTY A PROJEKTOWANIE OBIEKTOWE

z samego diagramu). Obiekt klasy ShopProductWriter wywołuje następnie dla każdego otrzymanego obiektu metodę ShopProduct::getSummaryLine(), prowokując wyprowadzenie na wyjście kompletu danych o wszystkich produktach. Jak widać, przebieg procesu w systemie da się modelować diagramem sekwencji, dobrze odzwierciedlającym dynamiczne interakcje i prezentującym je w sposób nadspodziewanie czytelny.  Uwaga Spójrz na rysunki 6.16 i 6.20. Zauważ, w jaki sposób diagram klas ilustruje polimorfizm, pokazując klasy pochodne klas ShopProductWriter i ShopProduct. Zwróć teraz uwagę, że szczegół ten stał się niewidoczny przy modelowaniu komunikacji pomiędzy obiektami. Tam, gdzie to możliwe, chcemy bowiem operować na obiektach najbardziej ogólnych z dostępnych typów, ukrywając tym samym szczegóły implementacji.

Podsumowanie W rozdziale przeszliśmy od niskopoziomowych szczegółów programowania obiektowego do kwestii ogólniejszych — projektowych. Przedstawiono pojęcia hermetyzacji, spójności i osłabiania zależności mające zasadnicze znaczenie dla elastyczności systemów obiektowych i możliwości wielokrotnego wykorzystania ich komponentów. Udało się też omówić podstawy języka UML, kładąc tym samym fundament pod omówienie wzorców projektowych stanowiących temat kolejnej części niniejszej książki.

141

PHP. OBIEKTY, WZORCE, NARZĘDZIA

142

ROZDZIAŁ 7 

Czym są wzorce projektowe? Do czego się przydają? Większość problemów, z jakimi stykamy się jako programiści, już wcześniej rozwiązali nasi koledzy po fachu. Wzorce projektowe pozwalają im podzielić się z nami zyskanym przy tej okazji doświadczeniem. Każdy nowy wzorzec projektowy wzbogaca nasz język, ułatwiając wyrażanie idei projektowych i ich konsekwencji. Wzorce projektowe to po prostu destylaty najczęściej obserwowanych problemów, definiujące ich sprawdzone rozwiązania i opisujące wyniki wdrażania tych rozwiązań. Wiele książek i artykułów koncentruje się na szczegółach języków programowania, dostępnych funkcjach, klasach i metodach bibliotecznych. Katalogi wzorców koncentrują się natomiast na tym, jak od tych szczegółów technicznych („co”) przejść do poziomu rozpoznawania problemów i dostrzegania potencjalnych rozwiązań do użycia w danym projekcie („dlaczego” i „jak”). W niniejszym rozdziale wprowadzę Czytelnika w tematykę wzorców projektowych, próbując przy okazji wyjaśnić przyczyny ich rosnącej popularności. Rozdział będzie traktował o:  Podstawach wzorców projektowych — czyli o tym, czym one są.  Strukturze wzorców projektowych — czyli o kluczowych elementach wzorca projektowego.  Zaletach stosowania wzorców projektowych — czyli o tym, dlaczego warto poświęcać swój czas na ich poznanie.

Czym są wzorce projektowe? „W świecie oprogramowania wzorzec jest namacalną manifestacją pamięci i doświadczeń członków organizacji” — Grady Booch, Core J2EE Patterns1 „[Wzorzec] to rozwiązanie problemu w danym kontekście” — The Gang of Four, Design Patterns: Elements of Reusable Object-Oriented Software2 Jak wynika z powyższych cytatów, wzorzec projektowy to analiza problemu i prezentacja jego rozwiązania zgodnego z „zasadami sztuki”. Jako programiści aplikacji WWW wciąż od nowa musimy rozwiązywać te same problemy. Jak obsługiwać napływające żądania? Jak konwertować zawarte w nich dane do instrukcji wykonywanych we wnętrzu aplikacji? Jak pozyskiwać dane ze źródeł danych? Jak prezentować wyniki? Nasze mniej lub bardziej eleganckie propozycje rozwiązań z czasem tworzą nieformalny zbiór technik, które wykorzystujemy w kolejnych projektach. Owe techniki to nic innego jak wzorce projektowe. 1 2

Wydanie polskie: J2EE. Wzorce projektowe. Wydanie 2, Helion, 2004 — przyp. tłum. Wydanie polskie: Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku, Helion, 2010 — przyp. tłum.

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Wzorzec projektowy to artykulacja i formalizacja obserwowanych problemów i ich rozwiązań, wyrażenie z trudem nabytego doświadczenia udostępnianego w ten sposób szerszemu gronu programistów. Wzorce są (a przynajmniej powinny być) wstępujące, nie zstępujące — wywodzą się z praktyki, a nie z teorii. Nie znaczy to, że wzorce projektowe całkowicie ignorują osiągnięcia teoretyczne (o tym, że tak nie jest, przekonasz się już w następnym rozdziale), ale że bazują na technikach praktycznych opracowywanych przez żywych i czynnych zawodowo programistów. Znany i uznany popularyzator wzorców, Martin Fowler, mawia, że on wzorce odkrywa, a nie wynajduje. Jest to rozróżnienie jak najbardziej zasadne, bo wiele wzorców projektowych prowokuje uczucie déjà vu, kiedy rozpoznajemy w nich techniki, które sami z powodzeniem stosujemy. Katalogu wzorców projektowych nie da się porównać do książki kucharskiej. Gdyby tak było, katalog mógłby zawierać kod dający się zastosować we własnych projektach po niewielkich jedynie modyfikacjach — niekoniecznie przy pełnym zrozumieniu tego kodu (i kucharz nie musi znać procesów zachodzących w kotle, jeśli tylko trzyma się ściśle receptury). Tymczasem wzorce projektowe opisują podejście do poszczególnych problemów. Szczegóły implementacji rozwiązania mogą być w różnych kontekstach bardzo różne. Ów kontekst obejmuje przy tym choćby język programowania, naturę programowanej aplikacji, rozmiar projektu i specyfikę problemu. Załóżmy dla przykładu, że w projekcie potrzebujemy utworzyć system szablonów. Na podstawie nazwy pliku szablonu trzeba uruchomić operację parsowania szablonu i zbudowania drzewa obiektów reprezentujących znaczniki występujące w szablonie. Zaczniemy od domyślnego parsera skanującego tekst szablonu w poszukiwaniu wyróżnionych elementów leksykalnych. Po odnalezieniu takiego elementu odpalany jest inny parser, który z kolei jest specjalizowany do obsługi znaczników zagnieżdżonych w danym elemencie. Postępujemy tak dopóty, dopóki parsowanie szablonu nie zostanie przerwane błędem albo się nie zakończy, ewentualnie dopóki nie dojdzie do miejsca odpalenia kolejnego parsera specjalizowanego. Łącznie wszystkie te komponenty opatrzymy mianem rekurencyjnego parsera zstępującego. Wyróżniliśmy więc uczestników systemu: parser główny MainParser, parser znacznika TagParser oraz parser argumentów ArgumentParser. Do tworzenia i zwracania obiektów tych klas powołamy wytwórnię ParserFactory. Oczywiście rzecz nie będzie tak zupełnie prosta — gdzieś w połowie może się okazać, że w szablonach trzeba obsługiwać więcej niż jedną składnię. Trzeba będzie wtedy utworzyć równoległy zestaw parserów odpowiednich dla nowej składni — np. OtherTagParser, OtherArgumentParser i tak dalej. Tak właśnie przedstawia się nasz problem: potrzebujemy mechanizmu do generowania różnych zestawów obiektów zależnie od okoliczności i chcemy, aby mechanizm ten był mniej lub bardziej przezroczysty dla pozostałych komponentów systemu. Tak się składa, że Banda Czworga zdefiniowała podobny problem w podsumowaniu swojej książki — to wzorzec abstrakcyjnej wytwórni Abstract Factory, a słownie: „udostępnianie interfejsu do tworzenia rodzin obiektów powiązanych lub współzależnych, bez jawnego wymieniania konkretnych klas”. Chyba pasuje? Tak właśnie sama natura problemu określa i kształtuje stosowany wzorzec projektowy. Nie ma przy tym mowy o prostym kopiowaniu i wklejaniu rozwiązań — będzie się można o tym przekonać w rozdziale 9., gdzie zajmiemy się właśnie wzorcem Abstract Factory. Bardzo ważny jest akt nazwania wzorca projektowego — nazwy wzorców mają stanowić słownictwo do powszechnego użytku; w bardziej wiekowych profesjach i rzemiosłach nazwy kształtowały się samorzutnie całymi latami. Takie „gotowce” sprzyjają kooperacji w projektowaniu, o ile ich wymowa będzie odpowiednio wyważona. Przy omawianiu alternatywnych rodzin parserów z kolegami z zespołu możemy więc posłużyć się porównaniem: potrzebujemy systemu, który tworzy każdy z zestawów parserów za pomocą abstrakcyjnej wytwórni. Koledzy pokiwają głowami, albo od razu łapiąc zagadnienie, albo zapamiętując, że mają sprawdzić, cóż to za wzorzec. Tak czy inaczej zyskaliśmy narzędzie poznawcze: dla całego inwentarza pojęć i konsekwencji projektowych posiadamy chwytliwą i łatwą do zapamiętania nazwę, pozostającą w ścisłym związku z sednem naszego problemu. Wedle prawa międzynarodowego nie wolno pisać o wzorcach projektowych, nie powołując się na Christophera Alexandra, architekta, którego prace miały silny wpływ na pierwszych pomysłodawców i promotorów wzorców projektowych w dziedzinie programowania obiektowego. W książce A Pattern Language (Oxford University Press, 1977), napisał on: Każdy wzorzec opisuje problem charakterystyczny dla naszego środowiska i wciąż się w nim przewijający; ujmuje też sedno rozwiązania tego problemu w taki sposób, aby rozwiązanie to dało się stosować miliony razy, a nigdy tak samo.

144

ROZDZIAŁ 7.  CZYM SĄ WZORCE PROJEKTOWE? DO CZEGO SIĘ PRZYDAJĄ?

Powyższa definicja, przewidziana dla problemów i rozwiązań z dziedziny architektury, w pierwszej kolejności wymienia problem i jego otoczenie, a dopiero na drugim miejscu stawia rozwiązanie. Ostatnimi laty pojawiały się głosy krytykujące nadużywanie wzorców projektowych, zwłaszcza przez niedoświadczonych programistów. Takie nadużycia wynikają często ze stosowania rozwiązania mimo braku rozpoznania problemu i jego kontekstu. Wzorce to coś więcej niż jakaś zalecana organizacja klas i obiektów i coś więcej niż zdefiniowanie ich współdziałania. Struktura wzorców obejmuje również definicje warunków stosowalności rozwiązania a także omówienie efektów wdrożenia rozwiązania. W niniejszej książce przyjmiemy dla wzorców formę zaproponowaną w klasycznej już pozycji Design Patterns: Elements of Reusable Object-Oriented Software3 autorstwa Ericha Gammy, Richarda Helma, Ralpha Johnsona i Johna Vlissidesa (Addison-Wesley Professional, 1995). Pozycja ta traktuje o wzorcach w programowaniu i projektowaniu obiektowym — przedstawiono w niej szereg klasycznych już wzorców dających się stosować w większości współczesnych projektów obiektowych. Znaczenie książki Bandy Czworga wynika nie tylko z artykulacji kluczowych wzorców projektowych, ale i z ujęcia w nich zasad projektowych. Niektóre z tych zasad omówiono w następnym rozdziale.  Uwaga Wzorce opisywane przez Bandę Czworga i omawiane w tej książce rozpatrywane w całości stanowią rodzaj słownika albo katalogu problemów i rozwiązań, zorganizowanego tak, że jego pozycje wzajemnie się uzupełniają, tworząc spójną całość. Istnieją też języki wzorców dla innych dziedzin problemów niż programowanie — językiem takim dysponujemy choćby w projektowaniu wizualnym i zarządzaniu projektami (oraz, rzecz jasna, w architekturze). Wzorce projektowe omawiane w tej książce odnoszą się natomiast wyłącznie do problemów i rozwiązań właściwych dla dziedziny programowania obiektowego.

Wzorzec projektowy Wzorzec projektowy zasadniczo składa się z czterech części: nazwy, problemu, rozwiązania i konsekwencji.

Nazwa Nazwa ma kolosalne znaczenie. Ma wzbogacać język społeczności programistów, proponować kilka krótkich słów w zgrabnym układzie, zastępującym rozwlekłe opisy złożonych niekiedy problemów i rozwiązań. Musi zachowywać równowagę pomiędzy zwartością a wymownością. Banda Czworga stwierdza wprost: „Znalezienie właściwych nazw było jedną z większych trudności w opracowywaniu naszego katalogu”. Zgadza się z tym również Martin Fowler: „Nazwy wzorców mają zasadnicze znaczenie, ponieważ jednym z zadań wzorców jest ustalenie słownictwa umożliwiającego efektywne komunikowanie się programistów” (Patterns of Enterprise Application Architecture4, Addison-Wesley Professional, 2002). W swojej książce Martin Fowler doprecyzował wzorzec dostępu do baz danych, z którym po raz pierwszy zetknąłem się w czasie lektury Core J2EE Patterns5 autorstwa Deepaka Alura, Dana Malksa i Johna Crupiego (Prentice Hall, 2003). Zdefiniował dwie specjalizacje starszego wzorca. Logika tego podejścia jest w oczywisty sposób poprawna (jeden z nowych wzorców modelował obiekty, drugi zaś modelował tabele bazy danych — rozróżnienie to w pierwotnej wersji było rozmyte). Trudno było mi się jednak zmusić do przyjęcia nowych wzorców do wiadomości i znaleźć dla nich miejsce w pamięci — nazwę oryginalnego wzorca stosowałem w dokumentacjach tak długo i często, że zakorzeniła się mocno w moim prywatnym słowniku.

3 4 5

Wydanie polskie: Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku, Helion, 2010 — przyp. tłum. Wydanie polskie: Architektura zarządzania przedsiębiorstwem. Wzorce projektowe, Helion, 2005 — przyp. tłum. Wydanie polskie: J2EE. Wzorce projektowe. Wydanie 2, Helion, 2004 — przyp. tłum.

145

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Problem Niezależnie od elegancji rozwiązania (a elegancja niektórych jest nieprzeciętna) sednem wzorca projektowego jest właśnie problem i jego kontekst. W przypadku większości wzorców projektowych znacznie trudniejsze od wdrożenia rozwiązania jest rozpoznanie samego problemu. Stąd właśnie wynika ryzyko nadużywania bądź chybionego stosowania wzorców. Wzorce bardzo starannie opisują przestrzeń problemu. Najpierw mamy więc skrótowy zarys problemu, potem zaś jego umiejscowienie w kontekście poparte najczęściej typowym przykładem i jednym bądź kilkoma diagramami. Dalej problem jest dzielony wedle dających się wyróżnić detali i rozmaitych jego manifestacji. Opisywane są przy tym wszystkie symptomy pozwalające na rozpoznanie problemu.

Rozwiązanie Początkowo rozwiązanie jest zestawiane z problemem. Dalej następuje jego szczegółowy opis często wykorzystujący diagramy klas i interakcji języka UML. Zwykle wzorzec zawiera też w tej części przykładowe kody źródłowe. Mimo obecności kodu nie należy sądzić, że będzie nadawał się on do prostego skopiowania do bieżącego projektu. Wzorzec opisuje bowiem — przypominam — podejście do problemu, a faktyczna implementacja rozwiązania może obejmować setki niuansów. Można to porównać do instrukcji zasiewu — jeśli będziemy przestrzegać ich zupełnie ślepo, może się zdarzyć, że w czasie zbiorów cierpieć będziemy głód. Lepsze byłoby podejście charakterystyczne dla wzorców, w ramach którego sedno rozwiązania jest prezentowane w maksymalnie ogólny sposób, który będzie stanowić niezmiennik tego rozwiązania (chodzi wszak zawsze o zbiór plonów, a to osiąga się w wyniku zasiewu, nawożenia, nawadniania i żniw). Ale konkretna procedura uprawy będzie zależna od mnóstwa czynników, w tym rodzaju gleby, położenia upraw, orientacji terenu, występowania lokalnych szkodników upraw i wielu innych. Martin Fowler określa obrazowo rozwiązania prezentowane w ramach wzorców projektowych jako „niedopieczone” — programista otrzymuje do dyspozycji pewien pomysł, ale o jego wykończenie musi zadbać sam.

Konsekwencje Każda podejmowana decyzja projektowa ma swój — zwykle szeroki — oddźwięk. Pierwszą konsekwencją powinno być oczywiście rozwiązanie rozpoznanego problemu. Rozwiązanie jednak, raz wdrożone, może okazać się idealnie dopasowane do pozostałych wdrażanych wzorców, ale równie dobrze może się okazać, że się z nimi kłóci.

Format wzorca według Bandy Czworga Gdy piszę te słowa, mam przed sobą pięć katalogów wzorców projektowych. Szybki przegląd zawartych w nich wzorców wykazuje, że w każdym z nich przyjęto inną strukturę opisu wzorca. Niektóre struktury są bardziej formalne od innych, niektóre zaś bardzo hasłowe, z wieloma punktami; jeszcze inne mają bardziej opisowy charakter. W obiegu jest przynajmniej kilka uznanych i sprawdzonych struktur wzorców projektowych, w tym format oryginalny proponowany przez Christophera Alexandra czy format opisowy promowany w ramach repozytorium Portland Pattern Repository (tzw. format portlandzki). Z racji znaczenia przypisywanego powszechnie książce Bandy Czworga (jak i dlatego, że opisano w niej większość z tych wzorców, które będę prezentować) powinniśmy przyjrzeć się elementom występującym w zawartej tam propozycji struktury wzorca:  Zamysł — krótka deklaracja przeznaczenia wzorca.  Opis problemu — zazwyczaj w ujęciu typowych dla niego sytuacji. Niekiedy opis jest anegdotyczny, co pozwala na łatwiejsze przyswojenie wzorca.

146

ROZDZIAŁ 7.  CZYM SĄ WZORCE PROJEKTOWE? DO CZEGO SIĘ PRZYDAJĄ?

 Zakres zastosowań — analiza sytuacji, w których można wdrożyć wzorzec. Tutaj, w odróżnieniu od poprzedniej sekcji, gdzie problem opisywany jest w ujęciu typowym, pojawiają się omówienia sytuacji szczególnych, z analizą odpowiedniości rozwiązania w ich kontekstach.  Struktura (interakcje) — te sekcje mogą zawierać diagramy klas i interakcji konstruowane w języku UML, a opisujące relacje pomiędzy klasami i obiektami występującymi w rozwiązaniu.  Implementacja — szczegóły rozwiązania z uwzględnieniem wszelkich kwestii, które mogą pojawić się podczas wdrażania rozwiązania, wraz z poradami co do sposobu wdrożenia wzorca.  Próbki kodu — sekcja czytana często w pierwszej kolejności, gdyż kod przykładowy często pozwala ogarnąć wzorzec jako całość. Przykładowa próbka kodu jest zwykle okrojona, tak aby zawierała jedynie samo sedno rozwiązania. Może być wyrażona w dowolnym języku programowania. W tej książce będzie to oczywiście wyłącznie PHP.  Znane wdrożenia — prawdziwe, działające systemy, w których z powodzeniem zastosowano wzorzec (rozpoznano problem, jego kontekst i wdrożono rozwiązanie). Niektórzy uważają, że wzorzec staje się wzorcem dopiero po wdrożeniu w przynajmniej trzech publicznie dostępnych kontekstach (mowa o tzw. regule trzech).  Wzorce pokrewne — niektóre z wzorców implikują stosowanie kolejnych. Wdrażając rozwiązanie, tworzymy bowiem kontekst, w którym zastosowanie znajdują inne wzorce. Tego rodzaju pokrewieństwa są przedmiotem analizy w ostatniej sekcji wzorca. Tutaj również omawia się wzorce, które dotyczą podobnych problemów albo mają podobne rozwiązania, oraz wszelkie wzorce bazowe — te, które były definiowane i publikowane gdzie indziej, a stanowią podstawę konstrukcji bieżącego wzorca.

Po co nam wzorce projektowe? Jakie korzyści możemy osiągnąć, stosując wzorce projektowe? Skoro wzorzec to opis problemu i rozwiązania, odpowiedź jest oczywista. Wzorce są pomocne w rozwiązywaniu najczęstszych problemów. Ale nie tylko.

Wzorzec projektowy definiuje problem Każdy programista wielokrotnie osiągał w projektach etap, w którym okazywało się, że nie ma dalszej drogi naprzód. Trzeba się wtedy cofnąć i być może zmienić niektóre z założeń projektu, umożliwiając jego dalszy rozwój. Wzorce projektowe, definiując najczęściej spotykane problemy, pomagają w ulepszaniu projektu. Niekiedy zaś pierwszym krokiem do rozwiązania jest rozpoznanie problemu, w czym wzorce są niezastąpione.

Wzorzec projektowy definiuje rozwiązanie Po zdefiniowaniu i rozpoznaniu problemu (i upewnieniu się, że diagnoza jest trafna) z wzorca zaczerpniemy rozwiązanie poparte analizą konsekwencji jego wdrożenia. Choć wzorzec projektowy nie zwalnia programisty i projektanta od samodzielnej analizy skutków wdrożenia i odpowiedzialności za podjętą decyzję, daje chociaż pewność, że proponowane rozwiązanie jest rozwiązaniem wypróbowanym w wielu sytuacjach.

Wzorce projektowe są niezależne od języka programowania Wzorce definiują obiekty i rozwiązania obiektowe. Oznacza to, że ich zastosowania nie ograniczają się do jednego czy drugiego języka programowania. Kiedy zaczynałem stosować wzorce w swoich projektach, czytałem próbki kodu w językach C++ i Smalltalk i implementowałem na ich bazie rozwiązania w języku Java. Wzorzec daje się zazwyczaj stosować w niemal dowolnym języku obiektowym, choć transfer może odbyć się

147

PHP. OBIEKTY, WZORCE, NARZĘDZIA

kosztem zmiany zakresu stosowalności czy efektów i konsekwencji wdrożenia. Tym samym oparcie projektowania na wzorcach ułatwia znakomicie przenosiny do innego języka programowania. Aplikacja, która zostanie oparta na prawidłach projektowania obiektowego, daje się najczęściej łatwo dostosować do innych niż macierzysty języków programowania (choć nigdy nie odbywa się to zupełnie bezproblemowo).

Wzorce definiują słownictwo Wzorce, uzupełniając propozycje rozmaitych technik nazwami, wzbogacają i upraszczają równocześnie język komunikacji w zespołach programistycznych. Wyobraźmy sobie zebranie takiego zespołu. Znamy już opis rozwiązania abstrakcyjnej wytwórni obiektów Abstract Factory, teraz należałoby opisać strategię zarządzania danymi przetwarzanymi w systemie. Wyjaśnimy swoje zamiary koledze: Ja: Zamierzam zastosować wzorzec Composite. Kolega: Chyba tego nie przemyślałeś… Nie doczekaliśmy się zrozumienia u kolegi, ale to norma. Ważne jednak, że wiedział, o czym mowa — z tego punktu widzenia nasza wymiana myśli była niezwykle udana. Jak wyglądałaby ta sama scenka, gdybyśmy obaj nie znali słownictwa zaczerpniętego z wzorców? Spójrzmy: Ja: Zamierzam zastosować drzewo obiektów dzielących wspólny typ. Interfejs typu będzie obejmował metody umożliwiające dodawanie do drzewa obiektów potomnych innych typów. W ten sposób będę mógł konstruować w czasie wykonania złożone kombinacje obiektów implementacji. Kolega: Hę? Wzorce, a raczej opisywane przez nie techniki, wzajemnie się niekiedy uzupełniają. I tak wzorzec Composite (kompozyt) całkiem dobrze współpracuje z wzorcem Visitor (wizytator), na przykład: Ja: A potem będzie można zestawiać dane za pomocą wizytatorów. Kolega: Nie o to chodzi. Nie warto go słuchać. Nie będę już rozpisywał tego dialogu w wersji pozbawionej „wzorcowego” słownictwa projektowego. Nawiasem mówiąc, wzorzec Composite zostanie zaprezentowany w rozdziale 10., a Visitor — w rozdziale 11. Sęk w tym, że i bez języka wywodzącego się z wzorców projektowych stosowalibyśmy opisywane nim techniki. Istniały one bowiem i były z powodzeniem stosowane, jeszcze zanim zostały nazwane i sformalizowane. Gdyby więc wzorce jeszcze nie istniały, prędzej czy później pojawiłyby się w takiej czy innej formie. Każde odpowiednio długo wykorzystywane narzędzie otrzymuje przecież w końcu swoją nazwę.

Wzorce są wypróbowane Skoro wzorce dokumentują uznane praktyki projektowe i programistyczne, to czy jedynym wkładem twórców katalogów wzorców jest nazwanie owych technik? Pogląd ten zdaje się zasadny. Wzorce prezentują uznane praktyki w kontekście obiektowym. Dla bardziej doświadczonych programistów cała ta zabawa z ich artykułowaniem jest zapewne zbędna. Dla reszty społeczności programistów rzecz wygląda inaczej — wzorce dają dostęp do problemów i rozwiązań, których samodzielne rozpoznanie i zastosowanie kosztowałoby sporo wysiłku. Wzorce zwiększają „dostępność” projektowania. Pojawiają się coraz to nowe katalogi zawierające coraz ściślejsze specjalizacje wzorców, z których czerpać mogą nawet najbardziej doświadczeni programiści, jeśli wkraczają na nowe dla nich pola. Programista wyspecjalizowany w graficznych interfejsach użytkownika ma dzięki nim łatwy i szybki dostęp do problemów oraz rozwiązań powszechnych w programowaniu aplikacji korporacyjnych. Programista aplikacji WWW może zaś dzięki nim szybko zarysować strategie unikania pułapek charakterystycznych dla projektów przeznaczonych dla PDA i telefonów komórkowych.

148

ROZDZIAŁ 7.  CZYM SĄ WZORCE PROJEKTOWE? DO CZEGO SIĘ PRZYDAJĄ?

Wzorce mają współpracować Wzorce powinny być „generatywne” i dać się komponować — zastosowanie jednego wzorca powinno tworzyć warunki do zastosowania innego. Inaczej mówiąc, zastosowanie wzorca wyznacza gotowe ścieżki projektowe. Katalogi wzorców są zwykle układane pod tym właśnie kątem, a możliwość komponowania poszczególnych wzorców jest zawsze jasno dokumentowana w nich samych.

Wzorce promują prawidła projektowe Wzorce projektowe demonstrują i wdrażają prawidła projektowania obiektowego. Studium wzorców projektowych może więc niejednego w tym zakresie nauczyć. Daje bowiem nową, szerszą perspektywę możliwości wzajemnego łączenia klas i obiektów.

Wzorce są stosowane w popularnych frameworkach Niniejsza książka jest przede wszystkim poświęcona projektowaniu aplikacji od podstaw. Omawiane tu wzorce i zasady powinny umożliwić zaprojektowanie własnego frameworka odpowiedniego do planowanego projektu. Jednak w programowaniu liczy się też skuteczność, a dobrzy programiści są leniwi, więc nie od rzeczy jest korzystanie z gotowych już frameworków, takich jak Zend, CodeIgniter czy Symfony (może być przecież i tak, że otrzymamy w spadku do rozwijania aplikację opartą na którymś z popularnych frameworków). Dogłębne zrozumienie podstawowych wzorców projektowych bardzo ułatwia rozpoznanie i ogarnięcie zasad stosowania interfejsów (API) nowoczesnych frameworków.

Wzorce projektowe a PHP W niniejszym rozdziale mało było o PHP, co jest zresztą o tyle usprawiedliwione, że natura wzorców nie każe klasyfikować ich jako użytecznych jedynie w konkretnym języku programowania. Nie dotyczy to rzecz jasna każdego wzorca. Niektóre z wzorców przewidzianych do stosowania w aplikacjach korporacyjnych nadają się do wdrażania w tych językach, w których proces trwa również pomiędzy żądaniami kierowanymi do serwera. W PHP jest jednak inaczej — to każde żądanie obsługiwane jest osobnym, uruchamianym od nowa skryptem. Oznacza to, że niektóre z wzorców, jeśli dają się w tym języku zaimplementować, wymagają szczególnej staranności i uwagi. Przykładem może być choćby Front Controller (kontroler fasady), który cechuje się znacznym niekiedy czasem inicjalizacji. Nie ma problemu, jeśli inicjalizacja odbywa się raz, przy uruchamianiu aplikacji — gorzej, jeśli inicjalizacja obciąża każde kolejne żądanie. Nie znaczy to, że wzorzec nie daje się stosować w ogóle — zdarzyło mi się go już skutecznie zaimplementować. Chodzi jednak o to, aby przy okazji omawiania wzorców nie zapominać o uwarunkowaniach charakterystycznych dla PHP. Każdy wzorzec omawiany w książce jest więc rozpatrywany właśnie w kontekście języka PHP. Wcześniej powoływałem się na języki obiektowe. W języku PHP da się programować, całkowicie pomijając definicje wszelkich klas (choć trudno całkowicie uniknąć styczności z obiektami przy stosowaniu cudzych bibliotek i frameworków). Choć niniejsza książka traktuje niemal w całości o obiektowych rozwiązaniach problemów programistycznych, nie zamierzam wcale wszczynać kolejnej wojny o wyższości świąt Wielkiej Nocy nad świętami Bożego Narodzenia. Wzorce można znakomicie wykorzystać w PHP i o takim połączeniu opowiada ta książka. Użyteczność wzorców nie ogranicza się jednak jedynie do obiektowych rozszerzeń PHP. Weźmy choćby repozytorium PEAR, którego pakiety w znakomitej większości opierają się na prawidłach projektowania obiektowego i elegancko wdrażają wzorce projektowe. Nie oznacza to bynajmniej, że pakiety te nie nadają się do stosowania w projektach proceduralnych — przeciwnie, można je w nich z powodzeniem wykorzystywać. Łatwość wykorzystania wynika tu głównie z ukrycia implementacji pakietów PEAR za eleganckimi interfejsami, dzięki którym sprawdzają się one w dowolnego rodzaju projektach.

149

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Podsumowanie W rozdziale Czytelnik miał okazję zapoznać się z wzorcami projektowymi, poznać ich strukturę (wedle formatu proponowanego przez Bandę Czworga), a także przesłanki, dla których stosowanie wzorców projektowych we własnych skryptach warto uznać za dobry pomysł. Ważne, aby zapamiętać, że wzorce projektowe nie są w żadnej mierze gotowymi do zastosowania receptami, które można jak klocki składać, konstruując z nich całe projekty. Są raczej sugerowanymi sposobami postrzegania powszechnie obserwowanych problemów. Proponowane w ich ramach rozwiązania wcielają w życie kluczowe prawidła projektowania. I to właśnie ten sposób postrzegania problemów i te prawidła będą omawiane w następnym rozdziale.

150

ROZDZIAŁ 8 

Wybrane prawidła wzorców

Wzorce projektowe zawierają opisy rozwiązań wybranych problemów; tak się składa, że proponowane rozwiązania promują elastyczność i przydatność kodu do ponownego wykorzystania. A tym samym przejawiają niektóre kluczowe zasady projektowania obiektowego. Niektóre z nich spróbujemy rozpoznać w niniejszym rozdziale, odkładając szczegółowe omówienie do rozdziałów następnych. W rozdziale omawiam:  Kompozycję — jak agregować obiekty celem uzyskania elastyczności nieosiągalnej przy samym dziedziczeniu.  Rozprzęganie — jak redukować zależności pomiędzy elementami systemu.  Potencję dziedziczenia — czyli wzorce i polimorfizm.  Kategorie wzorców — typy wzorców omawianych w dalszej części książki.

Olśnienie wzorcami Jako programista po raz pierwszy zetknąłem się z obiektami w języku Java. Jak można się spodziewać, przyswojenie niektórych koncepcji obiektowych zajęło mi trochę czasu. Kiedy jednak wreszcie zacząłem pojmować całość, zrozumienie przyszło szybko — prawie jak olśnienie. Uległem bez reszty elegancji dziedziczenia i hermetyzacji. Wyczuwałem, że dają one zupełnie nowe możliwości konstruowania systemów. „Załapałem” też polimorfizm, czyli operowanie na typie z przełączaniem implementacji w czasie wykonania. Zdawało mi się, że to nowe podejście pozwoli mi łatwiej rozwiązać większość problemów projektowych w moich systemach, a także pomoże mi konstruować bardziej eleganckie oprogramowanie. Wszystkie książki na półkach mojej biblioteczki skupiały się wtedy na cechach i elementach języka oraz rozmaitych interfejsach dostępnych programiście języka Java. Nie podejmowano w nich prób analizy strategii projektowych — najwyżej dawano krótką definicję polimorfizmu. Jednak same obiektowe elementy języka nie narzucają programiście obiektowego projektowania. Tak więc, choć moje projekty spełniały stawiane przed nimi wymagania, jakoś rozmijały się z tym, co oferuje dziedziczenie, hermetyzacja i polimorfizm, jeśli je połączyć w odpowiedni sposób. Moje hierarchie dziedziczenia rozrastały się wszerz i w głąb, bo starałem się każdą ewentualność ująć w nową klasę. Struktura moich systemów utrudniała przenoszenie komunikatów pomiędzy warstwami bez udostępniania pośredniczącej w ich przekazywaniu klasie zbyt obszernych informacji o otoczeniu, co oczywiście wiązało te klasy z konkretną aplikacją i czyniło je bezużytecznymi w innych kontekstach.

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Trwało to dopóty, dopóki nie odkryłem Design Patterns1, znanej też jako książka Bandy Czworga — po jej lekturze zorientowałem się, że omijałem dotąd cały wymiar projektowania. Sam już do tego czasu odkryłem na własny użytek szereg podstawowych wzorców, ale lektura ukierunkowała mnie na całkiem nową drogę myślenia o projektach. Przekonałem się, że w swoich projektach zbyt wielki nacisk kładłem na dziedziczenie, próbując wbudowywać w swoje klasy zbyt obszerny zakres funkcji. Ale gdzież w systemie obiektowym jest miejsce funkcjonalności, jeśli nie w klasach? Odpowiedź znalazłem w kompozycji. Kompozycja obiektów w ramach elastycznych relacji pozwala na definiowanie komponentów programowych w czasie wykonania. Zasada ta została w książce Bandy Czworga wyartykułowana hasłem: „Faworyzuj kompozycję przed dziedziczeniem”. Wzorce opisywały sposób, w jaki można w czasie wykonania komponować obiekty, zyskując elastyczność nieosiągalną w drzewie dziedziczenia.

Kompozycja i dziedziczenie Dziedziczenie to efektywny sposób projektowania pod kątem zmiennych okoliczności i kontekstów. Może jednak ograniczać elastyczność, zwłaszcza jeśli klasy obciążane są wieloraką odpowiedzialnością.

Problem Wiemy już, że klasy pochodne dziedziczą metody i składowe klas bazowych (ale tylko składowe oraz metody publiczne i chronione). Fakt ten wykorzystujemy w projektowaniu klas pochodnych realizujących zadania specjalizowane względem zadań klas bazowych. Rysunek 8.1 prezentuje prosty przykład dziedziczenia opisanego w języku UML.

Rysunek 8.1. Klasa bazowa i dwie klasy pochodne Klasa abstrakcyjna Lesson (lekcja) z rysunku 8.1 modeluje lekcje odbywające się w college’u. Definiuje abstrakcyjne metody cost() (koszt) i chargeType() (sposób rozliczania). Diagram prezentuje dwie klasy implementujące abstrakcję z różnymi mechanizmami rozliczania kosztów uczestnictwa w lekcji: FixedPriceLesson (lekcja rozliczana ze stałą stawką) i TimedPriceLesson (lekcja rozliczana ze stawką godzinową). Bazując na takim schemacie dziedziczenia, możemy przełączać się pomiędzy implementacjami „lekcji”. Kod użytkujący hierarchię może operować na obiektach typu Lesson, a szczegóły implementacji rozliczania lekcji będą dla niego transparentne.

1

Wydanie polskie: Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku, Helion, 2010 r. — przyp. tłum.

152

ROZDZIAŁ 8.  WYBRANE PRAWIDŁA WZORCÓW

Co jednak będzie, kiedy wprowadzimy nowy zestaw specjalizacji? Powiedzmy, że mamy rozdzielić lekcje na wykłady i seminaria. Wymagają one osobnych klas z racji różnic w sposobie rekrutacji i konspektów. Otrzymamy jednak w jednej hierarchii dwie osie specjalizacji. Trzeba bowiem będzie w ramach tej hierarchii obsługiwać równocześnie strategie wyceny i podział na seminaria i wykłady. Pierwszą, „siłową” wersję rozwiązania ilustruje rysunek 8.2.

Rysunek 8.2. Nowa, nieprzemyślana struktura dziedziczenia Hierarchia widoczna na rysunku 8.2 jest w oczywisty sposób ułomna. Nie możemy już zarządzać mechanizmem rozliczania kosztów bez duplikowania całych obszarów funkcjonalności. Strategie rozliczania są teraz bowiem zdublowane w podgałęziach Lecture (wykłady) i Seminar (seminaria). W takim układzie możemy rozważać usunięcie nieszczęsnej duplikacji i zastosowanie w klasie bazowej Lesson instrukcji warunkowych. Zasadniczo rzecz ujmując, przesuwając logikę rozliczania do klasy bazowej, usuwamy ją z drzewa dziedziczenia. Mamy więc do czynienia z odwrotnością typowej refaktoryzacji, w ramach której polimorfizm eliminuje instrukcje warunkowe. Oto poprawiona klasa Lesson: abstract class Lesson { protected $duration; const FIXED = 1; const TIMED = 2; private $costtype; function __construct($duration, $costtype = 1) { $this->duration = $duration; $this->costtype = $costtype; } function cost() { switch($this->costtype) { case self::TIMED : return (5 * $this->duration); break; case self::FIXED : return 30; break; default : $this->costtype = self::FIXED; return 30; }

153

PHP. OBIEKTY, WZORCE, NARZĘDZIA

} function chargeType() { switch($this->costtype) { case self::TIMED : return "stawka godzinowa"; break; case self::FIXED : return "stawka stała"; break; default : $this->costtype = self::FIXED; return "stawka stała"; } } // pozostałe metody… } class Lecture extends Lesson { // implementacja właściwa dla wykładów… } class Seminar extends Lesson { // implementacja właściwa dla seminariów… }

Takim zestawem klas można operować na przykład tak: $lecture = new Lecture(5, Lesson::FIXED); print "{$lecture->cost()} ({$lecture->chargeType()})\n"; $seminar= new Seminar(3, Lesson::TIMED); print "{$seminar->cost()} ({$seminar->chargeType()})\n";

A oto wynik wykonania kodu: 30 (stawka stała) 15 (stawka godzinowa)

Nowy diagram klas widnieje na rysunku 8.3.

Rysunek 8.3. Hierarchia dziedziczenia ulepszona przez usunięcie z podklas rozliczania kosztów Struktura klas daje się teraz łatwiej zarządzać, ale nie za darmo. Konieczność zaszycia w hierarchii rozgałęzień warunkowych to jakby krok wstecz. Zazwyczaj przecież postępujemy odwrotnie, zastępując instrukcje warunkowe polimorfizmem. Jak widać, zmusiło to nas od razu do powielenia kodu — duplikaty instrukcji warunkowych mamy teraz w metodach cost() i chargeType(). Zdaje się, że jesteśmy skazani na tę duplikację.

154

ROZDZIAŁ 8.  WYBRANE PRAWIDŁA WZORCÓW

Zastosowanie kompozycji Sposobem na wyjście z impasu jest zastosowanie wzorca projektowego o nazwie Strategy (strategia). Wzorzec strategii zakłada przeniesienie zestawu algorytmów do odrębnego typu. Przeniesienie kalkulacji i rozliczenia kosztu lekcji pozwoli w oczywisty sposób uprościć typ Lesson — spójrzmy na rysunek 8.4.

Rysunek 8.4. Wyodrębnienie algorytmów do oddzielnego typu Tworzymy więc klasę abstrakcyjną o nazwie CostStrategy, definiującą abstrakcyjne metody cost() i chargeType(). Metoda cost() wymaga przekazania w wywołaniu egzemplarza klasy Lesson i na jego podstawie generuje dane o koszcie. Uwzględniamy dwie implementacje abstrakcji CostStrategy. Obiekty klasy Lesson współdziałają wyłącznie z typem CostStrategy, nie zaś z jego konkretnymi implementacjami; możemy dzięki temu dodawać w dowolnej chwili nowe algorytmy rozliczania kosztu, tworząc kolejne pochodne CostStrategy. Nie zmieni to nijak żadnej z klas hierarchii Lesson. Oto uproszczona wersja nowej klasy Lesson, wedle diagramu z rysunku 8.4: abstract class Lesson { private $duration; private $costStrategy; function __construct($duration, CostStrategy $strategy) { $this->duration = $duration; $this->costStrategy = $strategy; } function cost() { return $this->costStrategy->cost($this); } function chargeType() { return $this->costStrategy->chargeType(); } function getDuration() { return $this->duration; } // pozostałe metody klasy Lesson… } class Lecture extends Lesson { // implementacja właściwa dla wykładów...

155

PHP. OBIEKTY, WZORCE, NARZĘDZIA

} class Seminar extends Lesson { // implementacja odpowiednia dla seminariów... }

Utworzenie obiektu klasy Lesson wymaga teraz obiektu CostStrategy, którego referencję obiekt przechowuje w jednej ze składowych. Metoda Lesson::cost() ogranicza swoje działanie do wywołania metody CostStrategy::cost(). Podobnie jest w przypadku metody Lesson::chargeType(), która wywołuje po prostu metodę CostStrategy::chargeType(). Tego rodzaju jawne wywołanie metody innego obiektu celem realizacji żądania nosi nazwę delegacji. W naszym przykładzie obiekt CostStrategy jest więc delegatem klasy Lesson. Klasa Lesson „umywa ręce” od odpowiedzialności za obliczenie kosztu lekcji i składa ją na barki implementacji klasy CostStrategy. Oto przebieg delegowania: function cost() { return $this->costStrategy->cost($this); }

A oto klasa CostStrategy wraz z jej klasami pochodnymi: abstract class CostStrategy { abstract function cost(Lesson $lesson); abstract function chargeType(); } class TimedCostStrategy extends CostStrategy { function cost(Lesson $lesson) { return ($lesson->getDuration() * 5); } function chargeType() { return "stawka godzinowa"; } } class FixedCostStrategy extends CostStrategy { function cost(Lesson $lesson) { return 30; } function chargeType() { return "stawka stała"; } }

Zmiana sposobu rozliczania lekcji odbywa się w obiekcie klasy Lesson przez wybór w czasie wykonania innego niż domyślny obiektu hierarchii CostStrategy. Takie zastosowanie strategii znakomicie zwiększa elastyczność kodu. Zamiast bowiem konstruować konkretną funkcjonalność kodu statycznie, możemy kombinować obiekty dynamicznie (w czasie wykonania programu). $lessons[] = new Seminar(4, new TimedCostStrategy()); $lessons[] = new Seminar(4, new FixedCostStrategy()); foreach ($lessons as $lesson) { print "Koszt lekcji: {$lesson->cost()}."; print "Sposób rozliczania: {$lesson->chargeType()}.\n"; } Koszt lekcji: 20. Sposób rozliczania: stawka godzinowa. Koszt lekcji: 30. Sposób rozliczania: stawka stała.

156

ROZDZIAŁ 8.  WYBRANE PRAWIDŁA WZORCÓW

Jak widać, jednym z efektów przyjętego rozwiązania jest możliwość zawężenia odpowiedzialności klas do ich właściwych zadań. Obiekty klas Lesson zarządzają danymi lekcji, za rozliczanie zaś odpowiedzialne są obiekty klas CostStrategy. Kompozycja czyni kod bardziej elastycznym, umożliwiając dynamiczne kombinowanie obiektów w konfiguracje odpowiednie do wykonania zadania, przy czym swoboda konfiguracji znacznie przewyższa tę osiąganą w nawet najbardziej przemyślanych hierarchiach opartych wyłącznie na dziedziczeniu. Ponieważ jednak kompozycja owocuje powstawaniem nowych typów, których wzajemne relacje nie są ustalone z przewidywalnością i trwałością charakterystyczną dla relacji dziedziczenia, może odrobinę utrudniać identyfikację relacji w systemie.

Rozprzęganie W rozdziale 6. przekonaliśmy się, że warto konstruować system z maksymalnie niezależnych komponentów. System zawierający klasy o wysokim stopniu wzajemnego powiązania jest bowiem trudny w utrzymaniu i konserwacji. Zmiana w jednym miejscu może w nim prowokować kaskadę zmian w obrębie całego systemu.

Problem Zdatność poszczególnych komponentów do wielokrotnego wykorzystania to jedna z głównych trosk i podstawowy cel projektowania obiektowego. Ścisłe sprzęganie komponentów w oczywisty sposób uniemożliwia jego osiągnięcie. Ścisłe sprzęganie objawia się tym, że zmiana jednego z komponentów systemu prowokuje znaczną liczbę zmian w pozostałych jego komponentach. Należy więc dążyć do tworzenia niezależnych komponentów, w których można dokonywać modyfikacji niewymagających potem przerabiania połowy systemu. Zmiana komponentu ujawnia jego niezależność: im większa niezależność komponentu, tym mniejsze prawdopodobieństwo załamania i wymuszenia przepisania pozostałych elementów systemu. Przykład ścisłego sprzęgania widać na rysunku 8.2. Włączenie logiki rozliczania kosztu do typów Lecture i Seminar powoduje, że zmiana klasy TimedPriceLecture wymusza równoległą zmianę w TimedPriceSeminar. Aktualizacja jednej z klas i pominięcie aktualizacji drugiej naruszy zaś spójność systemu, oczywiście przy braku jakiegokolwiek ostrzeżenia ze strony PHP. Pierwsza propozycja rozwiązania, polegająca na zastosowaniu instrukcji warunkowych w klasie bazowej, ustanowiła podobną zależność, choć o nieco mniejszym zasięgu — zależność pomiędzy metodami cost() i chargeType(). Wdrażając wzorzec Strategy, wyodrębniliśmy algorytmy rozliczania do typu CostStrategy, definiując dla niego jednolity interfejs i skryte za nim implementacje. Innego rodzaju sprzęganie obserwujemy, kiedy znaczna ilość klas systemu zostanie wkomponowana jawnie w platformę czy środowisko. Powiedzmy, że konstruujemy system komunikujący się z bazą danych MySQL. Z serwerem bazy danych można porozumiewać się za pośrednictwem metod i funkcji takich jak mysqli::query(). Jeśli okaże się, że cały system trzeba wdrożyć na serwerze pozbawionym dostępu do bazy MySQL, można całość przystosować do obsługi choćby SQLite. Wymusi to co prawda rozległe zmiany w kodzie, pojawi się też problem utrzymania i synchronizacji dwóch już równoległych gałęzi projektu. Problemem nie jest w takim układzie zależność systemu od platformy zewnętrznej. Taka zależność jest przecież koniecznością. System musi się bowiem w jakiś sposób komunikować z jakimś serwerem baz danych. Problem wynika jedynie z rozproszenia kodu realizującego tę komunikację w całym projekcie. Komunikacja z bazą danych nie jest głównym zadaniem zdecydowanej większości klas systemu, najlepiej byłoby więc wyodrębnić kod owej komunikacji i scalić go w jednym miejscu, udostępniając dla niego spójny interfejs. W ten sposób promujemy niezależność klas systemu; równocześnie, koncentrując kod „bramki” do bazy danych w jednym miejscu, znacznie ułatwiamy przenosiny na inną platformę — koncentracja kodu „bramki” będzie też oznaczać koncentrację i ograniczenie zakresu wymaganych zmian. Proces ukrywania implementacji za dobrze określonym interfejsem nazwiemy hermetyzacją (ang. encapsulation). Analogiczny problem doczekał się rozwiązania w pakiecie PEAR::MDB2 (następcy pakietu PEAR::DB). Stanowi on wspólny, pojedynczy punkt wejścia do wielu baz danych. Ostatnio model ten został zapożyczony wprost do języka PHP za pośrednictwem rozszerzenia PDO (PHP Data Object).

157

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Klasa MDB2 udostępnia statyczną metodę o nazwie connect(), przyjmującą ciąg nazwy źródła danych (DSN, od Data Source Name). Zależnie od zawartości ciągu metoda zwraca obiekt odpowiedniej implementacji klasy o nazwie MDB2_Driver_Common. Tak więc dla ciągu "mysql://" metoda connect() zwróci obiekt klasy MDB2_Driver_mysql, a dla ciągu zaczynającego się od "sqlite://" otrzymamy obiekt klasy MDB2_Driver_sqlite. Omawianą strukturę klas ilustruje rysunek 8.5.

Rysunek 8.5. W pakiecie PEAR::MDB2 oddzielono kod kliencki od obiektów bazy danych Pakiet PEAR::MDB2 pozwala więc na odizolowanie aplikacji od szczegółów implementacji i dostępu do bazy danych. O ile w kodzie klienckim będziemy posługiwać się możliwie „koszerną” składnią SQL, o tyle będziemy mogli uruchamiać jeden i ten sam system na bazach danych MySQL, SQLite, MSSQL i wielu innych, a jedyna zmiana w programie będzie dotyczyć ciągu DSN konfigurującego pakiet MDB2. Co więcej, pakiet PEAR::MDB2 jest wielką pomocą przy godzeniu rozmaitych dialektów SQL — to jeszcze jeden powód, dla którego warto go stosować (kolejnymi powodami będą szybkość i wygoda stosowania obiektów PDO).

Osłabianie sprzężenia Aby elegancko zrealizować kod dostępu do danych, trzeba oddzielić logikę aplikacji od specyfiki dostępu do bazy danych wykorzystywanej w danym systemie. To jeden z częstszych przykładów rozdzielania i osłabiania sprzęgu pomiędzy komponentami projektów. Załóżmy dla przykładu, że system Lesson musi obejmować komponent rejestracji, odpowiedzialny za dodawanie nowych zajęć do systemu. W ramach procedury rejestracji powinno nastąpić powiadomienie administratora o nowych zajęciach. Użytkownicy systemu nie potrafią uzgodnić, czy to powiadomienie powinno być wysłane pocztą elektroniczną, czy może mieć postać komunikatu tekstowego. Z dyskusji wynika, że w przyszłości być może w ogóle zajdzie potrzeba stosowania innego systemu komunikacji. Co więcej, użytkownicy chcą być powiadamiani również o innych zdarzeniach w systemie. Zmiana trybu powiadamiania w jednym miejscu może więc oznaczać podobną zmianę w wielu innych miejscach systemu. Jeśli w systemie znajdują się jawne odwołania do klasy Mailer czy klasy Texter, powiemy, że system jest ściśle sprzężony z konkretnym trybem powiadamiania. Tak samo, jak powiedzielibyśmy o ścisłym sprzęgu z konkretną implementacją źródła danych w przypadku wykorzystania w systemie konkretnego, specjalizowanego interfejsu dostępu do bazy danych. Oto kod ukrywający szczegóły implementacji mechanizmu powiadomień i izolujący tę implementację od pozostałych komponentów systemu. class RegistrationMgr { function register(Lesson $lesson) { // jakieś operacje na obiekcie Lesson // i odpowiednie powiadomienie $notifier = Notifier::getNotifier(); $notifier->inform( "nowe zajęcia: koszt ({$lesson->cost()})" ); } } abstract class Notifier { static function getNotifier() { // pozyskanie konkretnej klasy odpowiedniej dla // konfiguracji bądź stanu logiki if ( rand(1,2) == 1 ) {

158

ROZDZIAŁ 8.  WYBRANE PRAWIDŁA WZORCÓW

return new MailNotifier(); } else { return new TextNotifier(); } } abstract function inform($message); } class MailNotifier extends Notifier { function inform($message) { print "powiadomienie w trybie MAIL: {$message}\n"; } } class TextNotifier extends Notifier { function inform($message) { print "powiadomienie w trybie TEXT: {$message}\n"; } }

Posiłkujemy się tu klasą RegistrationMgr — prostym klientem klasy Notifier. Klasa Notifier jest klasą abstrakcyjną, ale implementuje statyczną metodę getNotifier(), która pozyskuje i zwraca obiekt odpowiedniej klasy (TextNotifier lub MailNotifier). W prawdziwym projekcie wybór klasy Notifier odbywałby się zapewne na bazie odpowiednio elastycznego mechanizmu sterowanego np. plikiem konfiguracyjnym. Tutaj nieco oszukaliśmy i wybieramy implementację powiadamiania losowo. Klasy MailNotifier i TextNotifier nie robią niczego poza wypisaniem przekazanego komunikatu wraz z identyfikatorem sygnalizującym aktywny tryb powiadamiania. Zauważmy, że wiedza o tym, którą specjalizację klasy Notifier należy wybrać, została w całości skoncentrowana w metodzie Notifier::getNotifier(). Można więc niniejszym wysyłać powiadomienia z setek różnych komponentów systemu, ale ewentualna zmiana trybu powiadamiania ogranicza się do zmiany w tej jednej metodzie statycznej. Oto kod wykorzystujący klasę RegistrationMgr: $lessons1 = new Seminar(4, new TimedCostStrategy()); $lessons2 = new Lecture(4, new FixedCostStrategy()); $mgr = new RegistrationMgr(); $mgr->register($lessons1); $mgr->register($lessons2);

oraz wynik jego wykonania: powiadomienie w trybie TEXT: nowe zajęcia: koszt (20) powiadomienie w trybie MAIL: nowe zajęcia: koszt (30)

Użyte klasy ilustruje rysunek 8.6.

Rysunek 8.6. Klasa Notifier oddziela kod kliencki od implementacji trybu powiadamiania Zauważmy podobieństwo struktury z rysunku 8.6 do struktury reprezentującej komponenty pakietu MDB2 (z rysunku 8.5). 159

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Kod ma używać interfejsów, nie implementacji Ta zasada to jeden z motywów przewodnich tej książki. W rozdziale 6. (i ostatnim podrozdziale) przekonaliśmy się, jak skuteczne jest ukrywanie rozmaitych implementacji za wspólnym dla nich interfejsem definiowanym w klasie bazowej hierarchii. Użytkownik hierarchii może wtedy wymagać obiektu typu bazowego i korzystać z jego interfejsu, nie zajmując się szczegółami implementacji zdefiniowanymi w faktycznej klasie obiektu. Równoległe instrukcje warunkowe (jak te z Lesson::cost() i Lesson::chargeType()) to częsty symptom konieczności zastosowania polimorfizmu. Rozproszenie warunków i decyzji utrudnia utrzymanie i konserwację kodu, bo zmiana w jednym z wyrażeń warunkowych musi zostać odzwierciedlona w wyrażeniach siostrzanych, pod groźbą utraty spójności systemu. Dlatego o instrukcjach warunkowych występujących w takim układzie mówi się niekiedy jako o „dziedziczeniu symulowanym”. Wyodrębniając algorytmy rozliczania zajęć do osobnych klas implementujących klasę abstrakcyjną CostStrategy, eliminujemy powielanie instrukcji warunkowych. Ułatwiamy sobie tym samym przyszłe zmiany strategii rozliczania. Z perspektywy użytkowników wskazane byłoby wymuszanie w listach parametrów metod jak najbardziej ogólnych czy wręcz abstrakcyjnych typów. Im konkretniejszy typ, tym ściślejsze ograniczenie elastyczności kodu w czasie wykonania. To powiedziawszy, trzeba stwierdzić, że dobór poziomu ogólności typów wymuszanych w wywołaniach metod pozostaje w gestii programisty. Jeśli wybierze typ zbyt ogólny, ucierpieć może bezpieczeństwo metody. Jeśli bowiem we wnętrzu metody wymagana jest obecność funkcji i cech charakterystycznych dla pewnego podtypu specjalizującego, przekazanie w wywołaniu specjalizacji pochodzącej z innej gałęzi hierarchii dziedziczenia może uniemożliwić realizację zadań metody. Z drugiej strony, zbytnie ograniczenie wyboru klasy argumentu znosi korzyści wynikające z polimorfizmu. Spójrzmy na przykład na poniższy (zmodyfikowany) fragment klasy Lesson: function __construct($duration, FixedPriceStrategy $strategy) { $this->duration = $duration; $this->costStrategy = $strategy; }

Decyzja projektowa odzwierciedlona w powyższym kodzie ma dwie konsekwencje. Po pierwsze, obiekt klasy Lesson jest tutaj wiązany z konkretną strategią rozliczania, co blokuje możliwość dynamicznego zestawiania komponentów. Po drugie zaś, jawne odwołanie do klasy FixedPriceStrategy zmusza nas do utrzymywania

w hierarchii strategii rozliczania tej konkretnej implementacji. Dla porównania, narzucając jedynie wspólny interfejs, można kombinować obiekty Lesson z wybranymi implementacjami CostStrategy: function __construct($duration, CostStrategy $strategy) { $this->duration = $duration; $this->costStrategy = $strategy; }

Obserwujemy tu zniesienie sprzęgania klasy Lesson i szczegółów rozliczania kosztów zajęć. Liczy się tylko interfejs i gwarancja, że przekazany obiekt będzie ów interfejs honorował. Oczywiście ukierunkowanie na interfejs rodzi niejednokrotnie pytanie o sposób konkretyzacji obiektu. Mówiąc, że obiekt klasy Lesson można dynamicznie (w czasie wykonania programu) kombinować z dowolną implementacją CostStrategy, stawiamy równocześnie pytanie: „Ale skąd wziąć odpowiedni obiekt CostStrategy?”. Definiowanie abstrakcyjnej klasy bazowej zawsze rodzi pytanie o sposób konkretyzacji obiektów jej klas pochodnych. Jak zdecydować o wyborze pochodnej do konkretyzacji? Pytanie to leży u podstaw całej kategorii wzorców w katalogu Bandy Czworga. Przyjrzymy się im w następnym rozdziale.

160

ROZDZIAŁ 8.  WYBRANE PRAWIDŁA WZORCÓW

Zmienne koncepcje Łatwo interpretować już podjęte decyzje, jak jednak zdecydować, od czego zacząć? Banda Czworga zaleca, aby „zmienne koncepcje hermetyzować”. W kontekście naszego przykładu z rozliczaniem zajęć koncepcją zmienną jest algorytm rozliczania. Zmienność nie kończy się na dwóch już zaimplementowanych w przykładzie strategiach — mamy tu oczywistą możliwość ekspansji: oferty specjalne, stawki dla wolnych słuchaczy, studentów z wymiany międzynarodowej i tak dalej. Szybko ustaliliśmy, że wyprowadzanie pochodnych klasy Lesson nie jest odpowiednie, i uciekliśmy się do instrukcji warunkowych. Jednak gromadząc ową potencjalną zmienność w jednej klasie, uwypukliliśmy jedynie możliwość hermetyzacji. Banda Czworga zaleca aktywne poszukiwanie w klasach zmiennych koncepcji i szacowanie ich zdatności do konstytuowania osobnego typu. Każda z alternatyw w podejrzanej instrukcji warunkowej może zostać wyodrębniona do osobnej klasy rozszerzającej wspólną abstrakcyjną klasę nadrzędną. Taki nowy typ może być następnie wykorzystywany w klasie (czy klasach), z której (których) został wyodrębniony. Całość daje efekt:  zawężenia odpowiedzialności,  promowania elastyczności (przez kompozycję),  zwiększania zwartości i skupienia hierarchii dziedziczenia,  redukcji powielania kodu. Jak wyśledzić zmienne koncepcje? Choćby w nadużyciach dziedziczenia. Jeśli w hierarchii dziedziczenia mamy do czynienia z więcej niż jednym kryterium specjalizacji (jak w naszym przykładzie, kiedy w jednej hierarchii próbowaliśmy pogodzić podział na seminaria oraz lektury i równocześnie podział wedle sposobów rozliczania). Podejrzane są specjalizacje oparte na algorytmie pobocznym względem podstawowej odpowiedzialności danego typu. Innym symptomem zmienności kandydującej do hermetyzacji jest wyrażenie warunkowe.

Nadmiar wzorców Jedynym problemem, którego nie identyfikuje się w żadnym z wzorców projektowych, jest niepotrzebne bądź niewłaściwe stosowanie wzorców. To właśnie tego rodzaju nadużycia są przyczyną złej sławy, jaką wzorce cieszą się w niektórych kręgach. Rozwiązania proponowane w ramach wzorców są atrakcyjne oraz eleganckie i trudno oprzeć się pokusie ich stosowania gdzie popadnie — bez zastanowienia nad faktyczną przydatnością wzorca w danym miejscu. Metodologia eXtreme Programming (XP) obejmuje kilka zasad, które można tu zastosować. Pierwsza z nich brzmi: „Nie będziesz tego potrzebował”. Odnosi się ona głównie do mnożenia funkcji aplikacji, ale ma podobny sens również w odniesieniu do nadmiaru wzorców. Kiedy konstruuję w języku PHP większe środowiska, zazwyczaj dzielę aplikację na warstwy, starając się oddzielić logikę aplikacji od warstw prezentacji i utrwalania danych. Korzystam przy tym z wszelkich znanych mi wzorców, tak podstawowych, jak i przeznaczonych do środowisk korporacyjnych, łącząc je ze sobą. Kiedy jednak staję w obliczu zadania zbudowania formularza zwrotnego (ang. feedback form) dla witryny WWW obsługującej niewielką firmę, często ograniczam się do technik wyłącznie proceduralnych, zwłaszcza jeśli kod zawiera się w jednym skrypcie. Nie potrzebuję wtedy niezwykłej elastyczności, bo i nie przewiduję rozwoju raz zakończonego projektu. Nie potrzebuję wzorców identyfikujących problemy charakterystyczne dla większych systemów. Ograniczam się do stosowania drugiej zasady XP — „Wykonaj najprostszą rzecz, która zadziała”. Katalog wzorców pozwala uzmysłowić sobie strukturę problemu i proces rozwiązania oraz skonsolidować oba próbkami kodu przykładowego. Przed wdrożeniem wzorca trzeba jednak koniecznie zajrzeć do części „Problem” oraz „Zakres zastosowań”; nie wolno też pominąć części opisującej konsekwencje wdrożenia wzorca. Inaczej lekarstwo może okazać się gorsze od choroby.

Wzorce Niniejsza książka nie ma być katalogiem wzorców. Mimo tego w następnych kilku rozdziałach zaprezentuję niektóre z najważniejszych wzorców, jakie są obecnie w użyciu, uzupełniając prezentację implementacjami wzorców w języku PHP i ich omówieniem właśnie w kontekście programowania w tym języku.

161

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Opisywane wzorce zaczerpnąłem z najważniejszych i uznanych katalogów: Design Patterns, Patterns of Enterprise Application Architecture2 Martina Fowlera (Addison-Wesley, 2003) oraz Core J2EE Patterns3 Alura i innych (Prentice Hall PTR, 2001). Opierałem się przede wszystkim na kategoryzacji zaczerpniętej z klasycznej książki Bandy Czworga.

Wzorce generowania obiektów Wzorce dotyczące problematyki konkretyzacji obiektów to bardzo ważna kategoria, zwłaszcza w kontekście ukierunkowania na korzystanie z interfejsów. Jeśli opieramy projekt na abstrakcyjnych klasach bazowych, musimy opracować strategie konkretyzacji obiektów ich klas pochodnych. To przecież właśnie te obiekty będą przekazywane pomiędzy elementami systemu.

Wzorce organizacji obiektów i klas Te wzorce projektowe są pomocne w organizowaniu kompozycyjnych relacji pomiędzy obiektami. Ujmując rzecz w uproszczeniu, wzorce te pokazują, jak tworzyć kombinacje obiektów i klas.

Wzorce zadaniowe Wzorce te opisują mechanizmy, za pośrednictwem których klasy i obiekty współpracują w osiąganiu celu.

Wzorce korporacyjne Do tej kategorii zaliczymy niektóre z wzorców opisujących typowe problemy (i ich rozwiązania) w programowaniu aplikacji internetowych. Wzorce te, zaczerpnięte głównie z Patterns of Enterprise Application Architecture i Core J2EE Patterns4, odnoszą się do różnych elementów warstw logiki aplikacji, prezentacji i utrwalania danych (warstwy bazy danych).

Wzorce baz danych Są to wzorce pomocne przy składowaniu i pozyskiwaniu danych z baz danych oraz przy odwzorowywaniu obiektów na relacje baz danych

Podsumowanie W rozdziale przyjrzeliśmy się kilku podstawowym zasadom stanowiącym podszewkę wielu wzorców projektowych. Czytelnicy poznali sposoby wykorzystania kompozycji celem umożliwienia kombinacji i rekombinacji, dających elastyczność nieporównywalną z tą, którą można by osiągnąć za pośrednictwem dziedziczenia. Dowiedzieli się o rozluźnianiu sprzęgania, wyodrębnianiu komponentów programowych z ich kontekstu gwoli uogólnienia ich stosowalności. Uwidoczniliśmy też znaczenie interfejsu jako środka redukowania zależności pomiędzy użytkownikami a szczegółami implementacji. W następnych rozdziałach zajmiemy się już konkretnymi wzorcami projektowymi.

2 3 4

Wydanie polskie: Architektura zarządzania przedsiębiorstwem. Wzorce projektowe, Helion, 2005 r. — przyp. tłum. Wydanie polskie: J2EE. Wzorce projektowe. Wydanie 2, Helion, 2004 — przyp. tłum. Ibid.

162

ROZDZIAŁ 9 

Generowanie obiektów

Tworzenie obiektów nie jest sprawą łatwą. W wielu elementach projektów występują eleganckie klasy abstrakcyjne dające imponującą elastyczność wynikającą z polimorfizmu (przełączania implementacji w czasie wykonania), jednak cała ta elastyczność wymaga opracowania strategii generowania obiektów. Będą one tematem omówienia w niniejszym rozdziale. Rozdział będzie traktował o wzorcach:  Singleton — a więc o specjalnej klasie generującej jedyny w systemie egzemplarz obiektu.  Factory Method — czyli o konstruowaniu hierarchii dziedziczenia klas-generatorów.  Abstract Factory — a więc o konsolidacji tworzenia produktów powiązanych.  Prototype — czyli o generowaniu obiektów przez klonowanie.

Generowanie obiektów — problemy i rozwiązania Tworzenie obiektów może być słabym ogniwem projektu obiektowego. W poprzednim rozdziale przekonywałem do zasady pierwszeństwa interfejsu przed implementacją, zachęcając do działania na abstrakcyjnych klasach nadrzędnych. Zyskuje bowiem na tym elastyczność kodu, który może dobierać implementacje interfejsu w czasie wykonania. Ma to jednak poważny w skutkach efekt uboczny — opóźnienie konkretyzacji. Oto klasa przyjmująca w wywołaniu konstruktora ciąg nazwy i konkretyzująca pewien obiekt: abstract class Employee { protected $name; function __construct($name) { $this->name = $name; } abstract function fire(); } class Minion extends Employee { function fire() { print "{$this->name}: Spakuję manatki\n"; } } class NastyBoss { private $employees = array();

PHP. OBIEKTY, WZORCE, NARZĘDZIA

function addEmployee($employeeName) { $this->employees[] = new Minion($employeeName); } function projectFails() { if (count($this->employees) >0 ) { $emp = array_pop($this->employees); $emp->fire(); } } } $boss = new NastyBoss(); $boss->addEmployee("Harry"); $boss->addEmployee("Bob"); $boss->addEmployee("Mary"); $boss->projectFails(); Mary: Spakuję manatki

Definiujemy tu abstrakcyjną klasę bazową Employee (pracownik) z dwoma specjalizacjami, w tym specjalizacją reprezentującą uciemiężonych: Minion (pomagier). Obiekty tej klasy są konkretyzowane wywołaniem metody NastyBoss::addEmployee() z ciągiem znaków reprezentujących imię. Kiedy szef zaczyna się denerwować (co jest reprezentowane wywołaniem NastyBoss::projectFails()), szuka wśród obiektów klasy Minion kandydatów do rozwiązania stosunku pracy. Konkretyzując obiekty pracowników w klasie NastyBoss, ograniczamy elastyczność systemu. Gdyby obiekt NastyBoss mógł operować na dowolnych konkretyzacjach typu Employee, można by prezentowany kod tak poprawić, aby rekrutacja obiektów-pracowników realizowana w czasie wykonania programu owocowała bardziej zróżnicowanymi specjalizacjami. Omawianą hierarchię polimorficzną ilustruje rysunek 9.1.

Rysunek 9.1. Operowanie abstrakcyjnym typem umożliwia zachowanie polimorficzne Jeśli jednak klasa NastyBoss nie ma konkretyzować obiektów Minion, to skąd się będą one brały? Niektórzy autorzy zamiatają problem pod dywan, ograniczając typ argumentu wywołania metody — przez co zagadnienie konkretyzacji zostaje wygodnie ograniczone do kontekstu prostego, przykładowego testu: class NastyBoss { private $employees = array(); function addEmployee(Employee $employee) { $this->employees[] = new Minion($employeeName); } function projectFails() { if (count($this->employees)) {

164

ROZDZIAŁ 9.  GENEROWANIE OBIEKTÓW

$emp = array_pop($this->employees); $emp->fire(); } } } // nowa klasa pracownika — pieniacz... class CluedUp extends Employee { function fire() { print "{$this->name}: Zadzwonię do adwokata\n"; } } $boss = new NastyBoss(); $boss->addEmployee(new Minion("Harry")); $boss->addEmployee(new CluedUp("Bob")); $boss->addEmployee(new Minion("Mary")); $boss->projectFails(); $boss->projectFails(); $boss->projectFails(); Mary: Spakuję manatki Bob: Zadzwonię do adwokata Harry: Spakuję manatki

Ta wersja klasy NastyBoss operuje już na typie Employee, czerpiąc korzyści z polimorfizmu, wciąż nie widać jednak strategii tworzenia obiektów. Konkretyzacja obiektów jest tu traktowana po macoszemu, ale przecież ktoś musi to zrobić. Dlatego niniejszy rozdział będzie w całości poświęcony klasom i obiektom operującym na konkretnych klasach i wyręczającym w tym dziele pozostałe klasy systemu. Gdybym miał wskazać zasadę, którą należałoby się tu kierować, powiedziałbym: „Deleguj konkretyzację obiektów”. W poprzednim przykładzie delegacja była co prawda niejawna, bo wyrażona jedynie wymaganiem przekazania do metody addEmployee() klasy NastyBoss gotowego obiektu typu Employee. Moglibyśmy jednak równie dobrze jawnie złożyć ten obowiązek na barki osobnej klasy albo metody, która byłaby odpowiedzialna wyłącznie za generowanie obiektów hierarchii Employee. Dodajmy więc do klasy Employee statyczną metodę implementującą strategię kreacji obiektów: abstract class Employee { protected $name; private static $types = array('minion', 'cluedup', 'wellconnected'); static function recruit($name) { $num = rand(1, count(self::$types)) - 1; $class = self::$types[$num]; return new $class($name); } function __construct($name) { $this->name = $name; } abstract function fire(); } // nowa klasa pracownika — ustosunkowany... class WellConnected extends Employee { function fire() { print "{$this->name}: Poskarżę się ojcu\n"; } }

165

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Jak widać, mamy teraz do dyspozycji metodę przyjmującą ciąg znaków (imię) i wykorzystującą ów ciąg do konkretyzacji obiektu typu Employee, którego podtyp jest jednak dobierany losowo. Teraz możemy delegować szczegóły konkretyzacji do metody recruit() klasy Employee: $boss = new NastyBoss; $boss->addEmployee(Employee::recruit("Harry")); $boss->addEmployee(Employee::recruit("Bob")); $boss->addEmployee(Employee::recruit("Mary"));

Prosty przykład klasy pełniącej funkcję mieliśmy okazję analizować w rozdziale 4. Tam umieściliśmy w klasie ShopProduct statyczną metodę generacji obiektów o nazwie getInstance(). Metoda getInstance() jest odpowiedzialna za wygenerowanie obiektu odpowiedniego podtypu z hierarchii ShopProduct, a decyzję co do wyboru konkretnego typu podejmuje na podstawie zapytania do bazy danych. Metoda ta uczestniczy w definicji klasy ShopProduct, ale równocześnie stanowi „wytwórnię” obiektów konkretnych klas hierarchii ShopProduct. // Klasa ShopProduct public static function getInstance($id, PDO $dbh) { $query = "select * from products where id = ?"; $stmt = $dbh->prepare($query); if (! $stmt->execute(array($id))) { $error = $dbh->errorInfo(); die("błąd: ".$error[1]); } $row = $stmt->fetch(); if (empty($row)) { return null; } if ($row['type'] == "książka") { // konkretyzacja obiektu klasy BookProduct… } else if ($row['type'] == "cd") { $product = new CdProduct(); // konkretyzacja obiektu klasy CdProduct… } else { // konkretyzacja obiektu klasy ShopProduct… } $product->setId($row['id']); $product->setDiscount($row['discount']); return $product; }

 Uwaga Pojęcie „wytwórni” bądź „fabryki” będzie w tym rozdziale wykorzystywane wyjątkowo często. Za wytwórnię uważać będziemy klasę, ewentualnie metodę, której głównym zadaniem jest tworzenie obiektów.

Metoda getInstance() stosowała do określenia właściwego podtypu konkretyzowanego obiektu rozbudowaną instrukcję wyboru. Tego rodzaju instrukcje warunkowe są dla kodu wytwórni dość typowe. Często w projektach próbujemy pozbywać się rozbudowanych konstrukcji warunkowych, a niekiedy skutkuje to jednak przesuwaniem warunków do momentu generowania obiektów. Nie należy traktować tego jak poważnej wady kodu, ponieważ taka koncentracja pozwala przynajmniej na wyeliminowanie z projektu równoległego, powielanego kodu warunkowego. Przyjrzyjmy się niektórym wzorcom z książki Bandy Czworga. Interesować nas będą wzorce kreacji obiektów.

166

ROZDZIAŁ 9.  GENEROWANIE OBIEKTÓW

Wzorzec Singleton W programowaniu obiektowym bezlitośnie tępi się zmienne globalne. Przyczyny ich niepopularności powinny być już oczywiste. Zmienne globalne wiążą klasy z kontekstem, sabotując hermetyzację (patrz rozdział 6. oraz rozdział 8.). Klasy, która odwołuje się do zmiennych globalnych i na nich polega, nie da się w prosty sposób wyciągnąć z aplikacji i zastosować w innej, jeśli wcześniej nie zostanie sprawdzone, czy w nowej aplikacji zdefiniowany jest zestaw odpowiednich zmiennych globalnych. Tego rodzaju zależność jest oczywiście niepożądana, znacznie większym problemem jest jednak niebezpieczeństwo oparcia projektu na zmiennych globalnych, a to z racji ich natury. Jeśli zaczniemy polegać na zmiennych globalnych, to jedynie kwestią czasu będzie kolizja nazw zmiennych z nazwami zmiennych definiowanych w różnych bibliotekach. Wiemy już, że wobec braku przestrzeni nazw PHP i bez zmiennych globalnych jest podatny na kolizje nazw. Co gorsza, PHP nie ostrzeże nas o kolizji. Pierwszym symptomem problemu będzie dziwaczne zachowanie się skryptu. Jeszcze gorzej, jeśli w środowisku deweloperskim kolizja nazw nie objawia się w ogóle. Stosując zmienne globalne, narażamy naszych użytkowników na nieoczekiwane i niekiedy bardzo niebezpieczne kolizje nazw przy próbie wdrożenia naszej biblioteki obok innych komponentów systemu. Zmienne globalne mimo wszystko kuszą, bo niejeden raz jesteśmy skłonni zaakceptować pierworodny grzech dostępu globalnego jako cenę łatwości odwołań do danych z różnych klas i obiektów. Była już mowa o tym, że pewne zabezpieczenie przed ryzykiem kolizji stanowią przestrzenie nazw. Pozwalają przynajmniej na ograniczenie zasięgu zmiennych do pakietu, co oznacza, że biblioteki zewnętrzne raczej nie będą kolidować z nazwami wykorzystywanymi w naszym systemie. Ale mimo to ryzyko kolizji (choć znacznie mniejsze) wciąż istnieje — tyle że w obrębie pojedynczej przestrzeni nazw.

Problem Dobrze zaprojektowane systemy wymieniają dane za pośrednictwem obiektów przekazywanych pomiędzy wywołaniami metod. Każda klasa zachowuje dzięki temu pewną niezależność od szerszego kontekstu, bo z pozostałymi komponentami systemu komunikuje się za pośrednictwem wyraźnych i dobrze zdefiniowanych protokołów. Niekiedy jednak okazuje się, że zmuszeni jesteśmy wprowadzać na potrzeby tej komunikacji dodatkowe klasy pełniące rolę kanałów komunikacyjnych dla obiektów, o których te klasy nic nie wiedzą. Wprowadzamy tym samym — w imię poprawności projektu — dodatkowe zależności. Wyobraźmy sobie klasę Preferences przewidzianą jako kontener informacji działający na poziomie aplikacji. Obiekt klasy Preferences mógłby, na przykład, służyć do przechowywania takich danych jak ciągi DSN (Data Source Name, ciągi reprezentujące „ścieżki” dostępu do baz danych, zawierające informacje o użytkowniku i bazie docelowej), adresy URL, ścieżki dostępu do plików i tym podobne. Tego rodzaju informacje będą oczywiście różnić się pomiędzy instalacjami systemu. Obiekt takiej klasy mógłby też służyć jako swego rodzaju „tablica ogłoszeń”, centralizując wymianę komunikatów pomiędzy takimi obiektami systemu, które poza tym nie są ze sobą nijak powiązane. Przekazywanie obiektu klasy Preferences pomiędzy obiektami nie jest najlepszym pomysłem. Zmusi to wiele klas (które poza tym nie używałyby go w ogóle) do przyjmowania go tylko po to, aby móc przekazać go dalej do obiektów, z którymi współpracują. Powstałyby kolejne zależności. Potrzebna jest nam również pewność, że wszystkie obiekty systemu będą operowały na tym samym obiekcie Preferences. Nie można dopuścić do sytuacji, w których jedne obiekty będą ustawiały jeden obiekt Preferences, a inne odczytywały inny obiekt — taka „tablica ogłoszeń” byłaby zupełnie bezużyteczna. Podsumujmy wyróżnione aspekty problemu:  Obiekt klasy Preferences powinien być dostępny wszystkim obiektom systemu.  Obiekt klasy Preferences nie powinien być przechowywany w zmiennej globalnej, którą da się zamazać.  W systemie nie może istnieć więcej niż jeden obiekt Preferences. Oznacza to, że jeśli obiekt Y ustawi składową obiektu klasy Preferences, to obiekt Z powinien móc odczytać nową wartość tej składowej bez konieczności porozumiewania się z obiektem ustawiającym (o ile oba mają dostęp do obiektu Preferences).

167

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Implementacja Rozwiązywanie problemu możemy rozpocząć od przejęcia kontroli nad konkretyzacją obiektu. Utworzymy więc klasę, której nie da się skonkretyzować spoza niej samej. Brzmi to zawile, ale sprowadza się jedynie do zadeklarowania konstruktora jako metody prywatnej: class Preferences { private $props = array(); private function __construct() {} public function setProperty($key, $val) { $this->props[$key] = $val; } public function getProperty($key) { return $this->props[$key]; } }

Oczywiście na razie klasa Preferences jest zupełnie bezużyteczna. Ograniczenie dostępu konstruktora jest tu wręcz absurdalnie ścisłe. Jeśli bowiem konstruktor jest metodą prywatną, to obiektu klasy nie da się nijak skonkretyzować z poziomu kodu zewnętrznego względem klasy. Metody setProperty() i getProperty() są więc na razie zupełnie zbędne. Usługę pośrednictwa w konkretyzacji obiektu klasy może świadczyć metoda statyczna klasy odwołująca się do pewnej składowej statycznej: class Preferences { private $props = array(); private static $instance; private function __construct() {} public static function getInstance() { if (empty(self::$instance)) { self::$instance = new Preferences(); } return self::$instance; } public function setProperty($key, $val) { $this->props[$key] = $val; } public function getProperty($key) { return $this->props[$key]; } }

Składowa $instance jest prywatna i statyczna, nie można się więc do niej odwoływać spoza klasy. Ma do niej jednak dostęp metoda getInstance(). Ponieważ jest ona metodą publiczną i równocześnie statyczną, może być wywołana z kodu zewnętrznego względem klasy w dowolnym miejscu skryptu: $pref = Preferences::getInstance(); $pref->setProperty("imię", "Matt"); unset($pref); // usunięcie referencji $pref2 = Preferences::getInstance(); print $pref2->getProperty("imię") ."\n"; // wartość nie została utracona

168

ROZDZIAŁ 9.  GENEROWANIE OBIEKTÓW

Wynikiem wykonania kodu jest pojedyncza wartość dodana pierwotnie do obiektu Preferences i wciąż dostępna w osobnym odwołaniu: Matt

Metoda statyczna z definicji nie może odwoływać się do składowych obiektu, ponieważ nie jest wywoływana na rzecz żadnego konkretnego obiektu, a jedynie na rzecz klasy. Może jednak odwoływać się swobodnie do statycznych składowych klasy. Kiedy ma miejsce wywołanie metody getInstance(), w jej ciele następuje sprawdzenie wartości składowej Preferences::$instance. Jeśli składowa ta jest pusta, tworzony jest nowy obiekt klasy Prferences, a referencja tego obiektu jest przypisywana właśnie do Preferences::$instance. Utworzony egzemplarz klasy jest też zwracany (przez referencję — to domyślny sposób przekazywania obiektów pomiędzy metodami — przyp. tłum.) wywołującemu. Metoda getInstance() nie ma przy tym, mimo prywatności konstruktora, żadnego kłopotu z konkretyzacją obiektu — jest przecież częścią klasy Preferences. Graficzną ilustrację wzorca Singleton stanowi rysunek 9.2.

Rysunek 9.2. Przykład wzorca Singleton

Konsekwencje Jak wypada nasz Singleton w porównaniu ze zmienną globalną? Na początek złe wieści. Otóż wzorzec Singleton, podobnie jak klasyczne zmienne globalne, jest podatny na nadużycia. Obiekt Singleton jest dostępny z dowolnego miejsca systemu, może więc posłużyć do zawiązania trudnych do wykrycia zależności. Zmiana klasy Singleton może wymusić zmiany w wykorzystujących go klasach. Zależności nie są jednak problematyczne same w sobie — w końcu podobne zależności zawiązujemy zawsze, kiedy deklarujemy metody narzucające typy argumentów. Problemem jest raczej globalna natura Singletona, która pozwala programiście na pomijanie kanałów komunikacji definiowanych interfejsami klas. Tam, gdzie w użyciu jest Singleton, zależność jest jednak ukryta w ciele metody i nie jest wyrażana jej sygnaturą. Utrudnia to identyfikację zależności w systemie. Klasy Singleton powinny więc być wdrażane wstrzemięźliwie i z rozwagą. Uważam jednak, że umiarkowane i uważne stosowanie wzorca Singleton może znacznie ulepszyć system, eliminując z niego niepotrzebne przekazywanie obiektów pomiędzy elementami systemu. Singleton to jakby ulepszona wersja zmiennej globalnej, wykorzystywana zamiast niej w kontekstach obiektowych. Singletona nie da się przypadkowo zamazać niepoprawnymi danymi. Takie zabezpieczenie ma szczególne znaczenie w tych wersjach PHP, w których nie mamy do dyspozycji przestrzeni nazw. Tutaj nie ma możliwości wykrycia duplikatów nazw na etapie kompilacji.

169

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Wzorzec Factory Method W projektowaniu obiektowym implementacja jest poboczna względem abstrakcji — staramy się operować uogólnieniami, a nie specjalizacjami. Wzorzec Factory Method („metoda wytwórcza”) stosuje się do problematycznego zagadnienia tworzenia egzemplarzy klas, kiedy kod operuje typami abstrakcyjnymi. Jak to zrobić? Cóż, niech za konkretyzację odpowiadają klasy specjalizujące.

Problem Wyobraźmy sobie projekt osobistego programowego kalendarza. Manipuluje on obiektami Appointment (spotkanie). Nasza grupa biznesowa zawiązała współpracę z inną firmą i musimy teraz wymieniać dane o spotkaniach z pracownikami tamtej firmy; wymiana odbywa się w formacie BloggsCal. Zarząd przebąkuje o możliwych dalszych „fuzjach” i konieczności wdrażania kolejnych formatów wymiany informacji o spotkaniach. Pozostając na poziomie interfejsu, możemy natychmiast zidentyfikować składowe systemu. Potrzebujemy mianowicie modułu kodującego konwertującego obiekty klasy Appointment na odpowiedni format — klasę kodującą możemy opatrzyć nazwą ApptEncoder. Potrzebna będzie też klasa zarządzająca, która za pośrednictwem modułu kodującego będzie mogła porozumiewać się ze stroną komunikacji. Nazwijmy tę klasę CommsManager. W terminologii wzorców CommsManager byłby wytwórcą, a ApptEncoder — produktem. Rozpoznaną strukturę ilustruje rysunek 9.3.

Rysunek 9.3. Abstrakcje klas wytwórcy i produktów Skąd jednak wziąć w klasie zarządzającej konkretny obiekt ApptEncoder? Moglibyśmy zażądać przekazywania obiektu ApptEncoder do klasy CommsManager, ale nie byłoby to rozwiązanie, a jedynie odłożenie problemu kreacji na bok. Lepiej byłoby konkretyzować podtypy ApptEncoder wprost w klasie CommsManager: abstract class ApptEncoder { abstract function encode(); } class BloggsApptEncoder extends ApptEncoder { function encode() { return "Dane spotkania zakodowane w formacie BloggsCal\n"; } } class MegaApptEncoder extends ApptEncoder { function encode() { return "Dane spotkania zakodowane w formacie MegaCal\n"; } } class CommsManager { function getApptEncoder() { return new BloggsApptEncoder(); } }

170

ROZDZIAŁ 9.  GENEROWANIE OBIEKTÓW

Za generowanie obiektów podtypów ApptEncoder odpowiedzialna jest teraz klasa CommsManager. Kiedy jednak, co jest nieuniknione, nastąpi zwrot wizji biznesowej i zostaniemy poproszeni o konwersję systemu przystosowującą ją do obsługi nowego formatu, o nazwie MegaCal, wystarczy, że uzupełnimy metodę CommsManager::getApptEncoder() o rozgałęzienie warunkowe. Spróbujmy więc skonstruować taką implementację klasy CommsManager, która obsługiwałaby oba formaty wymiany danych: class CommsManager { const BLOGGS = 1; const MEGA = 2; private $mode = 1; function __construct($mode) { $this->mode = $mode; } function getApptEncoder() { switch ($this->mode) { case (self::MEGA): return new MegaApptEncoder(); default: return new BloggsApptEncoder(); } } } $comms = new CommsManager(CommsManager::MEGA); $apptEncoder = $comms->getApptEncoder(); print $apptEncoder->encode();

Za pośrednictwem składowej stałej definiujemy dwa tryby wykonania skryptu: tryb MEGA oraz tryb BLOGGS. W ciele metody getApptEncoder() stosujemy zaś instrukcję wyboru (switch) dobierającą podtyp konkretyzacji obiektu na bazie wartości składowej określającej bieżący tryb wykonania. Implementacja taka nie ma wielu słabych stron. Co prawda instrukcje warunkowe są niekiedy symptomami niedoróbki projektowej, ale tworzenie obiektu wymaga przecież wcześniej czy później podjęcia decyzji co do jego typu. Bylibyśmy mniej wyrozumiali na widok duplikatów instrukcji wyboru. Klasa CommsManager udostępnia funkcje komunikacji danych o spotkaniach. Załóżmy, że wykorzystywane w tej komunikacji protokoły zmuszają nas do opatrywania danych nagłówkami i stopkami wyróżniającymi poszczególne spotkania. Spójrzmy, jak wyglądałaby implementacja klasy rozszerzona o generującą nagłówek metodę getHeaderText(): class CommsManager { const BLOGGS = 1; const MEGA = 2; private $mode; function __construct($mode) { $this->mode = $mode; } function getHeaderText() { switch ($this->mode) { case (self::MEGA): return "Nagłówek MegaCall\n"; default: return "Nagłówek Bloggs\n"; } }

171

PHP. OBIEKTY, WZORCE, NARZĘDZIA

function getApptEncoder() { switch ($this->mode) { case (self::MEGA): return new MegaApptEncoder(); default: return new BloggsApptEncoder(); } } }

Jak widać, uzupełnienie klasy o funkcję generowania nagłówków danych zmusiło nas do powielenia instrukcji wyboru w kodzie implementacji protokołu. W miarę dodawania nowych protokołów będzie to coraz bardziej uciążliwe, zwłaszcza jeśli całość uzupełnimy jeszcze o obsługę stopek wiadomości w postaci metody getFooterText(). Podsumowując:  Aż do momentu uruchomienia programu nie znamy konkretnego typu obiektu, który musimy wygenerować (może chodzić albo o typ BloggsApptEncoder, albo o MegaApptEncoder).  Chcielibyśmy zapewnić sobie możliwość łatwego rozszerzania asortymentu typów „produktów” (przecież niedługo finalizacja kolejnej fuzji i zza węgła wychyla się już format SyncML!).  Każdy typ „produktu” jest powiązany z kontekstem, który poza kreacją wymaga wykonywania innych specjalizowanych operacji (getHeaderText(), getFooterText()). Poza tym zauważamy obecność podejrzanie wielu i podejrzanie powtarzających się instrukcji warunkowych, co każe nam poszukać dla nich alternatywy w postaci polimorfizmu. Wszystko to pasuje do wzorca Factory Method, który pozwala na stosowanie dziedziczenia i polimorfizmu celem hermetyzacji procesu tworzenia konkretnych „produktów”. Innymi słowy, dla każdego z protokołów potrzeba nam osobnej specjalizacji klasy CommsManager implementującej odpowiednio metodę getApptEncoder().

Implementacja Wzorzec Factory Method separuje klasę wytwórcy od produktów, które ten miał generować. Klasa wytwórcy jest klasą wytwórczą definiującą metodę kreacji obiektu-produktu. W obliczu braku implementacji domyślnej (domyślnej linii produktów) konkretyzacja spada w całości na klasy pochodne klasy wytwórcy. Typowo każda taka klasa konkretyzuje równoległą klasę pochodną produktu. Uczyńmy więc klasę CommsManager klasą abstrakcyjną. W ten sposób zachowamy elastyczną klasę bazową, przenosząc całość kodu charakterystycznego dla implementacji poszczególnych protokołów do jej konkretnych klas pochodnych. Tę zmianę projektową odzwierciedla rysunek 9.4.

Rysunek 9.4. Konkretne klasy wytwórców i produktów

172

ROZDZIAŁ 9.  GENEROWANIE OBIEKTÓW

Oto uproszczony kod implementacji: abstract class ApptEncoder { abstract function encode(); } class BloggsApptEncoder extends ApptEncoder { function encode() { return "Dane spotkania zakodowane w formacie BloggsCal\n"; } } abstract class CommsManager { abstract function getHeaderText(); abstract function getApptEncoder(); abstract function getFooterText(); } class BloggsCommManager extends CommsManager { function getHeaderText() { return "Nagłówek BloggsCal\n"; } function getApptEncoder() { return new BloggsApptEncoder(); } function getHeaderText() { return "Stopka BloggsCal\n"; } } $mgr = new BloggsCommsManager(); print $mgr->getHeaderText(); print $mgr->getApptEncoder()->encode(); print $mgr->getFooterText(); Nagłówek BloggsCal Dane spotkania zakodowane w formacie BloggsCal Stopka BloggsCal

 Uwaga Metoda BloggsCommsManager::getApptEncoder() zwraca obiekt klasy BloggsApptEncoder. Użytkownik wywołujący metodę getApptEncoder() może oczekiwać otrzymania obiektu typu ApptEncoder, niekoniecznie musi zaś wiedzieć, jaki jest konkretny podtyp zwróconego obiektu. Pewność co do typu otrzymanego obiektu jest w niektórych językach programowania gwarantowana wymuszaniem i kontrolą typów wartości zwracanych z metod. W języku PHP jest to kwestią nie tyle kontroli, co konwencji. Ważnym aspektem implementacji jest więc dokumentacja typu wartości zwracanych albo sygnalizowanie ich za pośrednictwem wyróżnionej konwencji nazewniczej.

Kiedy staniemy w obliczu zadania implementacji komunikacji w formacie MegaCal, implementacja sprowadzi się do wyprowadzenia z naszych klas abstrakcyjnych nowych specjalizacji. Udział tych klas w systemie ilustruje rysunek 9.5.

173

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Rysunek 9.5. Rozszerzenie projektu pod kątem obsługi nowego formatu

Konsekwencje Zauważmy, że nasze klasy wytwórców odzwierciedlają i niejako dublują hierarchię produktów. To częsty efekt wdrożenia wzorca Factory Method, przez niektórych traktowany jako jego wada — faktem jest bowiem, że dochodzi do swego rodzaju powielenia kodu. Kolejną kwestią jest ryzyko zachęcania programisty do wyprowadzania niepotrzebnych klas pochodnych. Jeśli jedynym powodem specjalizacji jest chęć wdrożenia wzorca Factory Method, warto rzecz przemyśleć ponownie (właśnie dlatego do naszego przykładu wprowadziłem dodatkową motywację do wdrożenia w postaci dodatkowych elementów protokołu — nagłówków i stopek). W przykładzie skupiliśmy się wyłącznie na spotkaniach. Gdybyśmy ów przykład rozszerzyli tak, aby obok terminarza spotkań obsługiwane były przypomnienia o zaplanowanych zadaniach i książka adresowa, stanęlibyśmy w obliczu kolejnego problemu. Potrzebowalibyśmy bowiem struktury, która obsługiwałaby równocześnie całe zestawy powiązanych implementacji. Z tego względu wzorzec Factory Method często otrzymuje wsparcie ze strony wzorca Abstract Factory omawianego w następnym podrozdziale.

Wzorzec Abstract Factory W większych aplikacjach potrzebne są niekiedy wytwórnie produkujące powiązane zestawy klas. Zastosowanie znajduje tu wzorzec Abstract Factory (wytwórnia abstrakcji).

Problem Wróćmy znów do przykładu implementacji kalendarza. W tej chwili obsługuje on dwa formaty kodowania danych o spotkaniach: BloggsCal i MegaCal. Strukturę projektu możemy rozwinąć „wszerz”, dodając kolejne formaty, ale w jaki sposób rozbudować strukturę „wzwyż”, co miałoby polegać na rozbudowaniu obsługi o powiadamianie o zaplanowanych zadaniach i książkę kontaktów?

174

ROZDZIAŁ 9.  GENEROWANIE OBIEKTÓW

Rysunek 9.6 prezentuje równoległe rodziny produktów, na których będziemy operować. Będą to spotkania (Appt), rzeczy zaplanowane (Ttd) i kontakty (Contact).

Rysunek 9.6. Trzy rodziny produktów Klasy obsługujące format BloggsCal nie są tutaj powiązane dziedziczeniem (choć mogą implementować wspólny interfejs), ale zrównolegleniem funkcjonalności. A skoro nasz system obsługuje kodowanie list zadań (BloggsTtdEncoder), powinien również zostać uzupełniony o kodowanie książki kontaktów (BloggsContactEncoder). Zaczniemy od interfejsu, podobnie jak w ramach wzorca Factory Method (patrz rysunek 9.7).

Rysunek 9.7. Abstrakcyjny wytwórca i abstrakcyjne produkty

Implementacja Abstrakcyjna klasa CommsManager definiuje interfejs wytwarzania każdego z trzech produktów konwerterów formatów (ApptEncoder, TtdEncoder i ContactEncoder). Aby móc faktycznie generować konkretne produkty z danej rodziny, musimy zaimplementować konkretnego wytwórcę. Na rysunku 9.8 widać projekt takiej implementacji dla formatu BloggsCal.

175

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Rysunek 9.8. Projekt uzupełniony o konkretnego wytwórcę i konkretne produkty Oto uproszczony kod klas CommsManager i BloggsCommsManager: abstract class CommsManager { abstract function getHeaderText(); abstract function getApptEncoder(); abstract function getTtdEncoder(); abstract function getContactEncoder(); abstract function getFooterText(); } class BloggsCommsManager extends CommsManager { function getHeaderText() { return "Nagłówek BloggsCal\n"; } function getApptEncoder() { return new new BloggsApptEncoder(); } function getTtdEncoder() { return new new BloggsTtdEncoder(); } function getContactEncoder() { return new new BloggsContactEncoder(); } function getFooterText() { return "Stopka BloggsCal\n"; } }

176

ROZDZIAŁ 9.  GENEROWANIE OBIEKTÓW

Zwróćmy uwagę na zastosowanie w tym przykładzie wzorca Factory Method. Metoda getContactEncoder() to w klasie CommsManager metoda abstrakcyjna, implementowana w BloggsCommsManager. Jak widać, wzorce projektowe często pasują do siebie i wdrożenie jednego wzorca stwarza kontekst do zastosowania kolejnego. Na rysunku 9.9 projekt uzupełniamy jeszcze o format MegaCal.

Rysunek 9.9. Kolejni konkretni wytwórcy i konkretne produkty

Konsekwencje Cóż ten wzorzec nam daje?  Po pierwsze, zyskujemy rozprzężenie systemu i szczegółów implementacji. Teraz możemy w prosty i wygodny sposób dodawać (i usuwać) dowolną liczbę formatów danych.  Po drugie, wymusiliśmy zgrupowanie funkcji powiązanych elementów. Korzystając z klasy BloggsCommsManager, mamy więc gwarancję, że będzie ona korzystać jedynie z klas związanych z formatem BloggsCal.  Po trzecie jednak, pojawiła się trudność uzupełniania systemu o nowe produkty. Trzeba dla nich dostarczyć nie tylko konkretną implementację produktu, ale i dokonać aktualizacji abstrakcyjnego wytwórcy i wszystkich jego konkretnych implementacji. Wiele implementacji wzorca projektowego Abstract Factory odwołuje się do wzorca Factory Method. Przyczyną tego stanu rzeczy może być fakt, że większość przykładów pisanych jest w języku Java bądź C++. Jednak w PHP nie mamy kontroli typów wartości zwracanych przez metody, co daje nam pewną dodatkową elastyczność. Zamiast tworzyć osobne metody Factory Method, możemy więc definiować pojedynczą metodę make(), która określa typ zwracanego obiektu na podstawie znacznika przekazanego argumentem wywołania. abstract class CommsManager { const APPT = 1; const TTD = 2; const CONTACT = 3; abstract function getHeaderText(); abstract function make($flag_int); abstract function getFooterText(); } class BloggsCommsManager extends CommsManager {

177

PHP. OBIEKTY, WZORCE, NARZĘDZIA

function getHeaderText() { return "Nagłówek BloggsCal\n"; } function make($flag_int) { switch ($flag_int) { case self::APPT: return new BloggsApptEncoder(); case self::CONTACT: return new BloggsContactEncoder(); case self::TTD: return new BloggsTtdEncoder(); } } function getFooterText() { return "Stopka BloggsCal\n"; } }

Jak widać, zyskujemy w ten sposób na zwartości interfejsu klas. Nie odbywa się to jednak za darmo. Korzystając z metody wytwórczej, definiujemy spójny interfejs i zmuszamy do jego honorowania wszystkie konkretne obiekty-wytwórców. Stosowaniem metody make() obligujemy się do pamiętania, aby w każdym konkretnym wytwórcy obsługiwane były wszystkie konkretne produkty. Wprowadzamy też równoległe gałęzie warunkowe, ponieważ każdy z wytwórców musi implementować te same testy znacznika. Użytkownik nie może być pewien, czy konkretni wytwórcy generują wszystkie produkty, ponieważ spójność wnętrzności make() pomiędzy wytwórcami to w każdym przypadku kwestia co najwyżej umowna — nie da się jej wymusić. Z drugiej strony zyskujemy na elastyczności wytwórców. Klasa bazowa wytwórcy może udostępniać metodę make() gwarantującą domyślną implementację każdej rodziny produktów, a klasy pochodne mogą wtedy (choć nie muszą) modyfikować zachowanie klasy bazowej w tym zakresie. Od owych klas zależeć będzie, czy wytwarzanie zrealizują wywołaniem domyślnej wersji make(), czy udostępnią jej własną implementację. W następnym podrozdziale omówimy jeszcze jedną wariację na temat wzorca Abstract Factory.

Prototyp W przypadku wzorca Factory Method problemem może być wyłanianie się równoległych, lustrzanych hierarchii dziedziczenia. Tego rodzaju sprzęganie jest dla niektórych programistów niewygodne. Każde uzupełnienie rodziny produktów zmusza nas bowiem do utworzenia odpowiedniego konkretnego wytwórcy (jak w przypadku produktu konwertera formatu BloggsCal, dla którego trzeba było zdefiniować wytwórcę BloggsCommsManager). W systemie cechującym się dynamicznym rozwojem i szybkim przyrostem liczby produktów zarządzanie tego rodzaju zależnościami może szybko stać się uciążliwe. Opisanej zależności można uniknąć, stosując dostępne w PHP słowo kluczowe clone i wykonując za jego pośrednictwem duplikaty istniejących konkretnych produktów. Bazą generowania produktów stają się wtedy same klasy konkretnych produktów. Tak wygląda wzorzec Prototype (prototyp). Pozwala on na zastąpienie dziedziczenia kompozycją. To z kolei promuje elastyczność czasu wykonania i redukuje liczbę klas, które trzeba utworzyć.

Problem Wyobraźmy sobie udostępnianą przez WWW grę wzorowaną na klasycznej Civilization, w ramach której na planszy podzielonej siatką operują jednostki graczy. Każde pole planszy może reprezentować morze, równinę albo las. Typ terenu ogranicza możliwości ruchowe i zdolności bojowe jednostek go okupujących. System taki może obejmować obiekt TerrainFactory udostępniający obiekty klas Sea, Forest i Plains. Zdecydowaliśmy, że będziemy pozwalali użytkownikowi na wybieranie radykalnie różnych środowisk rozgrywki, więc Sea będzie abstrakcyjną klasą bazową implementowaną np. klasami MarsSea (morze marsjańskie) i EarthSea (morze ziemskie).

178

ROZDZIAŁ 9.  GENEROWANIE OBIEKTÓW

Podobnie implementowane są typy Forest i Plains. Taki układ sprzyja wdrożeniu wzorca Abstract Factory. Mamy przecież różne rodziny produktów (Sea, Plains, Forest), z silną rodzinną zależnością wyrażaną dziedziczeniem. Sposób możliwego wdrożenia w tym projekcie wzorca Abstract Factory za pomocą Factory Method prezentowany jest na diagramie z rysunku 9.10.

Rysunek 9.10. Obsługa terenu według wzorca Abstract Factory Jak widać, grupujemy rodziny produktów generowane w wytwórni przez dziedziczenie. To rozwiązanie działa, ale wymaga konstruowania rozległej hierarchii dziedziczenia i jest stosunkowo mało elastyczne. Chcąc pozbyć się równoległych, lustrzanych hierarchii z myślą o maksymalizacji elastyczności czasu wykonania, można uciec się do wzorca projektowego Prototype (prototyp) — jednej z wariacji wzorca Abstract Factory.

Implementacja Operując wzorcami Abstract Factory i Factory Method, musimy w którymś momencie zdecydować — na przykład sprawdzając jakiś znacznik preferencji — o tym, który z konkretnych wytwórców ma wykonać pracę. Ponieważ i tak trzeba to kiedyś zrobić, dlaczego nie utworzyć po prostu klasy wytwórni, która przechowuje konkretne produkty, i nie wypełnić jej w czasie inicjalizacji? Pozbylibyśmy się w ten sposób kilku klas oraz — jak się za chwilę okaże — osiągnęli inne korzyści. Oto prosty kod wykorzystujący w ramach wytwórni wzorzec prototypu: class Sea {} class EarthSea extends Sea {} class MarsSea extends Sea {} class Plains {} class EarthPlains extends Plains {} class MarsPlains extends Plains {} class Forest {} class EarthForest extends Forest {} class MarsForest extends Forest {} class TerrainFactory { private $sea; private $forest; private $plains;

179

PHP. OBIEKTY, WZORCE, NARZĘDZIA

function __construct(Sea $sea, Plains $plains, Forest $forest) { $this->sea = $sea; $this->plains = $plains; $this->forest = $forest; } function getSea() { return clone $this->sea; } function getPlains() { return clone $this->plains; } function getForest() { return clone $this->forest; } } $factory = new TerrainFactory(new EarthSea(), new EarthPlains(), new EarthForest()); print_r($factory->getSea()); print_r($factory->getPlains()); print_r($factory->getForest()); EarthSea Object ( ) EarthPlains Object ( ) EarthForest Object ( )

Wytwórnię TerrainFactory załadowaliśmy przy inicjalizacji egzemplarzami produktów. Kiedy użytkownik wywoła jej metodę getSea(), otrzyma zwrotnie duplikat obiektu Sea, buforowanego w TerrainFactory od momentu inicjalizacji. Nie tylko pozbyliśmy się paru klas, ale i zyskaliśmy dodatkową elastyczność. Gracze pragną rozgrywki na nowej planecie, z morzami i lasami przypominającymi ziemskie, ale marsjańskimi równinami? Nic prostszego. Nie trzeba pisać nowej klasy wytwórcy, wystarczy zmienić układ obiektów przekazywanych w wywołaniu konstruktora TerrainFactory: $factory = new TerrainFactory(new EarthSea(), new MarsPlains(), new EarthForest());

Wzorzec Prototype pozwala więc na doświadczenie elastyczności charakterystycznej dla kompozycji. Ale to nie wszystko. Ponieważ obiekty składujemy w wytwórni i powielamy dynamicznie (w czasie wykonania programu), generując nowe produkty reprodukujemy stan obiektu. Załóżmy, że obiekty Sea posiadają składową $navigability (łatwość żeglugi). Jej wartość ma określać ilość energii, jakiej wymaga żegluga w każdym ruchu, i może być wykorzystana do regulacji stopnia trudności rozgrywki:

180

ROZDZIAŁ 9.  GENEROWANIE OBIEKTÓW

class Sea { $navigability = 0; function __construct($navigability) { $this->navigability = $navigability; } }

Jeśli teraz zainicjalizujemy obiekt TerrainFactory, możemy nadać obiektom Sea modyfikator łatwości żeglugi. Będzie on obowiązywał dla wszystkich obiektów udostępnianych przez wytwórnię TerrainFactory: $factory = new TerrainFactory(new EarthSea(-1), new MarsPlains(), new EarthForest());

Zyskana elastyczność jest widoczna również wtedy, kiedy generowany obiekt ma składać się z innych obiektów. Załóżmy, że wszystkie obiekty klasy Sea mogą zawierać obiekty hierarchii Resource (zasoby: FishResource — ryby, OilResource — ropa itd.). Można wtedy domyślnie przypisać wszystkim obiektom Sea ustalone znacznikiem zasoby. Pamiętaj jednak, że jeśli obiekt składa się z innych obiektów, jego powielenie wymaga jawnej implementacji metody __clone(). class Contained { } class Container { public $contained; function __construct() { $this->contained = new Contained(); } function __clone() { // Duplikat ma przechowywać kopię obiektu $contained, // a nie jego referencję: $this->contained = clone $this->contained; } }

 Uwaga Klonowanie obiektów omawialiśmy w rozdziale 4. Dla przypomnienia, słowo kluczowe clone generuje jedynie powierzchowną kopię wszelkich obiektów, do których jest stosowane. Składowe tak wygenerowanego obiektu będą co do wartości identyczne ze składowymi oryginału, ale jeśli którakolwiek ze składowych obiektu będzie innym obiektem, w kopii znajdzie się w jej miejsce jedynie referencja tego samego obiektu. Aby wykonać pełny duplikat, trzeba przesłonić w klasie powielanego obiektu metodę specjalną __clone(). To właśnie ta metoda jest automatycznie wywoływana w obliczu słowa kluczowego clone.

Ależ to oszustwo! Obiecałem, że ten rozdział będzie poświęcony logice i strategiom kreacji obiektów. A co się okazuje? Niektóre wzorce projektowe chytrze uchylają się od kreacji, a przynajmniej od jej części decyzyjnej. Wzorzec Singleton jest bez winy. Logika kreacji obiektu jest tu jasna i wbudowana we wzorzec. Wzorzec Abstract Factory skupia tworzenie rodzin produktów w rękach różnych konkretnych wytwórców. Ale jak wybrać wytwórcę? Podobny problem mamy ze wzorcem Prototype — oba wzorce stosują się do tworzenia obiektów, ale unikają kwestii wyboru obiektu czy grupy obiektów do utworzenia. Wybór konkretnego wytwórcy często dyktuje ustalony przełączniki albo parametr konfiguracji systemu. Parametr ten może być składowany w bazie danych, pliku konfiguracyjnym, pliku serwera (np. w .htaccess, czyli w pliku konfiguracyjnym katalogu dla serwera Apache) albo wreszcie zaszyty kodzie PHP w postaci zmiennej albo składowej. Ponieważ aplikacje PHP są konfigurowane od nowa dla każdego kolejnego żądania, inicjalizacyjna część skryptu powinna być jak najmniej zajmująca. Dlatego osobiście często decyduję się na sterowanie wyborem za pośrednictwem znaczników i zmiennych w kodzie PHP. Można to robić ręcznie albo napisać skrypt automatycznie generujący plik klasy. Oto zarys klasy, która definiuje znacznik protokołu stosowanego w przykładzie z wymianą danych:

181

PHP. OBIEKTY, WZORCE, NARZĘDZIA

class Settings { static $COMMSTYPE = 'Bloggs'; }

Dysponując znacznikiem (choć jest on mało elegancki), możemy utworzyć klasę opierającą na nim wybór obiektu CommsManager dla bieżącego żądania. Często w tej roli występują połączone implementacje wzorców Singleton i Abstract Factory, jak poniżej: require_once('Settings.php'); class AppConfig { private static $instance; private $commsManager; private function __construct() { // wywoływana tylko raz… $this->init(); } private function init() { switch (Settings::$COMMSTYPE) { case 'Mega': $this->commsManager = new MegaCommsManager(); break; default: $this->commsManager = new BloggsCommsManager(); } } public static function getInstance() { if (empty(self::$instance)) { self::$instance = new self(); } return self::$instance; } public function getCommsManager() { return $this->commsManager; } }

Klasa AppConfig to standardowy Singleton. Z tego względu możemy pozyskać obiekt klasy AppConfig z dowolnego miejsca systemu i zawsze otrzymamy ten sam egzemplarz. Metoda init() klasy jest wywoływana w konstruktorze, co daje gwarancję jedynie jednokrotnego jej wykonania w ramach procesu. Jej ciało sprawdza wartość składowej Settings::$COMMSTYPE, wybierając na jej podstawie do konkretyzacji właściwy podtyp obiektu CommsManager. Teraz nasz skrypt może pozyskać obiekt CommManager i operować na nim, nie wiedząc nawet, którą z konkretnych implementacji faktycznie dysponuje. $commsMgr = AppConfig::getInstance()->getCommsManager(); $commsMgr->getApptEncoder()->encode();

Podsumowanie Niniejszy rozdział prezentował kilka sztuczek służących do generowania obiektów. Omówienie obejmowało między innymi wzorzec Singleton dający globalny dostęp do jednego egzemplarza wybranej klasy. Dalej prezentowany był wzorzec Factory Method zakładający oparcie procesu kreacji obiektów na polimorfizmie. Wzorzec ten łączyliśmy ze wzorcem Abstract Factory, zamierzając generować klasy konkretyzujące zestawy powiązanych obiektów. Nie zabrakło też omówienia wzorca projektowego Prototype zakładającego wykorzystanie w kreacji obiektów powielania i kompozycji. 182

ROZDZIAŁ 10 

Wzorce elastycznego programowania obiektowego Uzbrojeni w wiedzę o strategiach tworzenia obiektów możemy przyjrzeć się strategiom i metodom organizacji klas i obiektów. Przyświecać nam będzie kolejna zasada projektowania, mówiąca o zaletach elastyczności kompozycji względem dziedziczenia. Wzorce projektowe analizowane w tym rozdziale zaczerpnięte zostały z katalogu Bandy Czworga. Rozdział traktować będzie o:  Wzorcu Composite — czyli o układaniu struktur, w ramach których w roli pojedynczych obiektów występują całe ich grupy.  Wzorcu Decorator — czyli o elastycznym mechanizmie kombinowania obiektów w czasie wykonania.  Wzorcu Facade — a więc o tworzeniu prostego interfejsu systemów złożonych i zmiennych.

Strukturalizacja klas pod kątem elastyczności obiektów Jeszcze w rozdziale 4. wspominałem, że wielu początkujących myli obiekty z klasami. To jednak prawda tylko połowiczna. W rzeczywistości również rzesze bardziej doświadczonych programistów i projektantów głowią się nad diagramami klas UML, próbując wykoncypować z prezentowanych na nich statycznych struktur dziedziczenia dynamiczne zależności, które pojawią się pomiędzy obiektami. Czy pamiętasz hasło: „Faworyzuj kompozycję przed dziedziczeniem”? Odzwierciedla ono sedno problemu polegającego na wyważeniu pomiędzy organizacją klas a organizacją obiektów. Aby nasze projekty były maksymalnie elastyczne, struktura klas powinna zaś umożliwiać konstruowanie użytecznych struktur obiektów w czasie wykonania. Zagadnienie to będzie miało swoje odbicie w dwóch pierwszych wzorcach projektowych omawianych w rozdziale. Dziedziczenie jest w obu ważnym elementem, ale wielką wagę ma też mechanizm, na bazie którego stosowana jest kompozycja będąca środkiem reprezentacji właściwych struktur i ułatwień w rozszerzaniu funkcjonalności.

Wzorzec Composite Wzorzec projektowy Composite (kompozyt) to chyba najbardziej wyraźny przykład wykorzystania dziedziczenia w służbie kompozycji. Reprezentuje prostą, a jednocześnie niezwykle elegancką koncepcję projektową, jest też wzorcem wielce przydatnym. Wypada tu ostrzec Czytelnika, że elegancja i skuteczność tego wzorca stanowi pokusę do jego nadużywania.

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Wzorzec Composite opisuje prostą metodę agregacji i zarządzania grupami podobnych do siebie obiektów w taki sposób, aby pojedynczy obiekt uczestniczący w agregacji był dla użytkownika nieodróżnialny od grupy obiektów. Wzorzec jest bardzo prosty, ale również niekiedy mylący — przyczyną tego jest między innymi podobieństwo struktur klas uczestniczących we wzorcu do struktury jego obiektów. Hierarchie dziedziczenia to struktury drzewiaste, zakorzenione w klasie nadrzędnej, bazowej, i rozgałęziające się wzdłuż poszczególnych specjalizacji. Drzewo dziedziczenia klas określone wzorcem Composite służy zaś do łatwego generowania i przeglądania drzew obiektów. Czytelnik, który nie poznał jeszcze tego wzorca, ma pełne prawo czuć się teraz nieco zdezorientowany. Spróbujmy więc analogii ilustrującej podobne przypadki jednostek, które traktuje się identycznie jak zbiory. Otóż mając do dyspozycji podstawowe składniki, takie jak zboża, kasze i mięso (albo soję, jak kto lubi), możemy przygotować posiłek — np. kiełbasę. Wynik złożenia iluś składników jest dla nas pojedynczą jednostką i od momentu złożenia możemy na niej operować — możemy bowiem kiełbasę sprzedawać, kupować, jeść czy gotować, tak samo jak wcześniej moglibyśmy sprzedawać, kupować, jeść czy gotować wchodzące w jej skład mięso. Kiełbasę możemy z kolei połączyć z kolejnymi składnikami, przygotowując danie; tym samym jeden produkt wieloskładnikowy zmienilibyśmy w kolejny produkt wieloskładnikowy. Wobec wieloskładnikowych kompozycji działamy zatem tak jak wobec niepodzielnych jednostek. Tego rodzaju zależność pomiędzy komponentami a kolekcjami w kodzie modeluje wzorzec Composite.

Problem Zarządzanie grupami obiektów może być zadaniem skomplikowanym, zwłaszcza jeśli rzeczone obiekty mogą zawierać w sobie kolejne obiekty. W programowaniu problem ten występuje dość powszechnie. Weźmy choćby faktury z kolejnymi pozycjami wyszczególniającymi nabywane produkty czy usługi albo listy zadań z pozycjami reprezentującymi zadania złożone z wielu czynności podrzędnych. Podobne przykłady można czerpać z dziedziny zarządzania treścią, kiedy to przenosząc obiekty, musimy zachować hierarchię rozdziałów, stron czy artykułów. Zarządzanie takimi strukturami z zewnątrz szybko staje się uciążliwe. Wróćmy do przykładu z poprzedniego rozdziału. Projektowaliśmy tam system gry bazujący na klasyce gatunku — grze Civilisation. Gracz ma mieć możliwość przemieszczania swoich jednostek po planszy składającej się z setek pól. Jednostki gracza mogą być łączone w oddziały, wspólnie podróżujące, walczące i broniące się. Zdefiniujmy kilka typów jednostek: abstract class Unit { abstract function bombardStrength(); } class Archer extends Unit { function bombardStrength() { return 4; } } class LaserCannonUnit extends Unit { function bombardStrength() { return 44; } }

Klasa Unit (jednostka) definiuje abstrakcyjną metodę bombardStrength() stanowiącą o sile rażenia ataku danej jednostki na jednostkę zajmującą sąsiednie pole. Metoda ta doczekała się implementacji w klasach Archer i LaserCannonUnit. Klasy te powinny również zawierać informacje o mobilności jednostki i możliwościach obronnych, ale dla uproszczenia pominiemy je. Jednostki możemy grupować za pośrednictwem osobnej klasy, jak tutaj: class Army { private $units = array(); function addUnit(Unit $unit) {

184

ROZDZIAŁ 10.  WZORCE ELASTYCZNEGO PROGRAMOWANIA OBIEKTOWEGO

array_push($this->units, $unit); } function bombardStrength() { $ret = 0; foreach($this->units as $unit) { $ret += $unit->bombardStrength(); } return $ret; } }

Klasa Army posiada metodę addUnit() (dodaj jednostkę) przyjmującą w wywołaniu obiekt klasy Unit. Obiekty Unit są przechowywane w prywatnej składowej tablicowej o nazwie $units. Metoda bombardStrength() służy tu do obliczania łącznej siły ataku jednostek składających się na oddział. Jej implementacja sprowadza się do przeglądania kolekcji obiektów Unit, wywołując na rzecz każdego z nich jego metodę bombardStrength(). Ten model jest akceptowalny, dopóki złożoność problemu pozostaje na prezentowanym poziomie. Ale co się stanie, jeśli model nieco rozbudujemy? Załóżmy, że oddział powinien móc grupować inne oddziały. Każdy z nich musi jednak zachowywać swoją tożsamość, tak aby później mógł odłączyć się od całości. Przecież w każdej chwili natarcie na odsłoniętą flankę wroga może zostać osłabione koniecznością odesłania części jednostek dla stłumienia rozruchów w kraju pozbawionym wojska. Z tego powodu nie można sprowadzić problemu do wcielenia wszystkich jednostek jednego oddziału do innego. Moglibyśmy klasę Army zmodyfikować tak, aby mogła kumulować tak jednostki, jak i oddziały: function addArmy(Army $army) { array_push($this->armies, $army); }

Wymaga to modyfikacji również metody bombardStrength(), która musi w nowej wersji sumować — oprócz siły rażenia jednostek — siłę rażenia armii składowych: function bombardStrength() { $ret = 0; foreach($this->units as $unit) { $ret += $unit->bombardStrength(); } foreach($this->armies as $army) { $ret += $army->bombardStrength(); } return $ret; }

Zwiększenie złożoności nie jest jeszcze problematyczne. Pamiętajmy jednak, że podobnych modyfikacji wymagałyby (pominięte u nas dla uproszczenia) pozostałe metody, jak movementRange() (zasięg ruchu) defensiveStrength() (siła obrony) itd. Przecież gra z założenia ma dawać graczowi znaczną swobodę zarządzania jednostkami. I już gracze domagają się transportera zwiększającego zasięg dziesięciu jednostek w niektórych rodzajach terenu. W oczywisty sposób transporter taki przypomina oddział, ponieważ również grupuje jednostki. Ma też jednak cechy właściwe tylko jemu. Moglibyśmy dalej poprawiać klasę Army, aby dało się do oddziału wcielać również obiekty TroopCarrier (transporter oddziału), ale wiemy z góry, że rodzajów pojazdów i typów grupowania będzie więcej. Oczywista staje się więc potrzeba opracowania bardziej elastycznego modelu. Przyjrzyjmy się jeszcze raz konstruowanemu modelowi. Wszystkie powołane do życia klasy cechowały się wspólną metodą bombardStrength(). W efekcie użytkownik nie musi rozróżniać oddziałów, transporterów i pojedynczych jednostek. Funkcjonalnie są one bowiem identyczne. Wszystkie mają zdolność przemieszczania się po planszy, atakowania jednostek okupujących sąsiednie pola i obrony własnych pozycji. Te obiekty, które zawierają w sobie inne, muszą udostępniać metody pozwalające na dodawanie obiektów i ich usuwanie. Wszystkie te podobieństwa zmuszają do wyciągnięcia wniosku — jeśli obiekt zawierający dzieli interfejs z obiektami zawieranymi, wszystkie one powinny dzielić rodzinę typu. 185

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Implementacja Wzorzec Composite definiuje pojedynczą hierarchię dziedziczenia, w której ujmuje dwa różne zbiory zadań. Klasy wzorca muszą obsługiwać zestaw wspólnych operacji stanowiących ich zadania podstawowe. Dla nas zadaniem tym jest obliczanie siły ataku metodą bombardStrength(). Klasy muszą jednak równocześnie definiować metody dodawania i usuwania obiektów potomnych. Diagram klas wzorca Composite przystosowanego do omawianego przykładu widnieje na rysunku 10.1.

Rysunek 10.1. Wzorzec Composite Jak widać, wszystkie jednostki naszego modelu rozszerzają klasę Unit. Użytkownik może więc być pewien, że każdy z obiektów hierarchii Unit będzie obsługiwał metodę bombardStrength(). Klasa Unit może być traktowana identycznie jak Archer. Klasy Army i TroopCarrier są kompozytami (ang. composite) — obiektami składającymi się z innych obiektów. Klasy Archer i LaserCannonUnit to liście bądź końcówki (ang. leaves), klasy reprezentujące końcowe węzły struktury drzewiastej przystosowane do obsługi podstawowych operacji hierarchii, ale nienadające się do przechowywania innych jej obiektów. Pojawia się wątpliwość, czy liście powinny implementować identyczny interfejs, co kompozyty (jak na rysunku 10.1). Na diagramie widać, że TroopCarrier i Army agregują inne jednostki, ale klasy liści również implementują wywołanie addUnit() — wrócimy do tego wkrótce. Na razie przyjrzyjmy się abstrakcyjnej klasie Unit: abstract class Unit { abstract function addUnit(Unit $unit); abstract function removeUnit(Unit $unit); abstract function bombardStrength(); }

Mamy tu zarys podstawowej funkcjonalności wszystkich obiektów hierarchii Unit. Spójrzmy teraz, jak wymienione metody abstrakcyjne mogłyby być implementowane w obiektach-kompozytach: class Army extends Unit { private $units = array(); function addUnit(Unit $unit) { if (in_array($unit, $this->units, true)) { return; } $this->units[] = $unit; } function removeUnit(Unit $unit) { $this->units = array_udiff($this->units, array($unit), function($a, $b) { return ($a === $b)? 0: 1; }); }

186

ROZDZIAŁ 10.  WZORCE ELASTYCZNEGO PROGRAMOWANIA OBIEKTOWEGO

function bombardStrength() { $ret = 0; foreach($this->units as $unit) { $ret += $unit->bombardStrength(); } return $ret; } }

Metoda addUnit() klasy Army przed włączeniem do oddziału przekazywanej w wywołaniu jednostki sprawdza, czy nie posiada jej już w prywatnej tablicy jednostek. Metoda removeUnit() sprawdza w pętli (podobnie jak metoda addUnit()), czy usunąć dany obiekt Unit.  Uwaga W metodzie removeUnit() użyliśmy anonimowego wywołania zwrotnego. Wywołanie to sprawdza elementy tablicy $units pod kątem równoważności. Użytkownicy starszej wersji powinni utworzyć analogiczną funkcję wywołania zwrotnego za pomocą wywołania create_function(): $this->units = array_udiff($this->units, array($unit), create_function('$a,$b', 'return ($a === $b)? 0: 1;'));

Obiekty klasy Army mogą przechowywać dowolnego rodzaju obiekty hierarchii Unit, w tym inne obiekty klasy Army lub końcówki takie jak Archer czy LaserCannonUnit. Ponieważ wszystkie jednostki mają implementować metodę bombardStrength(), implementacja tej metody w klasie Army sprowadza się do przejrzenia wszystkich obiektów zawieranych, przechowywanych w składowej $units, i sumowania wartości zwracanych z inicjowanych na ich rzecz wywołań bombardStrength(). Problematycznym aspektem wzorca pozostaje implementacja operacji wcielania i usuwania jednostek. Klasyczny wzorzec zakłada definicję metod add...() i remove...() w abstrakcyjnej klasie bazowej. Dzięki temu wszystkie klasy objęte szablonem udostępniają wspólny interfejs. Ale przez to implementacje tych metod trzeba dostarczyć również w klasach liści, które nie są przewidziane do przechowywania obiektów: class UnitException extends Exception {} class Archer extends Unit { function addUnit(Unit $unit) { throw new UnitException(get_class($this)." to liść"); } function removeUnit(Unit $unit) { throw new UnitException(get_class($this)." to liść"); } function bombardStrength() { return 4; } }

Z definicji klasa Archer nie jest przewidziana do przechowywania obiektów hierarchii Unit, więc na wywołanie na rzecz obiektu Archer metody addUnit() albo removeUnit() reagujemy zgłoszeniem wyjątku. Ponieważ musielibyśmy podobną implementację przewidzieć dla wszystkich klas końcówek (liści), możemy zdecydować się na jej przeniesienie do abstrakcyjnej klasy bazowej. abstract class Unit { abstract function bombardStrength(); function addUnit(Unit $unit) { throw new UnitException(get_class($this)." to liść"); }

187

PHP. OBIEKTY, WZORCE, NARZĘDZIA

function removeUnit(Unit $unit) { throw new UnitException(get_class($this)." to liść"); } } class Archer extends Unit { function bombardStrength() { return 4; } }

Eliminujemy tym samym powielanie kodu w klasach pochodnych, ale za to tracimy kontrolowaną statycznie gwarancję implementacji metod addUnit() i removeUnit() w klasach pochodnych kompozytów. Problemy charakterystyczne dla wzorca Composite będziemy omawiać bardziej szczegółowo w następnym punkcie. Tymczasem wymienimy podstawowe zalety wzorca:  Elastyczność — ponieważ wszystkie klasy wzorca Composite dzielą wspólny typ nadrzędny, możemy w prosty sposób uzupełniać projekt o nowe klasy końcówek i kompozytów bez zmian szerszego kontekstu programu.  Prostota — użytkownik struktury wzorca Composite otrzymuje do dyspozycji prosty interfejs. Nie ma potrzeby rozróżniania pomiędzy obiektami będącymi kompozytami innych obiektów a obiektami-liśćmi struktury (z wyjątkiem dodawania nowych komponentów). Wywołanie Army::bombardStrength() może niejawnie spowodować kaskadę delegacji, ale dla użytkownika semantyka wywołania jest identyczna jak semantyka wywołania Archer::bombardStrength().  Kaskadowy zasięg operacji — obiekty we wzorcu Composite są zorganizowane w strukturze drzewiastej. Każdy kompozyt przechowuje referencje grupowanych liści. Operacja na pewnej gałęzi drzewa może więc mieć szerszy oddźwięk. Z obiektu Army możemy usunąć jeden z oddziałów podrzędnych, wcielając go do innego obiektu Army. Ta prosta operacja angażuje w kodzie pojedynczy obiekt, ale w efekcie zmienia status wszystkich obiektów grupowanych w ramach przemieszczanego oddziału i ich obiektów potomnych.  Łatwość przeglądania — struktury drzewiaste są łatwe do przeglądania, pozyskiwania przechowywanych w nich informacji i transformacji. Jedna z bardziej efektywnych technik przeglądania zostanie zaprezentowana w następnym rozdziale w ramach wzorca Visitor. Korzyści z wdrożenia wzorca są szczególnie widoczne dla użytkownika systemu. Utwórzmy kilka oddziałów: // utworzenie armii $main_army = new Army(); // włączenie do niej paru jednostek $main_army->addUnit(new Archer()); $main_army->addUnit(new LaserCannonUnit() ); // utworzenie nowej armii $sub_army = new Army(); // zaciąg do nowej armii $sub_army->addUnit(new Archer()); $sub_army->addUnit(new Archer()); $sub_army->addUnit(new Archer()); // wcielenie drugiej armii do pierwszej $main_army->addUnit($sub_army); // obliczenia siły ataku wykonywane automatycznie w tle print "Atak z siłą: {$main_army->bombardStrength()}\n";

188

ROZDZIAŁ 10.  WZORCE ELASTYCZNEGO PROGRAMOWANIA OBIEKTOWEGO

Do utworzonego oddziału głównego dodajemy kilka jednostek podstawowych. Proces ten powtarzamy dla drugiego utworzonego oddziału, który następnie wcielamy do pierwszego. Przy obliczaniu siły rażenia (Unit::bombardStrength()) wynikowego oddziału złożoność struktury hierarchii obiektów jest dla wywołującego zupełnie niewidoczna.

Konsekwencje Jeśli Czytelnik myśli podobnie jak ja, powinien na widok kodu klasy Archer nabrać podejrzeń. Po co bowiem do klas końcówek włączamy metody addUnit() i removeUnit(), jeśli nie ma potrzeby obsługiwania operacji wcielania i usuwania jednostek? Odpowiedź tkwi w przezroczystości typu Unit. Jeśli użytkownik otrzymuje obiekt typu Unit, ma pewność, że obiekt ten implementuje metody addUnit() i removeUnit(). Uwidacznia się tu przyjęta we wzorcu Composite zasada, że klasy obiektów niepodzielnych (liści) mają interfejs identyczny z klasami kompozytów. Taka odpowiedź jest jednak mało satysfakcjonująca, ponieważ honorowanie interfejsu nie oznacza w tym przypadku bezpieczeństwa wywołania metod addUnit() czy removeUnit() na rzecz każdego z obiektów hierarchii Unit. Gdybyśmy owe metody przesunęli tak, aby były dostępne jedynie dla klas kompozytów, wtedy z kolei powstałby problem niepewności co do tego, czy otrzymany obiekt hierarchii Unit obsługuje czy nie obsługuje daną metodę. Mimo wszystko pozostawienie metod-pułapek w klasach liści to dla mnie sytuacja mało komfortowa. Nie ma tu wartości dodanej, a jedynie zamieszanie w projekcie systemu, ponieważ interfejs w zasadzie okłamuje użytkowników co do swojej własnej funkcjonalności. Moglibyśmy w prosty sposób wyeliminować tę niedogodność, wydzielając dla kompozytów ich własny podtyp CompositeUnit. Polegałoby to przede wszystkim na usunięciu metod addUnit() i removeUnit() z klasy Unit: abstract class Unit { function getComposite() { return null; } abstract function bombardStrength(); }

Zwróćmy uwagę na metodę getComposite(). Wrócimy do niej za moment. Teraz potrzebujemy abstrakcyjnej klasy definiującej metody usunięte z klasy Unit. Możemy w niej nawet przewidzieć ich implementacje domyślne: abstract class CompositeUnit extends Unit { private $units = array(); function getComposite() { return $this; } protected function units() { return $this->units; } function removeUnit(Unit $unit) { $this->units = array_udiff($this->units, array($unit), function($a, $b) { return ($a === $b)? 0: 1; }); function addUnit(Unit $unit) { if (in_array($unit, $this->units, true)) { return; } $this->units[] = $unit; } }

189

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Klasa CompositeUnit (kompozyt jednostek) choć sama w sobie nie zawiera żadnych metod abstrakcyjnych, jest deklarowana jako abstrakcyjna. Równocześnie rozszerza klasę Unit, nie definiując jej abstrakcyjnej metody bombardStrength(). Klasa Army (i wszystkie inne klasy kompozytów) może teraz rozszerzać klasę CompositeUnit. Organizację klas po tej modyfikacji ilustruje rysunek 10.2.

Rysunek 10.2. Przeniesienie metod dodawania i usuwania z klas kompozytowych poza klasę bazową hierarchii Wyeliminowaliśmy irytujące i bezużyteczne implementacje metod dodawania i usuwania jednostek z klas liści, ale teraz klient musi przed wywołaniem tych metod sprawdzać, czy obiekt, na rzecz którego chce zainicjować wywołanie, jest obiektem klasy CompositeUnit. Tutaj do akcji wkracza metoda getComposite(). Domyślnie zwraca ona bowiem wartość pustą. Jedynie w klasach dziedziczących po CompositeUnit wartość zwracana to obiekt klasy CompositeUnit. Jeśli więc wywołanie tej metody zwróci obiekt, można na jego rzecz wywołać metodę addUnit(). Oto zastosowanie tej techniki z punktu widzenia użytkownika: class UnitScript { static function joinExisting(Unit $newUnit, Unit $occupyingUnit) { $comp; if (! is null($comp = $occupyingUnit->getComposite())) { $comp->addUnit($newUnit); } else { $comp = new Army(); $comp->addUnit($occupyingUnit); $comp->addUnit($newUnit); } return $comp; } }

Metoda joinExisting() (połącz siły) przyjmuje dwa obiekty hierarchii Unit. Pierwszy z nich reprezentuje jednostkę nowo przybyłą na dane pole, drugi — jednostkę już na tym polu przebywającą (okupującą pole planszy). Jeśli druga z tych jednostek jest kompozytem (obiektem klasy CompositeUnit), wtedy pierwszy z obiektów jest do niej dodawany. W innym przypadku tworzony jest nowy obiekt klasy Army, do którego wcielane są obie jednostki. Określanie przynależności do hierarchii klas kompozytowych odbywa się za pośrednictwem metody getComposite(). Jeśli zwróci ona obiekt, możemy wprost do niego dodawać nowe obiekty klasy Unit. Jeśli wynikiem wywołania getComposite() będzie wartość pusta, musimy utworzyć obiekt kompozytu na własną rękę, tworząc egzemplarz klasy Army i wcielając do niego obie jednostki. Model można uprościć jeszcze bardziej, wymuszając w metodzie Unit::getComposite() zwrócenie obiektu Army wypełnionego początkowo bieżącą jednostką Unit. Moglibyśmy też wrócić do poprzedniego modelu (w którym nie rozróżnialiśmy pomiędzy obiektami kompozytów a liśćmi) i zrealizować to samo w metodzie Unit::addUnit(): możemy tam utworzyć obiekt Army i dodać do niego oba obiekty Unit. To eleganckie rozwiązanie,

190

ROZDZIAŁ 10.  WZORCE ELASTYCZNEGO PROGRAMOWANIA OBIEKTOWEGO

ale bazuje na założeniu, że z góry znany jest typ kompozytu użytego do agregowania jednostek. Założenia czynione przy projektowaniu metod takich jak getComposite() czy addUnit() są określane przez logikę biznesową systemu. Tego rodzaju kombinacje to w przypadku wzorca Composite osłabienie jego użyteczności. Jedna z jego ważniejszych zalet, prostota, uzyskana została przez dziedziczenie wszystkich klas wzorca po wspólnej klasie bazowej. Ta prostota osiągnięta została kosztem bezpieczeństwa typów. Im bardziej złożony staje się model, tym częściej trzeba w jego ramach sprawdzać ręcznie typy obiektów. Załóżmy, że całość uzupełnimy o obiekt klasy Cavalry. Jeśli reguły rozgrywki mówią, że kawalerii nie można przewozić transporterem, nie będziemy dysponować automatycznym sposobem ujęcia tego ograniczenia we wzorcu Composite: class TroopCarrier { function addUnit(Unit $unit) { if ($unit instanceof Cavalry) { throw new UnitException( "Transporter nie może przewozić koni"); } parent::addUnit($unit); }

}

function bombardStrength() { return 0; }

Jesteśmy tu zmuszeni do testowania typu obiektu przekazanego w wywołaniu metody addUnit() za pośrednictwem operatora instanceof. Im więcej takich jak ten przypadków specjalnych, tym wady wzorca będą dokuczliwsze. Wzorzec Composite działa najlepiej wtedy, kiedy większość komponentów to obiekty wymienialne, o zbliżonej semantyce. Kolejną kwestią jest koszt niektórych operacji w ramach wzorca. Typowym przykładem jest wywołanie Army::bombardStrength(), prowokujące kaskadę wywołań propagowanych w dół drzewa struktury jednostek zawieranych w oddziale. Przy mocno rozbudowanych drzewach z wieloma pododdziałami owo jedno wywołanie może sprowokować „w tle” istną lawinę wywołań. Co prawda koszt wykonania metody bombardStrength() nie jest obecnie wysoki, łatwo jednak sobie wyobrazić efekty skomplikowania obliczania siły ataku niektórych jednostek. Jednym ze sposobów eliminacji nawału wywołań i delegowania jest buforowanie wyników poprzednich wywołań metod obiektów zawieranych w obiektach-kompozytach, tak aby w przyszłych odwołaniach do tej wartości można było pominąć narzut wywołań. Ale wtedy trzeba pilnować aktualizacji buforowanych wartości, wdrażając strategię opróżniania buforów po operacjach na drzewie obiektów. Może to wymagać wyposażenia obiektów zawieranych w referencje do obiektów kompozytów. Wreszcie słowo o trwałości. Wzorzec Composite jest co prawda wyjątkowo elegancki, ale nie bardzo nadaje się do utrwalania zbudowanej struktury obiektów w bazie danych, a to dlatego, że całe struktury traktowane są jako pojedyncze obiekty. Aby więc skonstruować taką strukturę na podstawie informacji odczytywanych z bazy danych, trzeba posłużyć się serią kosztownych zapytań. Problem można wyeliminować, przypisując do całego drzewa identyfikator, tak aby można było jednym zapytaniem wyodrębnić z bazy danych wszystkie komponenty drzewa. Po wyodrębnieniu wszystkich obiektów trzeba będzie jednak i tak odtworzyć budowę drzewa, z zachowaniem układu obiektów podrzędnych i nadrzędnych, który również trzeba odzwierciedlić w schemacie bazy danych. Nie jest to zadanie bardzo trudne, ale mimo wszystko nieco skomplikowane. Przystosowanie wzorca Composite do baz danych jest wątpliwe, zupełnie inaczej ma się sprawa z językiem XML, a to dlatego, że w XML-u bardzo łatwo tworzyć drzewiaste struktury elementów.

Composite — podsumowanie Dowiedliśmy już przydatności wzorca Composite w zastosowaniach wymagających traktowania grup obiektów tak, jak wymagałby tego pojedynczy obiekt, czy to tam, gdzie zbiór czy grupa obiektów stanowi faktyczny komponent z racji swych cech wewnętrznych (jak w przypadku oddziałów i pojedynczych kuszników), czy też tam, gdzie to kontekst nadaje zbiorowi cechy jego składowych (jak w przypadku faktur i pozycji faktur). Struktury kompozytowe są — zgodnie z założeniami Composite — reprezentowane strukturami drzewiastymi,

191

PHP. OBIEKTY, WZORCE, NARZĘDZIA

więc operacje na całości mogą wpływać na poszczególne elementy, a dane z poszczególnych elementów mogą być kumulowane jako dane całości. Owa kumulacja danych i rozprowadzanie operacji w drzewie są dla użytkownika procesami niewidocznymi. Drzewa takie łatwo przeglądać (sprawdzimy to w następnym rozdziale). Struktury tworzone na bazie wzorca Composite łatwo można uzupełniać o nowe typy komponentów. Z drugiej strony wzorzec zakłada podobieństwo wszystkich komponentów. Kiedy pojawiają się zależności wymuszające szczególne traktowanie komponentów względem kompozytów, kod znacznie się komplikuje. Kompozyty nie nadają się poza tym do składowania w bazach danych. Za to łatwo reprezentują się w języku XML.

Wzorzec Decorator Tak jak wzorzec Composite okazuje się pomocny przy tworzeniu elastycznych reprezentacji agregatów komponentów, tak wzorzec Decorator (dekorator) dzięki podobnym strukturom ułatwia modyfikowanie zakresu funkcji konkretnych komponentów takich agregatów. I tym razem podstawą wzorca jest dynamiczna kompozycja obiektów. Dziedziczenie jest dobrym mechanizmem wymuszania cech klasy bazowej w klasach pochodnych, jednak równocześnie utrudnia wprowadzanie wariacji w ramach hierarchii dziedziczenia. Stąd nacisk na kompozycję w miejsce dziedziczenia.

Problem Wbudowywanie pełnego zakresu funkcji w strukturę dziedziczenia może doprowadzić do wybuchowego przyrostu klas w systemie. Co gorsza, próby wprowadzania podobnych modyfikacji w różnych gałęziach hierarchii dziedziczenia łatwo prowadzą do powielania kodu. Wróćmy do naszej gry. Zdefiniujemy klasę Tile (pole) reprezentującą fragment planszy wraz z jej klasą pochodną: abstract class Tile { abstract function getWealthFactor(); } class Plains extends Tile { private $wealthfactor = 2; function getWealthFactor() { return $this->wealthfactor; } }

Klasa Tile reprezentuje prostokąt na planszy, w którego granicach mogą stacjonować jednostki gracza. Każde pole ma swoją charakterystykę. W tym przykładzie zdefiniowaliśmy dla pola współczynnik zasobności i metodę getWealthFactor() zwracającą wartość dochodów, jaką przynosi graczowi zajmowanie danego pola. Jak widać, na równinach współczynnik zasobności ustalony jest na 2. Oczywiście pola zarządzają również innymi danymi. Mogą również przechowywać referencję do wizerunku terenu, umożliwiającą odrysowanie planszy na ekranie. Jak zwykle jednak staramy się maksymalnie upraszczać przykład. W czasie rozgrywki trzeba modyfikować zachowanie obiektu klasy Plains (równina) tak, aby można było uwzględniać w grze zmiany terenu powodowane działalnością jednostek gracza. Chcemy modelować występowanie cennych kruszców przy powierzchni terenu (DiamondPlains) i straty w zasobach naturalnych powodowane zanieczyszczeniem (PollutedPlains). Można by to zrealizować przez dziedziczenie po klasie Plains: class DiamondPlains extends Plains { function getWealthFactor() { return parent::getWealthFactor() + 2; } } class PollutedPlains extends Plains { function getWealthFactor() {

192

ROZDZIAŁ 10.  WZORCE ELASTYCZNEGO PROGRAMOWANIA OBIEKTOWEGO

return parent::getWealthFactor() - 4; } }

Teraz możemy łatwo konstruować obiekty terenów zanieczyszczonych: $tile = new PollutedPlains(); print $tile->getWealthFactor();

Diagram klas dla opisywanej implementacji prezentowany jest na rysunku 10.3.

Rysunek 10.3. Wbudowywanie modyfikacji cech w drzewo dziedziczenia Taka struktura na pewno nie jest elastyczna. Możemy generować pola zawierające bogactwa naturalne, możemy też umieszczać na planszy pola zniszczone zanieczyszczeniami. Ale czy można połączyć te cechy w jednym polu? Nie, jeśli nie zamierzamy wyprowadzać na tę okoliczność klasy PollutedDiamondPlains i tym podobnych potworków o jeszcze dłuższych i dziwaczniejszych nazwach. To oczywiście skrajny przykład, jednak w pełni ilustruje problem. Definiowanie zakresu funkcji wyłącznie w oparciu o dziedziczenie prowokuje zwielokrotnianie klas i powielanie kodu. Przejdźmy do bardziej typowego przykładu. Poważne aplikacje WWW w ramach formowania odpowiedzi na żądanie często muszą realizować różnorodne czynności. Mogą one obejmować uwierzytelnianie użytkownika czy rejestrowanie żądania w dzienniku. Niekiedy trzeba przetworzyć żądanie, budując na podstawie przekazanych na wejście danych złożone struktury. Wreszcie trzeba zrealizować podstawowe zadania aplikacji. I tym razem rozszerzenie zestawu funkcji klasy ProcessRequest (przetwarzanie żądania) można zaimplementować w postaci klas pochodnych LogRequest (rejestracja), StructureRequest (układanie danych) i AuthenticateRequest (uwierzytelnianie). Hierarchię takich klas widać na rysunku 10.4.

Rysunek 10.4. Różnicowanie zestawu funkcji dziedziczeniem

193

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Co będzie, jeśli w ramach poszczególnych żądań konieczne będzie rejestrowanie żądania i uwierzytelnianie klienta, ale bez przygotowania danych wejściowych? Czy będziemy na tę okoliczność wyprowadzać klasę LogAndAuthenticateProcessor ? Widać wyraźnie, że potrzebujemy rozwiązania znacznie bardziej elastycznego.

Implementacja Zamiast rozwiązywać problem różnicowania zakresu funkcjonalnego wyłącznie na bazie dziedziczenia, powinniśmy, zgodnie ze wzorcem Decorator, uciec się raczej do kompozycji i delegacji. Klasy wzorca Decorator (klasy dekoratorów) przechowują egzemplarze innych klas. Implementują też operacje, których wywołania są delegowane do obiektów klas przechowywanych, ale dopiero po wykonaniu (albo przed wykonaniem) własnych zadań dekoratora uzupełniających operację docelową. Można też dynamicznie konstruować całe ciągi dekoratorów uzupełniających właściwą operację. Spróbujmy tę koncepcję zastosować w przykładzie z grą: abstract class Tile { abstract function getWealthFactor(); } class Plains extends Tile { private $wealthfactor = 2; function getWealthFactor() { return $this->wealthfactor; } } abstract class TileDecorator extends Tile { protected $tile; function __construct( Tile $tile ) { $this->tile = $tile; } }

Klasy Tile i Plains zadeklarowaliśmy jak poprzednio, ale wprowadziliśmy też nowość — klasę dekoratora pola, TileDecotrator. Nie implementuje ona metody getWealthFactor(), musi więc być deklarowana jako abstrakcyjna. Konstruktor klasy wymaga przekazania obiektu klasy Tile i zapisuje referencję tego obiektu w składowej $tile. Składowa ta została zabezpieczona z zewnątrz, więc mają do niej dostęp wyłącznie klasy pochodne. Zdefiniujmy teraz klasy reprezentujące żyły złota i obszary zanieczyszczone: class DiamondDecorator extends TileDecorator { function getWealthFactor() { return $this->tile->getWealthFactor() + 2; } } class PollutionDecorator extends TileDecorator { function getWealthFactor() { return $this->tile->getWealthFactor() - 4; } }

Każda z tych klas dziedziczy po klasie TileDecorator. Oznacza to, że dziedziczą referencję obiektu pola. W momencie wywołania na rzecz obiektów prezentowanych klas metody getWealthFactor() obiekty te delegują wywołania do odpowiedniej metody klasy Tile, modyfikując zwracaną wartość przed jej przekazaniem do wywołującego. Stosując takie połączenie kompozycji z delegowaniem, możemy w prosty sposób kombinować obiekty w czasie wykonania programu. Ponieważ wszystkie obiekty uczestniczące w implementacji wzorca dziedziczą po klasie Tile, użytkownik nie musi rozpoznawać kombinacji, na której przychodzi mu operować. Wystarczy, że ma

194

ROZDZIAŁ 10.  WZORCE ELASTYCZNEGO PROGRAMOWANIA OBIEKTOWEGO

gwarancję możliwości wywołania metody getWealthFactor() dla dowolnego obiektu Tile — odbywające się zaś w tle ingerencje w wartość zwracaną zupełnie go nie interesują: $tile = new Plains(); print $tile->getWealthFactor(); // 2

Obiekt Plains to komponent, zwraca po prostu 2: $tile = new DiamondDecorator( new Plains ); print $tile->getWealthFactor(); // 4

Obiekt DiamondDecorator dysponuje referencją obiektu klasy Plains. Wywołuje jego metodę getWealthFactor(), a do wyniku dodaje własny współczynnik 2: $tile = new PollutionDecorator( new DiamondDecorator( new Plains() )); print $tile->getWealthFactor(); // 0 PollutionDecorator dysponuje referencją obiektu klasy DiamondDecorator, który z kolei posiada referencję obiektu klasy Tile. Diagram klas odpowiedni dla tego przykładu widnieje na rysunku 10.5.

Rysunek 10.5. Wzorzec Decorator Ten model cechuje się łatwością rozszerzania. Nic nie ogranicza swobody uzupełniania systemu o nowe klasy dekoracji i nowe komponenty. Mimo znacznej liczby klas dekoracji zachowana jest łatwość konstruowania elastycznych struktur w czasie wykonywania programu. Cechy klasy komponentu (tutaj Plains) mogą być rozmaicie modyfikowane bez potrzeby włączania owych modyfikacji do hierarchii klas. Oznacza to po prostu, że możemy na planszy rozmieszczać pola zanieczyszczonych równin z żyłami złota bez konieczności definiowania dla takich pól odrębnej klasy o zawiłej nazwie. Wzorzec Decorator pozwala na konstruowanie ciągów dekoracji, co okazuje się skuteczną i elastyczną metodą konstruowania filtrów. Świetnym przykładem użycia klas dekoracji byłaby biblioteka strumieni wejścia-wyjścia (w rodzaju java.io z języka Java). Użytkownik mógłby w niej łączyć obiekty właściwych komponentów z obiektami dekoracji, implementującymi np. filtrowanie, buforowanie, kompresję i inne rozszerzenia zakresu metod takich jak read(). W taki ciąg konfigurowalnych rozszerzeń da się wtłoczyć również nasz przykład z obsługą żądań w aplikacji WWW, jak tutaj: class RequestHelper{} abstract class ProcessRequest { abstract function process( RequestHelper $req ); } class MainProcess extends ProcessRequest { function process(RequestHelper $req) { print __CLASS__.": obsługa żądania\n";

195

PHP. OBIEKTY, WZORCE, NARZĘDZIA

} } abstract class DecorateProcess extends ProcessRequest { protected $processrequest; function __construct(ProcessRequest $pr) { $this->processrequest = $pr; } }

Definiujemy (jak poprzednio) abstrakcyjną klasę bazową (ProcessRequest), konkretny komponent (MainProcess) i abstrakcyjną klasę dekoratora (DecorateProcess). Metoda MainProcess::process() ogranicza się do zasygnalizowania faktu wywołania. Obiekt klasy DecorateProcess przechowuje w chronionej (i dostępnej obiektom klas pochodnych) składowej obiekt hierarchii ProcessRequest. Oto kilka konkretnych klas dekoracji: class LogRequest extends DecorateProcess { function process(RequestHelper $req) { print __CLASS__.": rejestracja żądania\n"; $this->processrequest->process($req); } } class AuthenticateRequest extends DecorateProcess { function process(RequestHelper $req) { print __CLASS__.": uwierzytelnienie żądania\n"; $this->processrequest->process($req); } } class StructureRequest extends DecorateProcess { function process(RequestHelper $req) { print __CLASS__.": układanie danych żądania\n"; $this->processrequest->process($req); } }

Każda z wersji metody process() implementowanych w klasach dekoracji wyprowadza na wyjście komunikat, a potem wywołuje metodę process() obiektu przechowywanego. W czasie wykonywania programu możemy te klasy wykorzystać do konstruowania dynamicznego ciągu obiektów, budując z nich filtry realizujące rozmaite czynności w różnej kolejności, jak poniżej, gdzie w jednym takim filtrze uczestniczą wszystkie klasy dekoracji: $process = new AuthenticateRequest(new StructureRequest( new LogRequest( new MainProcess() ))); $process->process(new RequestHelper());

Wykonanie powyższego kodu spowoduje wyprowadzenie na wyjście następującego ciągu komunikatów: AuthenticateRequest: uwierzytelnianie żądania StructureRequest: układanie danych żądania LogRequest: rejestracja żądania MainProcess: obsługa żądania

 Uwaga Ten przykład jest przy okazji wcieleniem wzorca korporacyjnego o nazwie Intercepting Filter opisanego w książce Core J2EE Patterns1. 1

J2EE. Wzorce projektowe, Helion, 2003 r.

196

ROZDZIAŁ 10.  WZORCE ELASTYCZNEGO PROGRAMOWANIA OBIEKTOWEGO

Konsekwencje Wzorzec Decorator, podobnie jak Composite, może mylić projektanta. Trzeba pamiętać, że mamy tu równocześnie dziedziczenie i kompozycję. LogRequest dziedziczy interfejs po ProcessRequest, ale występuje w roli koperty otaczającej inny obiekt hierarchii ProcessRequest. Obiekt dekorujący tworzy otoczkę wokół obiektu podrzędnego, trzeba więc zadbać o odpowiednio skromny interfejs. Jeśli klasa bazowa będzie nadmiernie obciążona funkcjami, obiekty klas dekoracji będą musiały delegować wywołania do wszystkich publicznych metod obiektu zawieranego. Można tę delegację przenieść do abstrakcyjnej klasy bazowej dekoracji, ale mimo to wprowadzony zostanie podatny na błędy sprzęg. Niektórzy programiści tworzą obiekty klasy dekoracji o typie innym niż typ dekorowanego obiektu. Strategia taka działa znakomicie, o ile zadba się o honorowanie w obu typach identycznego interfejsu. Delegację można zautomatyzować za pośrednictwem odpowiednio zaimplementowanych metod przechwytujących chybione wywołania i kierujących wywołania do odpowiednich metod klas pochodnych. Tym samym jednak rezygnujemy z bezpieczeństwa wynikającego z kontroli typów. W naszych dotychczasowych przykładach kod użytkownika mógł wymusić przekazanie obiektu klasy Tile czy ProcessRequest w deklaracji listy argumentów metody i zyskać dzięki kontroli typów pewność co do implementacji interfejsu, niezależnie od bogactwa ewentualnej dekoracji obiektu.

Wzorzec Facade Być może Czytelnik miał już przyjemność włączać do swoich projektów systemy autorstwa osób trzecich. Tego rodzaju operacje są często nużące, kłopotliwe i czasochłonne, nawet jeśli rzeczony system ma strukturę obiektową. Z drugiej strony również nasz własny kod może się okazać wyzwaniem dla użytkującego go programisty, który musi się borykać z naszymi pomysłami, chcąc skorzystać z niewielkiego ułamka możliwości naszego systemu. Rozwiązaniem tego rodzaju problemów jest wdrożenie wzorca Facade (fasada) zakładającego udostępnianie prostych i zwartych interfejsów dla złożonych systemów.

Problem Systemy informatyczne mają to do siebie, że w miarę ich rozwoju coraz mniejsza część ich kodu jest użyteczna spoza systemu, coraz większa zaś implementuje jego zadania wewnętrzne. Skoro jednak klasy mogą definiować zwarty interfejs, a implementację chować przed otoczeniem, dlaczego nie przyjąć takiej strategii również dla całych systemów? Problem w tym, że nie zawsze wiadomo, które z części systemu są przeznaczone do publicznego użytku, a które powinny pozostać jego prywatną własnością. Gdy operuje się systemami (np. implementacją forum czy galerii WWW), można się przyłapać na wykonywaniu wywołań prowadzących do samych trzewi logiki podsystemu. Jeśli w takim układzie rzeczony podsystem ulegnie nieuniknionym w czasie zmianom, nasza interakcja z jego czeluściami może zostać w znacznej mierze unieważniona — słowem, w miarę rozwoju podsystemu coraz trudniej będzie synchronizować zmiany w kodzie go użytkującym. Nie czyń drugiemu, co tobie niemiłe — pisząc własny podsystem przeznaczony do użytku powszechnego, powinieneś wydzielić w nim wyraźnie rozróżnialne warstwy. Typowo jedna z takich warstw odpowiada za logikę aplikacji, inna za komunikację z bazami danych, jeszcze inna za prezentację danych itd. Zależności pomiędzy tymi warstwami powinny być jak najsłabsze, tak aby zmiany w jednej z nich miały minimalny oddźwięk w pozostałych. Jeśli sprzęg pomiędzy warstwami miałby być ścisły, równie dobrze z warstw można by zrezygnować. Poniżej prezentuję celowo mylący kod proceduralny, który bardzo wikła prosty skądinąd proces pobierania informacji z pliku dziennika i ich konwersji na postać obiektów:

197

PHP. OBIEKTY, WZORCE, NARZĘDZIA

function getProductFileLines($file) { return file($file); } function getProductObjectFromId($id, $productname) { // przeszukiwanie bazy danych… return new Product($id, $productname); } function getNameFromLine($line) { if (preg_match("/.*-(.*)\s\d+/", $line, $array)) { return str_replace('_', ' ', $array[1]); } return ''; } function getIDFromLine($line) { if (preg_match("/^(\d{1,3})-/", $line, $array)) { return $array[1]; } return -1; } class Product { public $id; public $name; function __construct($id, $name) { $this->id = $id; $this->nazwa = $name; } }

Wyobraźmy sobie, że struktura i treść kodu są jeszcze bardziej złożone i że przez to nie ma szans na ich przepisanie na własną rękę — pozostaje nam jedynie korzystanie z rozwiązania kalekiego, ale gotowego. Aby skonwertować plik o zawartości: 234-bluza_damska 55 532-kapelusz_męski 55

na postać tablicy obiektów, musimy wywołać wszystkie z prezentowanych funkcji (dla uproszczenia pomijamy przy tym wyodrębnianie z pliku liczby kończącej wiersz, reprezentującej cenę artykułu): $lines = getProductFileLines('test.txt'); $objects = array(); foreach ($lines as $line) { $id = getIDFromLine($line); $name = getNameFromLine($line); $objects[$id] = getProductObjectFromID($id, $name); }

Jeśli będziemy w swoim projekcie stosować powyższą procedurę odczytu plików, zwiążemy ściśle kod projektu z kodem podsystemu wczytywania plików. Jeśli potem ów podsystem ulegnie zmianom albo zdecydujemy się na skorzystanie z innego podsystemu w jego miejsce, staniemy w obliczu problemu rozległych modyfikacji kodu projektu. Trzeba by więc powołać do życia bramkę pomiędzy tym podsystemem a resztą projektu.

198

ROZDZIAŁ 10.  WZORCE ELASTYCZNEGO PROGRAMOWANIA OBIEKTOWEGO

Implementacja Oto prosta klasa udostępniająca interfejs do kodu proceduralnego, z którym borykaliśmy się w poprzednim punkcie: class ProductFacade { private $products = array(); function __construct($file) { $this->file = $file; $this->compile(); } private function compile() { $lines = getProductFileLines($this->file); foreach ($lines as $line) { $id = getIDFromLine($line); $name = getNameFromLine($line); $this->products[$id] = getProductObjectFromID($id, $name); } } function getProducts() { return $this->products; } function getProduct($id) { if (isset($this->products[$id])) { return $this->products[$id]; } return null; } }

Z punktu widzenia użytkownika tego kodu dostęp do obiektów Product generowanych na podstawie pliku rejestru produktów jest znacznie uproszczony: $facade = new ProductFacade('test.txt'); $facade->getProduct(234);

Konsekwencje Wzorzec projektowy Facade reprezentuje doprawdy prostą koncepcję. Chodzi w nim jedynie o utworzenie pojedynczego punktu dostępu do warstwy czy podsystemu. Jego wdrożenie daje wiele korzyści. Przede wszystkim zmniejsza zależności pomiędzy rozłącznymi funkcjonalnie częściami projektu i wykorzystywanymi w nim podsystemami. Użytkownicy utworzonej bramki chwalą sobie prostotę korzystania z części systemu. Dalej, odwołania do docelowego podsystemu są skupione w jednym miejscu kodu, dzięki czemu zmiany wprowadzane do podsystemu mają bardzo ograniczony oddźwięk. Korzyścią jest również zmniejszenie ilości błędów, które mogłyby być wynikiem bezpośredniego wykorzystywania wnętrzności podsystemu bez pełnego zrozumienia zachodzących w nim zależności — gdy używa się wzorca Facade, znajomość tych zależności wymagana jest jedynie od twórcy bramki. Prostota wzorca ma jednak też wady — przede wszystkim bardzo łatwo zapomnieć o jego stosowaniu, zwłaszcza kiedy podsystemy, na których operujemy, są nam znane i nie odczuwamy potrzeby uproszczenia ich interfejsu. Trzeba też wyważyć pomiędzy prostotą a wiernością. Z jednej strony mamy bowiem oczywiste korzyści wynikające z uproszczenia interfejsów obsługujących złożone systemy. Z drugiej zaś strony uproszczenie może iść za daleko i powstaną abstrakcje abstrakcji. Jeśli jednak wdrożenie wzorca miałoby dać efekt w postaci uproszczenia kodu użytkującego podsystem i (lub) ochrony projektu przed zmianami tego podsystemu, wtedy jest on jak najbardziej na miejscu.

199

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Podsumowanie W rozdziale przyjrzeliśmy się kilku nowym sposobom organizacji klas i obiektów w systemie. Szczególny nacisk położyliśmy na elastyczność wynikającą z zastąpienia dziedziczenia kompozycją. W dwu wzorcach omawianych w pierwszej kolejności — Composite i Decorator — dziedziczenie służy do promowania kompozycji i jest środkiem wymuszania wspólnego interfejsu zabezpieczającego użytkownika. W obu wzorcach obserwowaliśmy skuteczne zastosowanie delegacji wywołań. Na końcu czytelnicy poznali prosty, acz efektywny wzorzec Facade. To jeden z tych wzorców, które stosuje się latami niemal rutynowo, nie przypisując im szczególnego znaczenia ani nie obmyślając dla nich nazw. Tymczasem interfejsy fasadowe pozwalają na tworzenie jednolitych, zwartych i uproszczonych punktów wejścia do podsystemów. W języku PHP wzorzec Facade służy też do tworzenia obiektów kopertujących bloki kodu proceduralnego.

200

ROZDZIAŁ 11 

Reprezentacja i realizacja zadań

W niniejszym rozdziale zaczniemy wreszcie działać i przyjrzymy się wzorcom projektowym, które są pomocne w wykonywaniu zadań — od interpretacji minijęzyków po hermetyzacje algorytmów. Rozdział poświęcony będzie:  Wzorcowi Interpreter — umożliwiającemu konstruowanie interpreterów minijęzyków nadających się do wbudowywania w aplikacje interfejsów skryptowych.  Wzorcowi Strategy — zakładającemu identyfikowanie algorytmów stosowanych w systemie i ich hermetyzację do postaci osobnych, własnych typów.  Wzorcowi Observer — tworzącemu zaczepy umożliwiające powiadamianie obiektów o zdarzeniach zachodzących w systemie.  Wzorcowi Visitor — rozwiązującemu problem aplikacji wykonania określonej operacji na wszystkich węzłach drzewa obiektów.  Wzorcowi Command — czyli obiektom poleceń przekazywanym pomiędzy częściami systemu.

Wzorzec Interpreter Języki programowania powstają i są rozwijane (przynajmniej z początku) w innych językach programowania. PHP został na przykład „spisany” w języku C. Nic więc nie stoi na przeszkodzie, abyśmy za pomocą PHP zdefiniowali i wykorzystywali własny język programowania. Oczywiście każdy utworzony tak język będzie powolny i dość ograniczony, ale nie oznacza to, że będzie bezużyteczny — minijęzyki są całkiem przydatne, co postaram się zademonstrować w tym rozdziale.

Problem Tworząc interfejsy WWW (ale również interfejsy wiersza poleceń) w języku PHP, dajemy użytkownikowi dostęp do pewnego zestawu funkcji. Zawsze w takim przypadku stajemy przed wyborem pomiędzy prostotą korzystania z interfejsu a zakresem możliwości oddawanych w ręce użytkownika. Im więcej możliwości dla użytkownika, tym z reguły bardziej złożony i rozdrobniony interfejs. Bardzo pomocne jest tu staranne zaprojektowanie interfejsu i rozpoznanie potrzeb użytkowników — jeśli 90% z nich wykorzystuje jedynie 30% (tych samych) funkcji systemu, koszt udostępniania maksimum funkcjonalności może okazać się za wysoki w stosunku do efektów. Można wtedy rozważyć uproszczenie systemu pod kątem „przeciętnego” użytkownika. Ale co wtedy z owymi 10% użytkowników zaawansowanych korzystających z kompletu zaawansowanych funkcji systemu? Ich potrzeby można by zaspokoić inaczej, na przykład udostępniając im wewnętrzny język programowania typu DSL (od ang. Domain Specific Language, język specjalizowany), w którym będą mogli odwoływać się do wszystkich funkcji systemu.

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Mamy już co prawda pod ręką jeden język programowania. Chodzi o PHP. Moglibyśmy więc udostępnić go użytkownikom i pozwolić im na tworzenie własnych skryptów: $form_input = $_REQUEST['form_input']; // zmienna zawiera: "print file_get_contents('/etc/passwd');" eval( $form_input );

Jednakże takie rozszerzenie dostępności systemu wydaje się szaleństwem. Jeśli Czytelnik nie jest przekonany o nonsensowności tego pomysłu, powinien przypomnieć sobie o dwóch kwestiach: bezpieczeństwie i złożoności. Kwestia bezpieczeństwa jest dobrze ilustrowana w naszym przykładzie — umożliwiając użytkownikom uzupełnianie systemu o ich własny kod w języku PHP, dajemy im pełny dostęp do serwera, na którym działa nasza aplikacja. Równie dużym problemem jest jednak złożoność — niezależnie od przejrzystości kodu aplikacji przeciętny użytkownik będzie miał problemy z jej rozszerzeniem, zwłaszcza jeśli ma z nią kontakt jednie za pośrednictwem okna przeglądarki. Problemy te można wyeliminować, opracowując i udostępniając użytkownikom własny minijęzyk. Można w nim połączyć elastyczność, zredukować możliwość wyrządzania szkód przez użytkowników i równocześnie zadbać o zwartość całości. Wyobraźmy sobie aplikację do tworzenia quizów. Autorzy mieliby układać pytania i ustalać reguły oznaczania poprawności odpowiedzi udzielanych przez uczestników quizu. Chodziłoby o to, żeby quizy toczyły się bez interwencji operatora, choć część odpowiedzi miałaby być wprowadzana przez uczestników w polach tekstowych. Oto przykładowe pytanie: Ilu członków liczy banda Design Patterns?

Poprawnymi odpowiedziami są „cztery” albo „4”. Możemy utworzyć interfejs WWW, który pozwala twórcy quizu angażować do rozpoznawania poprawnych odpowiedzi wyrażenia regularne: ^4|cztery$

Jednak od twórców quizów rzadko wymaga się biegłości w konstruowaniu wyrażeń regularnych — są oni cenieni raczej ze względu na wiedzę ogólną. Aby uprościć im życie, można więc zaimplementować przyjaźniejszy mechanizm rozpoznawania poprawnych odpowiedzi: $input equals "4" or $input equals "cztery"

Mamy tu propozycję języka programowania obsługującego zmienne, operator o nazwie equals oraz operacje logiczne (or czy and). Programiści uwielbiają nadawać nazwy swoim dziełom, nadajmy więc językowi miano — MarkLogic. Język miałby być łatwo rozszerzalny, bo już oczyma wyobraźni widzimy postulaty zwiększenia jego możliwości. Odłóżmy chwilowo na bok kwestię analizy leksykalnej, skupiając się na mechanizmie wykorzystania języka w czasie wykonania do generowania ocen odpowiedzi. Tu właśnie zastosowanie znajdzie wzorzec Interpreter.

Implementacja Nasz język składa się z wyrażeń (to znaczy elementów, dla których da się obliczyć wartości). Z tabeli 11.1 wynika jasno, że nawet tak prosty język jak MarkLogic musi uwzględniać wiele elementów. Tabela 11.1. Elementy gramatyki języka MarkLogic Opis

Nazwa w notacji EBNF

Nazwa klasy

Przykład

Zmienna

variable

VariableExpression

$input

Literał łańcuchowy

LiteralExpression

"cztery"

Logiczne i

andExpr

BooleanAndExpression

$input equals '4' and $other equals '6'

Logiczne lub

orExpr

BooleanOrExpression

$input equals '4' or $other equals '6'

Test równości

eqExpr

EqualsExpression

$input equals '4'

202

ROZDZIAŁ 11.  REPREZENTACJA I REALIZACJA ZADAŃ

W tabeli 11.1 mamy między innymi kolumnę nazw EBNF. Cóż to za nazwy? To notacja wykorzystywana do opisu gramatyki języka. EBNF to skrót od Extended Backus-Naur Form (rozszerzona notacja Backusa-Naura). Notacja ta składa się z szeregu wierszy (zwanych regułami produkcyjnymi), w których znajdują się nazwa i opis przyjmujący postać odniesień do innych reguł produkcyjnych (ang. productions) i symboli końcowych (ang. terminals), których nie da się już wyrazić odwołaniami do kolejnych reguł produkcyjnych. Naszą gramatykę w notacji EBNF można by zapisać następująco: expr ::= operand (orExpr | andExpr)* operand ::= ( '(' expr ')' | | variable ) (eqExpr)* orExpr ::= 'or' operand andExpr ::= 'and' operand equalsExpr ::= 'equals' operand variable ::= '$'

Niektóre z symboli mają znaczenie specjalne (znane z notacji wyrażeń regularnych): na przykład gwiazdka (*) oznacza zero lub więcej wystąpień, a pionowa kreska (|) to to samo co w języku naturalnym „lub”. Elementy grupujemy za pośrednictwem nawiasów. W powyższym przykładzie wyrażenie ( expr) składa się z operandu (operand), z którym występuje zero lub więcej wyrażeń logicznej sumy (orExpr) bądź logicznego iloczynu (andExpr). Operand może być wyrażeniem ujętym w nawiasy, ciągiem ograniczonym znakami cudzysłowu (tej reguły produkcyjnej nie ma co prawda w powyższym przykładzie) albo zmienną (variable), za którymi mogą znajdować się wystąpienia dowolnej liczby eqExpr. Jeśli przyzwyczaić się do ciągłego odsyłania od jednej reguły produkcyjnej do kolejnej, notacja EBNF staje się całkiem poręczna. Na rysunku 11.1 mamy prezentację elementów gramatyki w postaci klas.

Rysunek 11.1. Klasy wzorca Interpreter obsługujące język MarkLogic Jak widać, klasa BooleanAndExpression i jej „rodzeństwo” dziedziczą po klasie OperatorExpression. Wszystkie te klasy realizują bowiem operacje na obiektach wyrażeń (obiektach klasy Expression). Klasy VariableExpression i LiteralExpression operują wprost na wartościach. Wszystkie obiekty hierarchii Expression implementują metodę interpret() zdefiniowaną w abstrakcyjnej klasie bazowej hierarchii, czyli właśnie w klasie Expression. Metoda ta oczekuje przekazania w wywołaniu obiektu klasy InterpreterContext wykorzystywanego w roli wspólnego repozytorium danych. Każdy obiekt klasy Expression może składować dane w obiekcie klasy InterpreterContext, który jest przekazywany pomiędzy

203

PHP. OBIEKTY, WZORCE, NARZĘDZIA

obiektami hierarchii Expression. Aby dało się w prosty sposób wyodrębniać dane z obiektu InterpreterContext, klasa bazowa Expression implementuje metodę getKey() zwracającą unikalny uchwyt. Zobaczmy, jak całość działa w praktyce z implementacjami abstrakcji Expression: abstract class Expression { private static $keycount=0; private $key; abstract function interpret(InterpreterContext $context); function getKey() { if (! isset($this->key)) { self::$keycount++; $this->key=self::$keycount; } return $this->key; } } class LiteralExpression extends Expression { private $value; function __construct($value) { $this->value = $value; } function interpret(InterpreterContext $context) { $context->replace($this, $this->value); } } class InterpreterContext { private $expressionstore = array(); function replace(Expression $exp, $value) { $this->expressionstore[$esp->getKey()] = $value; } function lookup(Expression $exp) { return $this->expressionstore[$exp->getKey()]; } } $context = new InterpreterContext(); $literal = new LiteralExpression('cztery'); $literal->interpret($context); print $context->lookup($literal) . "\n";

Na wyjściu programu otrzymamy: cztery

Zacznijmy od klasy InterpreterContext. Jak widać, jest ona w istocie jedynie fasadą tablicy asocjacyjnej reprezentowanej składową $expressionstore i służącej do przechowywania danych. Metoda replace() klasy Context przyjmuje na wejście obiekt klasy Expression, który występuje w roli klucza tablicy asocjacyjnej, oraz wartość dowolnego typu lądującą w tablicy asocjacyjnej w parze z przekazanym kluczem. Klasa udostępnia również metodę lookup() umożliwiającą odczyt zapisanych w tablicy danych. Klasa Expression definiuje abstrakcyjną metodę interpret() i konkretną metodę getKey(), która na bazie statycznego licznika generuje, przechowuje i zwraca identyfikator. Metoda ta jest wykorzystywana w metodach InterpreterContext::lookup() i InterpreterContext::replace() do indeksowania danych.

204

ROZDZIAŁ 11.  REPREZENTACJA I REALIZACJA ZADAŃ

Klasa LiteralExpression definiuje konstruktor przyjmujący wartość dowolnego typu zapisywaną w składowej $value. Metoda interpret() klasy wymaga zaś przekazania obiektu klasy InterpreterContext. Jej implementacja sprowadza się do wywołania metody InterpreterContext::replace() z przekazaniem w wywołaniu wartości zwracanej przez metodę getKey() i wartością składowej $value. Schemat taki będziemy obserwować w pozostałych klasach wyrażeń. Metoda interpret() zawsze wypisuje wyniki swojego działania za pośrednictwem obiektu InterpreterContext. W prezentowanym kodzie nie zabrakło przykładowego kodu użytkującego klasy, konkretyzującego obiekty klas InterpreterContext i LiteralExpression (z wartością "cztery"), a następnie przekazującego obiekt InterpreterContext do wywołania LiteralExpression::interpret(). Metoda ta zapisuje parę klucz i wartość w obiekcie InterpreterContext, z którego można ją później wyodrębnić wywołaniem lookup(). Zdefiniujmy pozostałe klasy symboli końcowych naszej gramatyki. Klasa VariableExpression jest już nieco bardziej złożona: class VariableExpression extends Expression { private $name; private $val; function __construct($name, $val=null) { $this->name = $name; $this->val = $val; } function interpret(InterpreterContext $context) { if (!is_null($this->val)) { $context->replace($this, $this->val); $this->val = null; } } function setValue($value) { $this->val = $value; } function getKey() { return $this->name; } } $context = new InterpreterContext(); $myvar = new VariableExpression('input', 'cztery'); $myvar->interpret($context); print $context->lookup($myvar). "\n"; // wypis: // cztery $newvar = new VariableExpression('input'); $newvar->interpret($context); print $context->lookup($newvar) . "\n"; // wypis: // cztery $myvar->setValue("pięć"); $myvar->interpret($context); print $context->lookup($myvar) . "\n"; // wypis: // pięć print $context->lookup($newvar) . "\n"; // wypis: // pięć

205

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Klasa VariableExpression przyjmuje przy konstrukcji parę wartości: nazwę zmiennej i jej wartość. Udostępnia też metodę setValue(), aby użytkownicy mogli przypisywać do zmiennych nowe wartości. Metoda interpret() sprawdza przede wszystkim, czy składowa $val obiektu ma wartość niepustą. Jeśli tak, wartość ta jest zapisywana w obiekcie InterpreterContext, po czym do składowej $val przypisywana jest wartość pusta, na wypadek, gdyby metoda interpret() została ponownie wywołana po tym, jak inny egzemplarz VariableExpression o tej samej nazwie zmienił wartość zapisaną w obiekcie InterpreterContext. W rozszerzeniach języka trzeba by przewidzieć operowanie na obiektach Expression, tak aby zmienna mogła zawierać wyniki testów i operacji. Na razie jednak taka implementacja VariableExpression jest wystarczająca. Zauważmy, że przesłoniliśmy w niej implementację getKey(), tak aby wartość klucza konstytuowana była nie identyfikatorem egzemplarza klasy, a ciągiem zapisanym w składowej $name. Wyrażenia operatorów w naszym języku każdorazowo operują na dwóch obiektach Expression (obsługujemy bowiem wyłącznie operatory dwuargumentowe). Zasadne jest więc wyprowadzenie ich ze wspólnej klasy bazowej. Oto klasa OperatorExpression: abstract class operatorExpression extends Expression { protected $l_op; protected $r_op; function __construct(Expression $l_op, Expression $r_op) { $this->l_op = $l_op; $this->r_op = $r_op; } function interpret(InterpreterContext $context) { $this->l_op->interpret($context); $this->r_op->interpret($context); $result_l = $context->lookup($this->l_op); $result_r = $context->lookup($this->r_op); $this->doInterpret($context, $result_l, $result_r); } protected abstract function doInterpret( InterpreterContext $context, $result_l, $result_r); }

Klasa OperatorExpression to klasa abstrakcyjna. Implementuje co prawda metodę interpret(), ale definiuje również abstrakcyjną metodę doInterpret(). Konstruktor oczekuje przekazania dwóch obiektów klasy Expression: $l_op i $r_op, do których referencje zapisywane są w zabezpieczonych składowych obiektu. Implementacja metody interpret() rozpoczyna się od wywołania interpret() na rzecz obu operandów (jeśli pamiętasz poprzedni rozdział, zauważysz tu zapewne zastosowanie wzorca Composite). Po ewaluacji operandów metoda interpret() musi pozyskać zwracane przez nie wartości. Odwołuje się do niech za pośrednictwem metody InterpreterContext::lookup() wywoływanej dla obu składowych. Dalej następuje wywołanie doInterpret(), o którego wyniku decyduje jednak implementacja w klasach pochodnych.  Uwaga Metoda doInterpret() to wcielenie wzorca projektowego Template Method (metoda szablonowa). Według tego wzorca klasa nadrzędna zarówno definiuje, jak i wywołuje (używa) metodę abstrakcyjną, natomiast jej implementację pozostawia klasom pochodnym. W ten sposób można uprościć programowanie konkretnych klas poprzez zgromadzenie wspólnej logiki działania w klasie nadrzędnej i pozostawienie klasom pochodnym jedynie szczegółów charakterystycznych dla ich konkretnej specyfiki.

206

ROZDZIAŁ 11.  REPREZENTACJA I REALIZACJA ZADAŃ

Spójrzmy na jej implementację w klasie EqualsExpression, porównującej dwa obiekty klasy Expression: class EqualsExpression extends OperatorExpression { protected function doInterpret( InterpreterContext $context, $result_l, $result_r) { $context->replace($this, $result_l == $result_r); } }

Klasa EqualsExpression implementuje jedynie metodę doOperation(), w ramach której porównuje wartości operandów przekazanych z metody interpret() klasy nadrzędnej, a wynik porównania umieszcza w przekazanym obiekcie klasy InterpreterContext. Implementację klas wyrażeń wieńczą klasy wyrażeń logicznych — BooleanOrExpression i BooleanAndExpression: class BooleanOrExpression extends OperatorExpression { protected function doInterpret( InterpreterContext $result_l, $result_r) { $context->replace($this, $result_l || $result_r); } } class BooleanAndExpression extends OperatorExpression { protected function doInterpret( InterpreterContext $result_l, $result_r) { $context->replace($this, $result_l && $result_r); } }

Zamiast sprawdzania równości aplikujemy tu operatory operacji logicznej sumy (w BooleanOrExpression) bądź logicznego iloczynu (BooleanAndExpression). Wynik operacji jest przekazywany do metody InterpreterContext::replace(). Mamy już bazę kodu wystarczającą do wykonania prezentowanego wcześniej fragmentu kodu naszego minijęzyka. Oto on: $input equals "4" or $input equals "cztery"

Powyższe wyrażenie możemy odwzorować w hierarchii Expression w sposób następujący: $context = new InterpreterContext(); $input = new VariableExpression('input'); $statement = new BooleanOrExpression( new EqualsExpression($input, new LiteralExpression('cztery')), new EqualsExpression($input, new LiteralExpression('4')), );

Konkretyzujemy tu zmienną o nazwie input, ale wstrzymujemy się z przypisaniem jej wartości. Następnie tworzymy obiekt wyrażenia sumy logicznej BooleanExpression operującego na wynikach dwóch porównań realizowanych przez obiekty EqualsExpression. W pierwszym porównaniu uczestniczą: obiekt wyrażenia wartości (ValueExpression) przechowywanej w $input z obiektem ciągu znaków (LiteralExpression) zawierającym ciąg "cztery"; w drugim porównywany jest ten sam obiekt wartości $input z ciągiem znaków "4". Po takim rozpracowaniu wyrażenia z przykładowego wiersza kodu możemy przystąpić do obliczenia wartości zmiennej input i uruchomienia mechanizmu oceny: foreach (array("cztery", "4", "52") as $val) { $input->setValue($val); print "$val:\n";

207

PHP. OBIEKTY, WZORCE, NARZĘDZIA

$statement->interpret($context); if ($context->lookup($statement)) { print "Znakomita odpowiedź\n\n"; } else { print "Do oślej ławki\n\n"; } }

Mamy tu trzykrotne uruchomienie tego samego kodu, dla trzech różnych wartości zmiennej wejściowej. Za pierwszym razem ustawiamy tymczasową zmienną $val na "cztery", przypisując ją następnie do obiektu VariableExpression za pośrednictwem metody setValue(). Dalej wywołujemy metodę interpret() na rzecz szczytowego obiektu Expression (obiektu BooleanOrExpression zawierającego referencję do pozostałych obiektów wyrażeń uczestniczących w instrukcji). Spójrzmy na sposób realizacji tego wywołania:  Obiekt $statement wywołuje metodę intepret() na rzecz składowej $l_op (pierwszego obiektu klasy EqualsExpression).  Pierwszy z obiektów EqualsExpression wywołuje z kolei metodę interpret() na rzecz swojej składowej $l_op (referencji do obiektu VariableExpression przechowującego wartość „cztery”).  Obiekt VariableExpression zapisuje swoją bieżącą wartość do wskazanego obiektu klasy InterpreterContext (wywołaniem InterpreterContext::replace()).  Pierwszy z obiektów EqualsExpression wywołuje metodę interpret() na rzecz swojej składowej $r_op (referencji do obiektu LiteralExpression inicjowanego wartością "cztery").  Obiekt LiteralExpression rejestruje właściwą dla siebie parę klucz i wartość w obiekcie InterpreterContext.  Pierwszy z obiektów EqualsExpression odczytuje wartości $l_op ("cztery") i $r_op ("cztery") z obiektu InterpreterContext.  Pierwszy z obiektów EqualsExpression porównuje odczytane w poprzednim kroku wartości i rejestruje wynik porównania (true) wraz z właściwym sobie kluczem w obiekcie InterpreterContext.  Po powrocie w górę drzewa obiektów następuje wywołanie metody interpret() na rzecz składowej $r_op obiektu $statement. Wartość tego wywołania (tym razem false) obliczana jest identycznie jak dla pierwszego obiektu $l_op.  Obiekt $statement odczytuje wartości swoich operandów z obiektu InterpreterContext i porównuje je za pośrednictwem operatora ||. Suma logiczna wartości true i false daje true i taka wartość jest ostatecznie składowana w obiekcie InterpreterContext. Cały ten proces to zaledwie pierwsza iteracja pętli. Oto wynik wykonania wszystkich trzech przebiegów: cztery: Znakomita odpowiedź 4: Znakomita odpowiedź 52: Do oślej ławki

Być może zrozumienie tego, co dzieje się w powyższym kodzie, wymagać będzie kilkukrotnej lektury opisu — znów mamy bowiem do czynienia z pomieszaniem pomiędzy drzewami klas a hierarchiami obiektów. Klasy wyrażeń tworzą hierarchię dziedziczenia Expression, ale równocześnie obiekty tych klas są w czasie wykonywania formowane w strukturę drzewiastą. Należy jednak pamiętać o rozróżnieniu obu hierarchii. Kompletny diagram klas dla tego przykładu prezentowany jest na rysunku 11.2.

208

ROZDZIAŁ 11.  REPREZENTACJA I REALIZACJA ZADAŃ

Rysunek 11.2. Wdrożenie wzorca Interpreter

Ciemne strony wzorca Interpreter Po ułożeniu rdzenia hierarchii klas wzorca Interpreter jego rozbudowa jest już dość prosta, odbywa się jednak przez tworzenie coraz to nowych klas. Z tego względu wzorzec Interpreter najlepiej stosować do implementacji języków stosukowo uproszczonych. W obliczu potrzeby pełnoprawnego języka programowania należałoby raczej skorzystać z gotowych narzędzi przeznaczonych do analizy leksykalnej i implementacji własnej gramatyki. Dalej, klasy wzorca Interpreter często realizują bardzo podobne zadania, warto więc pilnować, aby nie dochodziło w nich do niepotrzebnego powielania kodu. Wiele osób, przymierzając się do pierwszego wdrożenia wzorca Interpreter, rozczarowuje się odkryciem faktu, że wzorzec ten nie obejmuje analizy leksykalnej. Oznacza to, że nie wystarczy on do implementacji gotowego mechanizmu skryptowego rozszerzania aplikacji. Przykładowa implementacja analizy leksykalnej mocno uproszczonego języka prezentowana jest w dodatku B.

Wzorzec Strategy Klasy często obciążane są nadmierną liczbą zadań. To zrozumiałe: niemal zawsze tworzymy je z myślą o kilku podstawowych funkcjach. W trakcie kodowania okazuje się, że niektóre z tych funkcji trzeba zmieniać w zależności od okoliczności. Oznacza to konieczność podziału klasy na podklasy. I zanim się ktokolwiek obejrzy, dojdzie do rozdarcia projektu.

Problem Ponieważ zdołaliśmy ostatnio opracować implementację miniaturowego języka oceny, trzymajmy się przykładu z quizami. Quizy nie mogą się obejść bez pytań, skonstruujemy więc klasę Question (pytanie) i wyposażymy ją w metodę oceny — mark(). Wszystko w porządku, dopóki nie pojawi się potrzeba obsługiwania różnych mechanizmów oceniania. 209

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Załóżmy, że mamy zaimplementować ocenę wedle języka MarkLogic, ocenę na podstawie prostego dopasowania odpowiedzi i ocenę z dopasowaniem przy użyciu wyrażeń regularnych. W pierwszym podejściu moglibyśmy zróżnicować projekt pod kątem tych mechanizmów oceny, jak na rysunku 11.3.

Rysunek 11.3. Definiowanie klas pochodnych wedle strategii oceny Całość będzie się sprawdzać dopóty, dopóki ocena pozostanie jedynym zmiennym aspektem hierarchii. Wyobraźmy sobie jednak, że zażądano od nas dodatkowo obsługi różnego rodzaju pytań: czysto tekstowych i opartych na materiale audiowizualnym. Powstaje problem uwzględnienia dwóch kierunków zmian w jednym drzewie dziedziczenia — patrz rysunek 11.4.

Rysunek 11.4. Wyróżnianie klas pochodnych według dwóch kryteriów podziału Nie tylko doszło do podwojenia (niemal) liczby klas w hierarchii, ale i do powielenia kodu. Nasza logika oceniania jest bowiem powielona w obu podgałęziach hierarchii dziedziczenia. Jeśli kiedykolwiek staniesz w obliczu powielania algorytmu w równoległych gałęziach hierarchii dziedziczenia (powielania tak przez wydzielanie klas pochodnych, jak i rozbudowywanie instrukcji warunkowych), powinieneś rozważyć wyodrębnienie algorytmu do jego własnego typu.

210

ROZDZIAŁ 11.  REPREZENTACJA I REALIZACJA ZADAŃ

Implementacja Wzorzec Strategy (strategia), podobnie jak cała gama najlepszych wzorców, łączy prostotę z wielkimi możliwościami. Kiedy klasy muszą obsługiwać wielorakie implementacje interfejsu (u nas są to wielorakie mechanizmy oceny), wzorzec ten zakłada zaniechanie rozbudowywania oryginalnej hierarchii klas, zalecając wyodrębnienie owych implementacji do osobnego typu. Odnosząc to do naszego przykładu, powiedzielibyśmy, że najlepiej byłoby wyodrębnić osobny typ mechanizmu oceny — Marker. Nową strukturę projektu ilustruje rysunek 11.5.

Rysunek 11.5. Wyodrębnienie algorytmów do osobnego typu To kolejny znakomity przykład wdrożenia jednej z podstawowych zasad projektowych promowanych przez Bandę Czworga (i nie tylko), mówiącej o wyższości kompozycji nad dziedziczeniem. Definiując i hermetyzując algorytmy oceny, redukujemy liczbę pochodnych w hierarchii dziedziczenia i zwiększamy równocześnie elastyczność systemu. Możemy go bowiem w dogodnych momentach uzupełniać o następne strategie oceny bez konieczności wprowadzania jakichkolwiek zmian w klasach hierarchii Question. Wszystkie klasy tej hierarchii mają do swojej dyspozycji egzemplarz klasy Marker, a interfejs klas udostępnia metodę oceny mark(). Szczegóły implementacji są dla wywołującego tę metodę zupełnie nieistotne. Oto hierarchia Question wyrażona kodem źródłowym: abstract class Question { protected $prompt; protected $marker; function __construct($prompt, Marker $marker) { $this->marker = $marker; $this->prompt = $prompt; } function mark($response) { return this->marker->mark($response); } } class TextQuestion extends Question { // operacje charakterystyczne dla prezentacji pytań w formie tekstowej… } class AVQuestion extends Question { // operacje charakterystyczne dla prezentacji pytania z materiałem audiowizualnym… }

211

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Szczegóły implementacyjne odróżniające klasy TextQuestion i AVQuestion pozostawiłem wyobraźni Czytelnika. Najważniejsze z naszego65 punktu widzenia funkcje tych klas zdefiniowane zostały bowiem w klasie bazowej Question, która ponadto przechowuje w składowej $marker obiekt oceny (obiekt klasy Marker). Kiedy następuje wywołanie metody Question::mark() z argumentem reprezentującym odpowiedź uczestnika quizu, realizacja wywołania w klasie Question polega na oddelegowaniu wywołania do odpowiedniej metody obiektu Marker. Zdefiniujmy klasę obiektów Marker: abstract class Marker { protected $test; function __construct($test) { $this->test = $test; } abstract function mark($response); } class MarkLogicMarker extends Marker { private $engine; function __construct($test); parent::construct($test); // $this->engine = new MarkParse($test); } function mark($response) { // return $this->engine->evaluate($response); // na razie działa "na niby": return true; } } class MatchMarker extends Marker { function mark($response) { return ($this->test == $response); } } class RegexpMarker extends Marker { function mark($response) { return (preg_match($this->test, $response)); } }

W implementacji klas hierarchii Marker niewiele jest elementów zaskakujących — niewiele z nich wymaga zatem w ogóle jakiegokolwiek komentarza. Zauważmy jedynie, że obiekty klasy MarkLogicMarker są przystosowane do korzystania z analizatora leksykalnego, którego kod jest prezentowany w dodatku B. Jednak na potrzeby tego przykładu możemy ten aspekt klasy pominąć, więc metoda MarkLogicMarker::mark() realizuje na razie ocenę „na pół gwizdka”, zwracając za każdym razem true. Najważniejsza w tej hierarchii jest definiowana nią struktura, nie zaś szczegóły implementacji poszczególnych strategii ocen. Struktura ta ma zaś umożliwiać przełączanie mechanizmu oceny pomiędzy obiektami hierarchii Marker bez uszczerbku dla klasy Question, która się do tego mechanizmu odwołuje. Wciąż pozostaje oczywiście kwestia podjęcia decyzji co do zastosowania jednego z konkretnych obiektów hierarchii Marker. Problem ten rozwiązuje się w praktyce na dwa sposoby. Pierwszy polega na wyborze strategii oceny na etapie układania quizu przez jego autora, a wybór sprowadza się do zaznaczenia odpowiedniego pola w formularzu. Drugi sposób to rozróżnianie mechanizmu oceny na podstawie struktury ciągu określającego bazę oceny. Jeśli baza wyrażona jest prostą odpowiedzią, wybierany jest mechanizm prostego porównania-dopasowania (MatchMarker): pięć

212

ROZDZIAŁ 11.  REPREZENTACJA I REALIZACJA ZADAŃ

Wybór mechanizmu MarkLogic sygnalizowany jest znakiem dwukropka poprzedzającego wyrażenie oceny: :$input equals 'pięć'

Z kolei ocena na podstawie dopasowania wyrażenia regularnego wybierana jest w przypadku rozpoznania w ciągu wzorca odpowiedzi znaków ukośników ograniczających wyrażenie: /pi../

Oto kod ilustrujący stosowanie poszczególnych klas: $markers = array(new RegexpMarker("/pi../")), new MatchMarker("pięć"), new MarkLogicMarker('$input equals "pięć"')); foreach ($markers as $marker) { print get_class($marker)."\n"; $question = new TextQuestion("Ile boków ma pięciobok?", $marker); foreach(array("pięć", "cztery")) as $response) { print "\todpowiedź: $response"; if ($question->mark($response)) { print "wyśmienita odpowiedź\n"; } else { print "pomyłka\n"; } } }

Konstruujemy powyżej trzy obiekty strategii oceny, z których każdy jest następnie wykorzystywany do konstrukcji obiektu pytania TextQuestion. Następnie każdy z takich obiektów jest konfrontowany z dwoma przykładowymi odpowiedziami. Klasa MarkLogicMarker jest w swej obecnej postaci jedynie makietą, a jej metoda mark() każdorazowo zwraca wartość true. Oznaczony komentarzem kod da się jednak uruchomić w połączeniu z przykładową implementacją analizatora leksykalnego prezentowaną w dodatku B; można też ją przystosować do współpracy z analizatorami autorstwa osób trzecich. Oto wynik wykonania powyższego kodu: RegexpMarker: odpowiedź: pięć odpowiedź: cztery MatchMarker: odpowiedź: pięć odpowiedź: cztery MarkLogicMarker: odpowiedź: pięć odpowiedź: cztery

wyśmienita odpowiedź - pomyłka wyśmienita odpowiedź - pomyłka wyśmienita odpowiedź - wyśmienita odpowiedź

Pamiętamy, że MarkLogicMarker z tego przykładu to atrapa, która zawsze zwraca true, więc obie odpowiedzi zostały oznaczone jako poprawne. W tym przykładzie obserwowaliśmy przekazywanie konkretnych danych od użytkownika (podającego na wejście wartość zmiennej $response) do obiektu strategii oceny; przekazanie odbywało się za pośrednictwem metody Question::mark(). W pewnych sytuacjach ilość informacji wymaganych przez obiekt, na rzecz którego wywoływana jest operacja, nie jest znana z góry. Decyzję co do ilości i rodzaju pozyskiwanych danych można więc oddelegować, przekazując do obiektu strategii egzemplarz obiektu reprezentującego użytkownika. Wtedy obiekt strategii może wywoływać na rzecz obiektu użytkownika metody zwracające wymagane dane.

213

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Wzorzec Observer Znamy już pojęcie ortogonalności jako jednej z cnót projektu. Jednym z naszych (programistów) celów powinno być konstruowanie komponentów, które można swobodnie zmieniać albo przenosić, a których modyfikacje nie przenoszą się na pozostałe komponenty. Jeśli każda zmiana jednego z komponentów systemu prowokuje szereg zmian w innej części systemu, programowanie zmienia się w wyszukiwanie i poprawianie błędów wprowadzanych w coraz większej liczbie. Rzecz jasna nie zawsze da się osiągnąć pożądaną ortogonalność projektu. Elementy systemu muszą przecież dysponować referencjami do pozostałych części systemu. Można jednak minimalizować zawiązywane w ten sposób zależności. Obserwowaliśmy już choćby różne przykłady zastosowań polimorfizmu, dzięki któremu użytkownik jedynie korzysta z interfejsu komponentu, zaś jego właściwa implementacja pozostaje poza zakresem jego zainteresowań. W pewnych okolicznościach komponenty można oddalić od siebie jeszcze bardziej. Weźmy jako przykład klasę odpowiedzialną za pośredniczenie w dostępie użytkownika do systemu: class Login { const LOGIN_USER_UNKNOWN = 1; const LOGIN_WRONG_PASS = 2; const LOGIN_ACCESS = 1; private $status = array(); function handleLogin($user, $pass, $ip) { $isvalid=false; switch(rand(1, 3)) { case 1: $this->setStatus(self::LOGIN_ACCESS, $user, $ip); $isvalid = true; break; case 2: $this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip); $isvalid = false; break; case 3: $this->setStatus(self::LOGIN_USER_UNKNOWN, $user, $ip); $isvalid = false; break; } return $isvalid; } private function setStatus($status, $user, $ip) { $this->status = array($status, $user, $ip); } function getStatus() { return $this->status; } }

W przykładzie bliższym rzeczywistości metoda handleLogin() przeprowadziłaby oczywiście weryfikację danych użytkownika. W powyższej postaci klasa ta imituje proces logowania się użytkownika w systemie — wynik logowania określany jest losowo, na podstawie wartości wywołania funkcji rand(). Znacznik statusu użytkownika może w wyniku „logowania” przyjąć wartość LOGIN_ACCESS (przyznany dostęp do systemu), LOGIN_WRONG_PASS (niepoprawne hasło) bądź LOGIN_USER_UNKNOWN (niepoprawne konto). Ponieważ klasa Login to strażnik systemowych skarbów, będzie cieszyć się w czasie implementacji projektu (i zapewne również później) szczególną uwagą. Może się okazać, że w przyszłości kierownictwo działu marketingu zażąda utrzymywania w rejestrze logowania adresów IP użytkowników. Łatwo będzie wprowadzić żądane uzupełnienie:

214

ROZDZIAŁ 11.  REPREZENTACJA I REALIZACJA ZADAŃ

function handleLogin($user, $pass, $ip) { $isvalid=false; switch(rand(1, 3)) { case 1: $this->setStatus(self::LOGIN_ACCESS, $user, $ip); $isvalid = true; break; case 2: $this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip); $isvalid = false; break; case 3: $this->setStatus(self::LOGIN_USER_UNKNOWN, $user, $ip); $isvalid = false; break; } Logger::logIP($user, $ip, $this->getStatus()); return $isvalid; }

Nieustający w trosce o bezpieczeństwo administratorzy mogą z kolei zażądać powiadamiania o nieudanych próbach logowania. Trzeba będzie ponownie wrócić do implementacji metody handleLogin() i umieścić w jej ciele dodatkowe wywołanie: if (!$isvalid) { Notifier::mailWarning($user, $ip, $this->getStatus()); }

Nie można wykluczyć, że w nieokreślonej przyszłości sekcja rozwoju ogłosi strategiczne połączenie działalności z pewnym dostawcą usług internetowych i zażąda ustawiania dla pewnej grupy użytkowników wyróżniających ich ciasteczek. I tak dalej, i tak dalej. Wszystkie te żądania z osobna są proste do spełnienia, ale zawsze ich realizacja odbywa się kosztem projektu. Klasa Login niechybnie stanie się w ich wyniku klasą głęboko osadzoną w konkretnym systemie. Nie da się jej potem łatwo wyciągnąć z projektu i zastosować w kolejnym — trzeba będzie „obrać” jej kod z wszystkich naleciałości charakterystycznych dla systemu, w którym była osadzona. Jeśli nawet okaże się to nieskomplikowane, powrócimy do programowania opartego nie na projekcie, a na umiejętnym wycinaniu i wklejaniu kodu. W efekcie otrzymamy zaś w dwóch różnych systemach dwie niepodobne już do siebie klasy Login, a ulepszenia jednej z nich będziemy próbować niezależnie wprowadzić w drugiej, aż synchronizacja taka stanie się niemożliwa z powodu zbyt wielkiej ich odmienności. Cóż możemy zrobić, aby zachować klasę Login? Możemy wdrożyć wzorzec Observer.

Implementacja Sedno wzorca Observer (obserwator) polega na rozdzieleniu elementów użytkujących (obserwatorów) od klasy centralnej (podmiotu obserwacji). Obserwatory muszą być informowane o zdarzeniach zachodzących w podmiocie obserwacji. Równocześnie nie chcemy wprowadzać trwałych i sztywnych zależności pomiędzy podmiotem obserwacji a klasami obserwatorów. Możemy więc umożliwić obserwatorom rejestrowanie się w klasie podmiotu. W tym celu powinniśmy uzupełnić klasę Login o trzy nowe metody: rejestracji (attach()), rezygnacji (detach()) i powiadomienia (notify()), przystosowując klasę do wymogów wyróżniających podmioty obserwacji interfejsu (tutaj ma on nazwę Observable): interface Observable { function attach(Observer $observer); function detach(Observer $observer); function notify();

215

PHP. OBIEKTY, WZORCE, NARZĘDZIA

} // Klasa Login… class Login implements Observable { private $observers = array(); private $storage; const LOGIN_USER_UNKNOWN = 1; const LOGIN_WRONG_PASS = 2; const LOGIN_ACCESS = 3; function attach(Observer $observer) { $this->observers[] = $observer; } function detach(Observer $observer) { $this->observers = array_filter($this->observers, function($a) use ($observer) { return (!($a === $observer)); }); } function notify() { foreach($this->observers as $obs) { $obs->update($this); } } // …

Mamy więc klasę podmiotu utrzymującą listę obiektów obserwatorów. Obiekty te są dodawane do listy z zewnątrz poprzez wywołanie metody attach(). Rezygnacja z obserwacji i usunięcie z listy następuje w wyniku wywołania metody detach(). Z kolei wywołanie metody notify() służy jako powiadomienie obiektów obserwatorów o potencjalnie interesujących ich zdarzeniach. Implementacja tej metody sprowadza się do przejrzenia tablicy obiektów obserwatorów i wywołania na rzecz każdego z nich metody update(). Wywołanie metody rozsyłającej powiadomienia następuje we wnętrzu klasy Login, w ciele metody handleLogin(): function handleLogin($user, $pass, $ip) { switch(rand(1, 3)) { case 1: $this->setStatus(self::LOGIN_ACCESS, $user, $ip); $isvalid = true; break; case 2: $this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip); $isvalid = false; break; case 3: $this->setStatus(self::LOGIN_USER_UNKNOWN, $user, $ip); $isvalid = false; break; } $this->notify(); return $isvalid; }

Zdefiniujmy interfejs klas obserwatorów: interface Observer { function update(Observable $observable); }

Do listy obserwatorów można dodawać (za pośrednictwem metody attach() klasy podmiotu obserwacji) dowolne obiekty, które implementują interfejs Observable. Tak wygląda tworzenie konkretnego egzemplarza: class SecurityMonitor implements Observer { function update(Observable $observable) { $status = $observable->getStatus();

216

ROZDZIAŁ 11.  REPREZENTACJA I REALIZACJA ZADAŃ

if ($status[0] == Login::LOGIN_WRONG_PASS) { // wyślij wiadomość do administratora… print __CLASS__."\twysyłam wiadomość do administratora\n"; } } } $login = new Login(); $login->attach( new SecurityMonitor() );

Zwróćmy uwagę, jak obiekt obserwatora odwołuje się do egzemplarza klasy Observable (podmiotu obserwacji) w celu pozyskania dodatkowych informacji o zdarzeniu. Metody, za pośrednictwem których obiekty obserwatorów mogłyby dowiadywać się o stanie, powinny zostać udostępnione właśnie w klasie podmiotu obserwacji. W tym przypadku klasa podmiotu ma zdefiniowaną metodę getStatus(), dzięki której obiekty obserwatorów mogą dowiadywać się o bieżącym stanie obiektu obserwowanego. Pojawia się tutaj pewien problem. Otóż w wywołaniu metody Login::getStatus() klasa SecurityMonitor bazuje na wiedzy o klasie Login, na której nie powinna polegać. Przecież w wywołaniu otrzymuje obiekt Observable, ale nie ma żadnej gwarancji, że będzie to właśnie obiekt Login. Mamy tu kilka możliwości: możemy rozszerzyć interfejs Observable tak, aby zawierał w sobie deklarację metody getStatus(), i możemy od razu przemianować interfejs na ObservableLogin, sygnalizując, że ma związek z klasami Login. Możemy też utrzymać ogólny interfejs Observable i obarczyć klasy Observable odpowiedzialnością za to, aby podmioty obserwacji były odpowiedniego typu. Możemy wtedy złożyć na nie również zadanie kojarzenia się z podmiotami obserwacji. Ponieważ będziemy mieć więcej niż jeden typ Observer, a zamierzamy zaimplementować przy okazji czynności porządkowe wspólne dla wszystkich podtypów, możemy od razu udostępnić abstrakcyjną klasę bazową: abstract class LoginObserver implements Observer { private $login; function __construct(Login $login) { $this->login = $login; $login->attach($this); } function update(Observable $observable) { if ($observable === $this->login) { $this->doUpdate($observable); } } abstract function doUpdate( Login $login ); }

Klasa LoginObserver wymaga do konstrukcji obiektu typu Login. W konstruktorze zachowuje sobie referencję obiektu i wywołuje własną metodę Login::attach(). W wywołaniu update() następuje sprawdzenie, czy przekazany obiekt Observable jest w istocie referencją obserwowanego podmiotu, po czym dochodzi do wywołania metody szablonowej doUpdate(). Teraz możemy utworzyć cały zestaw obiektów LoginObserver, z których każdy będzie operował na obiekcie Login, a nie na dowolnym obiekcie implementującym nasz stary interfejs Observable: class SecurityMonitor extends LoginObserver { function doUpdate(Login $login) { $status = $login->getStatus(); if ($status[0] == Login::LOGIN_WRONG_PASS) { // wysłanie wiadomości do administratora print __CLASS__.":\twysyłam wiadomość do administratora\n"; } } }

217

PHP. OBIEKTY, WZORCE, NARZĘDZIA

class GeneralLogger extends LoginObserver { function doUpdate(Login $login) { $status = $login->getStatus(); // dodanie danych do rejestru print __CLASS__.":\tdodaję dane logowania do rejestru\n"; } } class PartnershipTool extends LoginObserver { function doUpdate(Login $login) { $status = $login->getStatus(); // sprawdzenie adresu IP // ustawienie ciasteczka dla dopuszczonego IP print __CLASS__.":\tustawiam ciasteczko dla dopuszczonego IP\n"; } }

Tworzenie i podłączanie obserwatorów LoginObserver jest teraz wykonywane w czasie konkretyzacji obiektów: $login = new Login(); new SecurityMonitor($login); new GeneralLogger($login); new PartnershipTool($login);

Dopracowaliśmy się niniejszym elastycznego powiązania pomiędzy obserwatorami a klasami podmiotów obserwacji. Diagram klas ilustrujący to powiązanie można obejrzeć na rysunku 11.6.

Rysunek 11.6. Klasy wzorca Observer

218

ROZDZIAŁ 11.  REPREZENTACJA I REALIZACJA ZADAŃ

PHP dysponuje wbudowanym mechanizmem obserwacji za pośrednictwem rozszerzenia SPL (Standard PHP Library). SPL to zestaw narzędzi pomocnych w implementowaniu typowych zagadnień obiektowych. Aspekt obserwacji jest tu reprezentowany trzema elementami: SplObserver, SplSubject oraz SplObjectStorage. SplObserver i SplSubject to interfejsy dokładnie odpowiadające interfejsom Observer i Observable omawianym w tym podrozdziale. Z kolei SplObjectStorage to klasa narzędziowa przewidziana do zarządzania tworzeniem i usuwaniem obiektów. Nasza implementacja wzorca Observer w SPL wyglądałaby tak: class Login implements SplSubject { private $storage; //... function __construct() { $this->storage = new SplObjectStorage(); } function attach(SplObserver $observer) { $this->storage->attach($observer); } function detach(SplObserver $observer) { $this->storage->detach($observer); } function notify() { foreach ($this->storage as $obs) { $obs->update($this); } } //... } abstract class LoginObserver implements SplObserver { private $login; function __construct(Login $login) { $this->login = $login; $login->attach($this); } function update(SplSubject $subject) { if ($subject === $this->login) { $this->doUpdate($subject); } } abstract function doUpdate(Login $login); }

Nie ma istotnych różnic, jeśli chodzi o interfejsy SplObserver (u nas Observer) i SplSubject (u nas Observable), z tym że nie trzeba oczywiście ich deklarować — no i musimy zmienić deklaracje typów argumentów odpowiednio do nazw nowych interfejsów. Za to klasa SplObjectStorage realizuje naprawdę użyteczną usługę: w naszej pierwotnej implementacji Login::detach() przy usuwaniu obiektu stosowaliśmy funkcję array_filter (wraz z funkcją anonimową) do tablicy $observable. Klasa SplObjectStorage wykonuje tę czarną robotę za nas: implementuje wywołania attach() i detach(), a do tego można jej użyć w foreach i iterować po elementach.  Uwaga O SPL można poczytać więcej w dokumentacji PHP pod adresem http://www.php.net/spl. W szczególności można tam znaleźć omówienia wielu iteracji. Wbudowany w PHP interfejs Iterator będziemy omawiać w rozdziale 13. („Wzorce bazodanowe”).

219

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Problem komunikacji pomiędzy klasą obserwatora a podmiotem obserwowanym można by rozwiązać poprzez przekazywanie w wywołaniu metody update() obiektów obserwatorów nie egzemplarza podmiotu obserwacji, ale kompletu informacji o jego stanie. Osobiście często stosuję tę metodę, jeśli mam na szybko skonstruować działające rozwiązanie. W naszym przykładzie metoda update() powinna więc oczekiwać przekazania nie egzemplarza klasy Login, ale znacznika statusu logowania, identyfikatora użytkownika i adresu IP maszyny, z której zainicjowano próbę logowania, najlepiej w postaci tablicy. Pozwala to na wyeliminowanie jednej metody z klasy Login. Z drugiej strony, jeśli stan podmiotu obserwacji miałby być opisywany zbyt wielką liczbą danych, znacznie bardziej elastycznym rozwiązaniem byłoby jednak przekazywanie w wywołaniu update() egzemplarza Login. Można też zablokować typ w ogóle, odmawiając w klasie Login współpracy z obiektami klas innych niż wyróżniona (np. LoginObserver). W takim układzie należałoby jeszcze pomyśleć o jakichś realizowanych w czasie wykonania testach obiektów przekazywanych w wywołaniu metody attach(); alternatywą byłaby zmiana (uszczegółowienie) interfejsu Observable. Mamy tu ponowne zastosowanie kompozycji w czasie wykonania celem skonstruowania elastycznego i rozszerzalnego modelu. Klasa Login może zostać teraz łatwo wyodrębniona z kontekstu i przerzucona do zupełnie innego projektu, gdzie może współpracować z zupełnie odmiennym zestawem obserwatorów.

Wzorzec Visitor Jak widzieliśmy, wiele wzorców, podążając za zasadą wyższości kompozycji nad dziedziczeniem, zakłada konstruowanie struktur w czasie wykonania programu. Znakomitym przykładem takiego wzorca jest powszechnie stosowany wzorzec kompozycji — Composite. Tam, gdzie trzeba operować na zbiorach obiektów, niektóre z operacji mogą odwoływać się do zbiorów jako takich, ale inne mogą wymagać operowania na poszczególnych komponentach zbioru. Takie operacje można wbudować w same komponenty — w końcu to one znajdują się na najlepszej możliwej pozycji do ich realizacji. Podejście to nie jest jednak pozbawione wad. Nie zawsze na przykład mamy dostateczną ilość informacji o operacjach, które będą wykonywane na strukturze. Jeśli klasy są uzupełniane operacjami od przypadku do przypadku, ich interfejsy mogą się nadmiernie rozrosnąć. Wtedy można uciec się do wzorca Visitor (wizytator).

Problem Wróćmy do prezentowanego w poprzednim rozdziale przykładu zastosowania wzorca kompozycji. Na potrzeby pewnej gry stworzyliśmy tam armię komponentów o ciekawej cesze zastępowalności komponentu zbiorem komponentów. Operacje były tam wbudowywane w same komponenty — właściwe operacje realizowane były przez obiekty składowe, do których odwoływał się obiekt kompozyt. class Army extends CompositeUnit { function bombardStrength() { $strength = 0; foreach($this->units() as $unit) { $strength += $unit->bombardStrength(); } return $strength; } } class LaserCannonUnit extends Unit { function bombardStrength() { return 44; } }

Nie są tu problemem te operacje, które stanowią podstawowe zadania klasy kompozytu. Gorzej z operacjami pobocznymi. 220

ROZDZIAŁ 11.  REPREZENTACJA I REALIZACJA ZADAŃ

Weźmy choćby operację, w ramach której trzeba wykonać zrzut (w postaci tekstowej) informacji o obiektach składowych kompozytu. Operację taką można by włączyć do klasy Unit: // klasa Unit function textDump($num = 0) { $txtout = ""; $pad = 4 * $num; $txtout.= sprintf("%{$pad}s", ""); $txtout .= get_class($this).": "; $txtout .= "siła rażenia: ".$this->bombardStrength()."\n"; return $txtout; }

Metodę tę można następnie przesłonić w klasie CompositeUnit: // klasa CompositeUnit function textDump($num = 0) { $txtout = parent::textDump($num); foreach($this->units as $unit) { $txtout .= $unit->textDump($num + 1); } return $txtout; }

Moglibyśmy, idąc tym tropem, utworzyć metody zliczające liczbę jednostek w kompozycie, metody zapisywania danych o składowych kompozytu w bazie danych czy też obliczania liczby jednostek aprowizacji konsumowanych codziennie przez armię. Ale czy koniecznie powinniśmy włączać tego rodzaju operacje do interfejsu kompozytu? Po co rozszerzać interfejs o funkcje niekoniecznie związane z podstawowym zadaniem klasy? Odpowiedź jest prosta: postanowiliśmy zdefiniować te funkcje tu, bo z tego miejsca łatwo o dostęp do składowych kompozytu. Choć co prawda łatwość przeglądania zbioru jest jedną z podstawowych cech kompozytu, nie oznacza to, że dosłownie każda operacja wymagająca przejrzenia składowych kompozytu powinna być implementowana w jego klasie i zajmować miejsce w jego interfejsie. Mamy więc kolejny cel: wykorzystać w dowolnych operacjach łatwość odwołań do komponentów kompozytu bez niepotrzebnego rozdymania jego interfejsu.

Implementacja Zacznijmy od zdefiniowania w abstrakcyjnej klasie Unit metody accept(). abstract class Unit { protected $depth = 0; // ... function accept(ArmyVisitor $visitor) { $method = "visit".getClass($this); $visitor->$method($this); } protected function setDepth($depth) { $this->depth=$depth; } function getDepth() { return $this->depth; } }

221

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Jak widać, metoda accept() oczekuje przekazania w wywołaniu obiektu klasy ArmyVisitor. W języku PHP mamy możliwość dynamicznego konstruowania nazw metod, wykorzystywaną tu do realizacji dynamicznego wyboru metody do wywołania na rzecz przekazanego obiektu; nazwę metody ustalamy na podstawie nazwy bieżącej klasy i wywołujemy ją na rzecz dostarczonego obiektu ArmyVisitor. Jeśli więc bieżąca klasa to Army, wywołujemy metodę ArmyVisitor::visitArmy(), a jeśli bieżąca klasa to TroopCarrier, wywołujemy metodę ArmyVisitor::visitTroopCarrier() (i tak dalej). Dzięki temu nie musimy implementować metody accept() osobno dla każdego węzła końcowego naszej hierarchii klas. Przy okazji dodałem dwie pożyteczne metody getDepth() i setDepth(). Można je stosować do zapisywania i pozyskiwania głębokości jednostki w drzewie. Metoda setDepth() jest wywoływana przez klasę nadrzędną jednostki przy dodawaniu jej do drzewa z poziomu metody CompositeUnit::addUnit(). function addUnit(Unit $unit) { foreach ($this->units as $thisunit) { if ($unit === $thisunit) { return; } } $unit->setDepth($this->depth + 1); $this->units[] = $unit; }

Trzeba jedynie zdefiniować tę samą metodę w abstrakcyjnej klasie kompozytu. function accept(ArmyVisitor $visitor) { $method = "visit".getClass($this); $visitor->$method($this); foreach($this->units as $thisunit) { $thisunit->accept($visitor); } }

Metoda ta realizuje to samo zadanie, co Unit::accept(), z jednym tylko dodatkiem. Po wywołaniu odpowiedniej metody wizytującej na rzecz dostarczonego obiektu ArmyVisitor metoda przegląda wszystkie obiekty potomne i wywołuje na nich metodę accept(). A ponieważ accept() powtarza tu operacje definiowane w klasach nadrzędnych, możemy w prosty sposób wyeliminować duplikację kodu: function accept(ArmyVisitor $visitor) { parent::accept($visitor); foreach($this->units as $thisunit) { $thisunit->accept($visitor); } }

Takie ulepszenie jest bardzo eleganckie, ale choć tutaj dało oszczędność jednego zaledwie wiersza, odbyło się z pewną szkodą dla czytelności i przejrzystości kodu. Tak czy inaczej metoda accept() pozwala nam na dwie rzeczy:  wywoływanie metody wizytacji właściwej dla bieżącego komponentu;  przekazywanie obiektu wizytatora do wszystkich komponentów bieżącego kompozytu przez wywołanie ich metod accept(). Trzeba nam jeszcze zdefiniować interfejs klasy ArmyVisitor. Pewne pojęcie o jego składnikach daje już metoda accept(). Otóż klasa wizytatora powinna definiować wersje metody accept() dla wszystkich konkretnych klas w hierarchii. Dzięki temu na kompozytach różnych obiektów będzie można wykonywać różne operacje. W mojej wersji tej klasy zdefiniowałem domyślne wersje metody visit() wywoływane automatycznie, jeśli klasa implementująca nie określi własnej wersji tej operacji dla danej klasy hierarchii Unit.

222

ROZDZIAŁ 11.  REPREZENTACJA I REALIZACJA ZADAŃ

abstract class ArmyVisitor { abstract function visit(Unit $node); function visitArcher(Archer $node) { $this->visit($node); } function visitCavalry(Cavalry $node) { $this->visit($node); } function visitLaserCannonUnit(LaserCannonUnit $node) { $this->visit($node); } function visitTroopCarrierUnit(TroopCarrierUnit $node) { $this->visit($node); } function visitArmy(Army $node) { $this->visit($node); } }

Teraz więc problem sprowadza się do implementacji klas pochodnych ArmyVisitor. Oto przykład w postaci kodu generującego zestawienie informacji o obiektach kompozytach przeniesiony już do klasy ArmyVisitor: class TextDumpArmyVisitor extends ArmyVisitor { private $text = ""; function visit(Unit $node) { $txt = ""; $pad = 4 * $node->getDepth(); $txt .= sprintf("%{$pad}s", ""); $txt .= get_class($node); $txt .= "siła rażenia: ".$node->bombardStrength()."\n"; $this->text .= txt; } function getText() { return $this->text; } }

Spójrzmy, jak stosować taki kod: $main_army = new Army(); $main_army->addUnit(new Archer()); $main_army->addUnit(new LaserCannonUnit()); $main_army->addUnit(new Cavalry()); $texdump = new TextDumpArmyVisitor(); $main_army->accept($textdump); print $textdump->getText();

Powyższy kod powinien dać następujący rezultat: Army: siła rażenia: 50 Archer: siła rażenia: 4 LaserCannonUnit: siła rażenia: 44 Cavalry: siła rażenia: 2

223

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Utworzyliśmy obiekt klasy Army. Ponieważ jest to kompozyt, włączyliśmy do niego (za pomocą metody addUnit()) pewną liczbę utworzonych specjalnie w tym celu obiektów klasy Unit. Następnie utworzyliśmy obiekt klasy TextDumpArmyVisitor i przekazaliśmy go w wywołaniu metody Army::accept(). Metoda ta skonstruowała na podstawie nazwy klasy przekazanego obiektu nazwę metody do wywołania i wywołała metodę TextDumpArmyVisitor::visitArmy(). Ponieważ nie przewidzieliśmy specjalnej obsługi wywołania visit() dla obiektów klasy Army, wywołanie to zostanie zrealizowane przez domyślną wersję metody visit() dla obiektów tego typu. W wywołaniu visit() przekazywana jest referencja obiektu klasy Army, a w samej metodzie następują wywołania metod tegoż obiektu (w tym nowo dodanej metody getDepth(), informującej o bieżącym zagłębieniu w drzewie kompozytu), a za ich pośrednictwem generowane jest zestawienie opisujące kompozyt. Aby zestawienie było kompletne, metoda Army::accept() wywołuje następnie metody accept() komponentów, przekazując w wywołaniu ten sam obiekt wizytatora, który sama otrzymała. W ten sposób klasa ArmyVisitor „odwiedza” wszystkie obiekty wchodzące w skład drzewa kompozytu. Uzupełniając istniejący szkielet klas o kilka zaledwie metod, utworzyliśmy mechanizm, za pośrednictwem którego można dołączać do klasy kompozytu nowe funkcje, nie ingerując równocześnie w interfejs kompozytu i unikając powielania kodu realizującego przeglądanie komponentów. Załóżmy teraz, że na niektórych polach planszy jednostki muszą uiszczać myto. Poborca odwiedza wtedy poszczególne jednostki armii, przy czym różne jednostki są różnie opodatkowane. Na tym przykładzie będzie dobrze widać zalety specjalizowania metod klasy wizytatora: class TaxCollectionVisitor extends ArmyVisitor { private $due = 0; private $report = ""; function visit(Unit $node) { $this->levy($node, 1); } function visitArcher(Archer $node) { $this->levy($node, 2); } function visitCavalry(Cavalry $node) { $this->levy($node, 3); } function visitTroopCarrierUnit(TroopCarrierUnit $node) { $this->levy($node, 5); } private function levy(Unit $unit, $amount) { $this->report .= "Myto należne za ".getClass($unit); $this->report .= ": $amount\n"; $this->due += $amount; } function getReport() { return $this->report; } function getTax() { return $this->due; } }

W tym prostym przykładzie nie skorzystamy wprost z obiektu klasy Unit przekazywanego do różnych metod wizytacji. Korzystamy jedynie ze specjalizacji (podziału na klasy obiektów odwiedzanych), ustalając dla różnych klas jednostek różne stawki myta.

224

ROZDZIAŁ 11.  REPREZENTACJA I REALIZACJA ZADAŃ

Oto jak wygląda pobieranie myta z punktu widzenia użytkownika hierarchii: $main_army = new Army(); $main_army->addUnit(new Archer()); $main_army->addUnit(new LaserCannonUnit()); $main_army->addUnit(new Cavalry()); $taxcollector = new TaxCollectorVisitor(); $main_army->accept($taxcollector); print $taxcollector->getReport(); print "ŁĄCZNIE: "; print $taxcollector->getTax()."\n";

Tak jak poprzednio, do metody accept() wywołanej na rzecz obiektu klasy Army przekazany został obiekt wizytatora — tutaj TaxCollectorVisitor. Ponownie też obiekt klasy Army przekazuje referencję do samego siebie do metody visitArmy(), tuż przed oddelegowaniem wywołania do metod accept() swoich komponentów. Komponenty są nieświadome operacji przeprowadzonych w ramach wizytacji. Ograniczają się do współpracy z publicznym interfejsem wizytatora, przekazując się po kolei do metody wizytacji właściwej dla swojego typu, w ramach której następuje obliczenie należnego myta. Poza metodami zdefiniowanymi w klasie bazowej hierarchii wizytatorów klasa TaxCollectorVisitator definiuje dwie metody dodatkowe: getReport() i getTax(). Ich wywołania zwracają dane zebrane podczas wizytacji: Myto należne Myto należne Myto należne Myto należne ŁĄCZNIE: 7

za za za za

Army: 1 Archer: 2 LaserCannonUnit: 1 Cavalry: 3

Uczestników operacji prezentowanych w przykładzie ilustruje diagram z rysunku 11.7.

Rysunek 11.7. Wzorzec Visitor

Wady wzorca Visitor Wzorzec Visitor to kolejny wzorzec łączący prostotę z efektywnością. Zalety nie mogą jednak przesłonić całkowicie wad wzorca, gdyż i te istnieją. Po pierwsze, wizytator może być stosowany do dowolnych kolekcji obiektów (choć najlepiej pasuje do wzorca kompozycji). Da się więc na przykład zaimplementować wizytację list obiektów, w której każdy z obiektów przechowuje referencję swoich sąsiadów. Wyodrębniając operacje na kolekcji poza tę kolekcję, sprzeciwiamy się jednak hermetyzacji. Otóż może się okazać, że aby wizytator mógł w jakikolwiek użyteczny sposób przetworzyć obiekty kolekcji, będą one musiały udostępniać na zewnątrz swoje aspekty wewnętrzne (prywatne). Widać to było już choćby w pierwszym przykładzie

225

PHP. OBIEKTY, WZORCE, NARZĘDZIA

z tego podrozdziału, kiedy to na potrzeby klasy wizytatora TextDumpArmyVisitor trzeba było uzupełnić interfejs Unit o dodatkową metodę. Podobny dylemat mieliśmy przy wzorcu Observer. Ponieważ wzorzec ten zakłada również oddzielenie iteracji od operacji wykonywanych na komponentach kolekcji, trzeba zrzec się pewnej części kontroli — nie da się na przykład łatwo utworzyć metody wizytacji visit(), która wykonuje pewne operacje tak przed, jak i po odwiedzeniu komponentów zagnieżdżonych w kolekcji. Można by tę niedogodność wyeliminować, przenosząc odpowiedzialność za iterację do samych obiektów wizytatorów. Tyle że wtedy w obiektach tych dojdzie niechybnie do powielenia kodu iteracji. Osobiście preferuję więc obsługę iteracji w ramach klas wizytowanych, choć nie przeczę, że jej wysunięcie poza te klasy dałoby pewną zasadniczą zaletę: można wtedy zmieniać w poszczególnych wizytatorach sposób wizytacji.

Wzorzec Command Ostatnimi laty rzadko kiedy udawało mi się zakończyć projekt aplikacji WWW bez wdrażania w nim wzorca Command, czyli wzorca polecenia. Obiekty poleceń, choć pierwotnie stosowane w kontekście projektu graficznego interfejsu użytkownika, sprawdzają się również w projektach aplikacji korporacyjnych, wymuszając separację pomiędzy warstwą kontroli żądań (kodem obsługi i rozprowadzania żądań) a warstwą logiczną aplikacji.

Problem Wszystkie systemy muszą w odpowiedni sposób reagować na żądania użytkowników. W PHP proces podejmowania decyzji jest często rozproszony pomiędzy wieloma formularzami-stronami tworzącymi interfejs aplikacji. Wybór funkcji i interfejsu odbywa się tutaj przez wybór jednej ze stron witryny WWW, np. feedback.php. Ostatnio programiści PHP optują jednak coraz silniej za podejściem, w którym wyróżnione jest tylko jedno miejsce styku (patrz też następny rozdział). Tak czy inaczej odbiorca żądania musi je oddelegować do warstwy bliższej samej logice aplikacji. Owa delegacja jest szczególnie istotna, kiedy użytkownik może inicjować żądania za pośrednictwem różnych stron WWW. Bez delegacji projekt zostałby w nieunikniony sposób obciążony powieleniem kodu obsługi żądania. Wyobraźmy sobie więc projekt, w ramach którego powinniśmy realizować pewną liczbę zadań. W szczególności system nasz powinien pozwalać wybranym użytkownikom na zalogowanie się, a innym na przesłanie formularza zwrotnego. Do obsługi tych zadań moglibyśmy wyznaczyć strony login.php i feedback.php, konkretyzując w nich specjalizowane klasy realizujące żądania. Niestety, interfejsy systemu dla różnych użytkowników rzadko pokrywają się dokładnie z zadaniami, które system ma realizować. Może się więc okazać, że na każdej stronie potrzebujemy zarówno możliwości logowania, jak i przesłania informacji zwrotnej. Jeśli zaś strony mają obsługiwać różne zadania, to może powinniśmy oprzeć hermetyzację właśnie na zadaniach. W ten sposób ułatwimy sobie uzupełnianie funkcjonalności systemu o nowe zadania i stworzymy wyraźną granicę pomiędzy warstwami systemu. W ten sposób dojdziemy do wdrożenia wzorca Command.

Implementacja Interfejs obiektu polecenia jest tak prosty jak to możliwe — składa się w najprostszym wydaniu z jednej tylko metody — execute(). Na rysunku 11.8 Command jest klasą abstrakcyjną. Przy tym poziomie uproszczenia mógłby zostać równie dobrze zdefiniowany jako interfejs. Osobiście skłaniam się do stosowania abstrakcji w miejsce interfejsów, dlatego że niejednokrotnie okazuje się, że w abstrakcyjnej klasie bazowej można upchnąć parę funkcji wspólnych dla wszystkich obiektów pochodnych.

Rysunek 11.8. Klasa Command

226

ROZDZIAŁ 11.  REPREZENTACJA I REALIZACJA ZADAŃ

We wzorcu Command mamy jeszcze przynajmniej trzech innych uczestników: klienta, który konkretyzuje obiekt polecenia, inicjatora (ang. invoker), który wdraża obiekt w systemie, oraz odbiorcę, do którego polecenie się odnosi. Odbiorca może zostać wskazany w poleceniu przez klienta (przy konstruowaniu obiektu polecenia) albo pozyskany z pewnego rodzaju wytwórni. Osobiście preferuję to drugie podejście, bo pozwala na ujednolicenie sposobu konkretyzacji obiektów wszystkich poleceń. Wszystkie obiekty poleceń mogą być wtedy tworzone w ten sam sposób. Oto stosowna abstrakcyjna klasa bazowa: abstract class Command { abstract function execute(CommandContext $context); } A oto konkretna klasa potomna Command: class LoginCommand extends Command { function execute(CommandContext $context) { $manager = Registry::getAccessManager(); $user = $context->get('username'); $pass = $context->get('password'); $user_obj = $manager->login($user, $pass); if (is_null($user_obj)) { $context->setError($manager->getError()); return false; } $context->addParam("user", $user_obj); return true; } }

Klasa LoginCommand jest przewidziana do współpracy z obiektem klasy AccessManager. Ten jest na razie wyimaginowaną klasą, której zadaniem jest obsługa szczegółów związanych z procesem rejestrowania użytkowników w systemie. Zauważ, że nasza metoda Command::execute() żąda przekazania w wywołaniu obiektu klasy CommandContext (w książce Core J2EE Patterns1 występuje ona jako RequestHelper). Za jego pośrednictwem obiekt polecenia może odwoływać się do danych związanych z żądaniem i za jego pośrednictwem może przekazywać odpowiedzi do warstwy prezentacji. Zastosowanie w tej roli obiektu jest o tyle wygodne, że pozwala na ujednolicenie interfejsu obiektu polecenia, który przecież w zależności od realizowanego zadania musiałby przyjmować odmienne zestawy argumentów. CommandContext jest tu zasadniczo kopertą obiektową ujmującą zmienną typu tablicy asocjacyjnej, a niekiedy uzupełnioną o parę dodatkowych funkcji. Oto prosta implementacja tej klasy: class CommandContext { private $params = array(); private $error = ""; function __construct() { $this->params = $_REQUEST; } function addParam($key, $val) { $this->params[$key] = $val; } function get($key) { if (isset($this->params[$key])) { return $this->params[$key]; } return null; 1

Wydanie polskie: J2EE. Wzorce projektowe. Wydanie 2, Helion, 2004 r. — przyp. tłum.

227

PHP. OBIEKTY, WZORCE, NARZĘDZIA

} function setError($error) { $this->error = $error; } function getError() { return $this->error; } }

Uzbrojona w obiekt CommandContext klasa LoginCommand może odwoływać się do danych żądania, mianowicie do przesłanej nawy konta i hasła. Do zwracania obiektu AccesManager, dla którego ma działać LoginCommand, używam prostej klasy wytwórczej z metodami statycznymi służącymi do generowania często używanych obiektów. Jeśli obiekt AccessManager zgłosi błąd, komunikat o błędzie zostanie przekazany do obiektu CommandContext za pośrednictwem warstwy prezentacji. Jeśli wszystko pójdzie dobrze, LoginCommand po prostu zwróci true. Zauważmy, że obiekty poleceń nie realizują złożonej logiki: weryfikują dane wejściowe, obsługują błędy i buforują dane, ewentualnie wywołują inne obiekty w celu wykonania operacji. Jeśli do obiektów Command zacznie przenikać logika wyższego poziomu aplikacji, warto pomyśleć o refaktoryzacji. Prowadzi to bowiem do powielania kodu kopiowanego pomiędzy poleceniami. Trzeba wtedy przynajmniej zastanowić się, gdzie przynależy funkcjonalność. Może ona zostać przeniesiona do obiektów biznesowych albo do warstwy fasady. W naszym przykładzie wciąż nie mamy klienta: klasy, która generowałaby obiekty poleceń, oraz inicjatora: klasy, która operuje na wygenerowanych poleceniach. Najprostszy sposób na wybranie polecenia do skonkretyzowania w projekcie WWW to wyróżnienie parametru żądania. Oto uproszczona implementacja klienta: class CommandNotFoundException extends Exception {} class CommandFactory { private static $dir = 'commands'; static function getCommand($action='Default') { if (preg_match('/\W/', $action)) { throw new Exception("niedozwolone znaki parametru"); } $class = UCFirst(strtolower($action))."Command"; $file = self::$dir.DIRECTORY_SEPARATOR."{$class}.php"; if (!file_exists($file)) { throw new CommandNotFoundException("nie można znaleźć pliku '$file'"); } require_once($file); if (!class_exists($class)) { throw new CommandNotFoundException("nie można znaleźć klasy '$class'"); } $cmd = new $class(); return $cmd; } }

Klasa CommandFactory przeszukuje katalog o nazwie commands, szukając w nim pliku konkretnej klasy. Nazwa pliku konstruowana jest na bazie wyodrębnianego z obiektu CommandContext parametru $action, który z kolei powinien zostać przekazany wraz z żądaniem. Jeśli plik klasy uda się odnaleźć, a w pliku zdefiniowana jest szukana klasa, wtedy obiekt tej klasy jest zwracany wywołującemu. Moglibyśmy ten fragment kodu uzupełnić odpowiednimi operacjami kontroli błędów, upewniając się choćby, czy znaleziona klasa należy do hierarchii Command, czy konstruktor klasy faktycznie nie wymaga przekazania żadnych argumentów — dla celów przykładu tak okrojona implementacja jest jednak zupełnie wystarczająca. Siłą tego rozwiązania jest to, że system można uzupełniać o nowe klasy poleceń w dowolnym momencie, uzupełniając po prostu katalog commands — po umieszczeniu w nim nowej klasy system od razu może obsługiwać nowe polecenie.

228

ROZDZIAŁ 11.  REPREZENTACJA I REALIZACJA ZADAŃ

Kod inicjatora jest teraz równie prosty: class Controller { private $context; function __construct() { $this->context = new CommandContext(); } function getContext() { return $this->context; } function process() { $action = $this->context->get('action'); $action = (is_null($action)) ? "default" : $action; $cmd = CommandFactory::getCommand($action); if (!$cmd->execute($this->context)) { // obsługa błędu… } else { // sukces // rozprowadzenie widoku.. } } } $controller = new Controller(); // imitacja obsługi żądania użytkownika $context = $controller->getContext(); $context->addParam('action', 'login'); $context->addParam('username', 'bob'); $context->addParam('pass', 'hop125'); $controller->process();

Przed wywołaniem Controller::process() tworzymy fikcyjne żądanie WWW, ustawiając odpowiednio parametry obiektu kontekstu konkretyzowanego w konstruktorze kontrolera. Metoda process() pozyskuje parametr „akcji” (z wartością domyślną, jeśli żaden parametr nie został ustalony). Następnie process() deleguje konkretyzację obiektu polecenia do wytwórni CommandFactory, a następnie na rzecz tak otrzymanego obiektu wywołuje metodę execute(). Zauważmy, że kontroler nie wie wiele o cechach wewnętrznych polecenia — właśnie ta niezależność od szczegółów wykonania polecenia umożliwia nam dodawanie do systemu kolejnych klas poleceń przy minimalnym wpływie na zastany szkielet aplikacji. Utwórzmy jeszcze jedną klasę hierarchii Command: class FeedbackCommand extends Command { function execute(CommandContext $context) { $msgSystem = Registry::getMessageSystem(); $email = $context->get('email'); $msg = $context->get('msg'); $topic = $context->get('topic'); $result = $msgSystem->send($email, $msg, $topic); if (!$result) { $context->setError($msgSystem->getError()); return false; } return true; } }

229

PHP. OBIEKTY, WZORCE, NARZĘDZIA

 Uwaga Do wzorca Command wrócimy jeszcze w rozdziale 12., przy okazji omawiania pełniejszej implementacji klasy wytwórni poleceń. Zaprezentowany tu szkielet wykonywania poleceń jest jedynie uproszczoną wersją innego wzorca, z którym się niebawem zetkniemy — wzorca Front Controller.

Jeśli prezentowana wyżej klasa będzie definiowana w pliku FeedbackCommand.php, a plik umieszczony w katalogu commands, będzie można korzystać z pośrednictwa jej obiektów w obsłudze żądania obsługi formularza zwrotnego; ewentualne zmiany w sposobie tej obsługi nie będą wymagać żadnych czynności dostosowawczych w kodzie kontrolera ani w kodzie klas wytwórni poleceń. Uczestników wzorca Command prezentuje rysunek 11.9.

Rysunek 11.9. Uczestnicy wzorca Command

Podsumowanie Niniejszym rozdziałem zakończyliśmy przegląd wzorców z katalogu Bandy Czworga. Udało się przy tym zaprojektować miniaturowy język programowania i skonstruować na bazie wzorca Interpreter mechanizm jego interpretacji. We wzorcu Strategy rozpoznaliśmy kolejny sposób korzystania z kompozycji na rzecz zwiększania elastyczności i redukowania potrzeby wyprowadzania dublujących się po części pochodnych. Wzorzec Observer rozwiązał problem powiadamiania oddzielonych i różnych od siebie komponentów o zdarzeniach zachodzących w systemie. Wróciliśmy też na chwilę do przykładu z omówienia wzorca Composite, pokazując zastosowanie wzorca Visitor do wykonywania rozmaitych operacji na składnikach obiektu kompozytu. Na koniec mogliśmy docenić ułatwienie konstruowania rozszerzalnego systemu warstwowego w postaci wzorca Command. W następnym rozdziale porzucimy już katalog Bandy Czworga, zwracając się ku wzorcom powstałym specjalnie z myślą o programowaniu aplikacji korporacyjnych.

230

ROZDZIAŁ 12 

Wzorce korporacyjne

PHP to najważniejszy i przodujący język aplikacji WWW. A dzięki wyposażeniu go w kompletne mechanizmy obiektowe pozwala na pełne wykorzystanie zalet wzorców osadzonych w kontekście języków programowania obiektowego. Na potrzeby niniejszego rozdziału opracowałem pojedynczy przykład jako ilustrację dla omawianych tu wzorców. Pamiętajmy jednak, że decyzja o zastosowaniu danego wzorca nie oznacza, że koniecznie trzeba użyć również wszystkich innych wzorców, które zostały przedstawione jako powiązane bądź uzupełniające. Nie należy też mieć wrażenia, że pokazane tu implementacje są jedynymi słusznymi środkami wdrożenia omawianych wzorców. Przykłady mają jedynie dopomóc w zrozumieniu istoty i zalet omawianych wzorców — każdy może z tego wyciągnąć do własnych projektów to, co uzna za potrzebne i pożądane. Z racji ilości materiału to jeden z najdłuższych i najtrudniejszych rozdziałów w książce — lektura całości za jednym posiedzeniem może być trudna. Dlatego rozdział został podzielony na wprowadzenie i dwie główne części. Taki podział powinien ułatwić podjęcie decyzji o przerwie. W części „Przegląd architektury” opisałem poszczególne wzorce projektowe. Są one stosunkowo niezależnymi bytami, czytelnicy powinni więc dać radę użyć każdego z nich z osobna w swoich projektach, zapoznając się potem w wolnej chwili z pozostałymi powiązanymi wzorcami. W rozdziale omówione zostaną następujące zagadnienia:  Przegląd architektury — wprowadzenie do warstw typowych dla aplikacji korporacyjnych.  Wzorzec Registry — do zarządzania danymi aplikacji.  Warstwa prezentacji — narzędzia do zarządzania żądaniami i reagowania na nie, a także do prezentowania danych użytkownikom aplikacji.  Warstwa logiki biznesowej — realizująca faktyczne zadania aplikacji.

Przegląd architektury Jest sporo materiału do opanowania, zaczniemy więc od przeglądu wzorców wykorzystanych w dalszym omówieniu oraz od wprowadzenia do projektowania aplikacji warstwowych, czyli podzielonych na warstwy funkcjonalne.

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Wzorce Pora na przedstawienie wzorców wykorzystywanych w rozdziale. Tę część można czytać od początku do końca, albo wybrać dowolny z interesujących wzorców, bez zachowania kolejności omówienia. Wypada zaznaczyć, że wzorzec Command (polecenie) nie będzie tu omawiany osobno (była o nim mowa w rozdziale 11.), choć występuje we wzorcach Front Controller i Application Controller.  Registry — wzorzec rejestru przydaje się przy udostępnianiu danych dla klas. Przy odpowiednim stosowaniu serializacji można również zapewnić dostępność informacji pomiędzy żądaniami (w obrębie sesji), a nawet pomiędzy instancjami aplikacji.  Front Controller — wykorzystywany w większych systemach, w których potrzebna jest możliwie duża elastyczność i możliwość zarządzania potencjalnie wieloma widokami i poleceniami.  Application Controller — tworzy klasę zarządzającą logiką widoku i wyboru poleceń.  Template View — tworzy strony zarządzające wyłącznie interfejsem użytkownika i wyświetlaniem danych, wtłaczając informacje dynamiczne do formatu znacznikowego za pomocą możliwie najmniejszej ilości gołego kodu.  Page Controller — lżejsza, ale też mniej elastyczna wersja Front Controllera, realizująca jednak identyczne zadanie. Wzorzec użyteczny przy zarządzaniu żądaniami i obsługą logiki widoku tam, gdzie chcemy szybkich rezultatów i nie przewidujemy przyszłego znaczącego zwiększenia złożoności systemu.  Transaction Script — kiedy chcemy szybko i dobrze, przy minimalnym nakładzie na planowanie, zwracamy się w logice aplikacji do biblioteki kodu proceduralnego. Wzorzec ten słabo się skaluje.  Domain Model — drugi biegun względem wzorca Transaction Script; wykorzystujemy go do budowania obiektowych modeli procesów i komponentów biznesowych.

Aplikacje i warstwy Wiele (a właściwie większość) wzorców z tego rozdziału ma służyć promowaniu podziału aplikacji na szereg jak najmniej zależnych od siebie warstw. Warstwy w systemie korporacyjnym pełnią rolę specjalizującą, podobnie do klas, tyle że na nieco większą skalę. Typowy podział warstwowy systemu prezentuje rysunek 12.1.

Rysunek 12.1. Warstwy typowego systemu korporacyjnego

232

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

Struktura prezentowana na rysunku 12.1 nie jest zupełnie sztywna — w różnych projektach niektóre z prezentowanych warstw są ze sobą łączone, wdrażane są też odmienne strategie komunikacji pomiędzy warstwami; wybór konkretnej architektury jest zależny od stopnia złożoności konkretnego systemu. Niemniej jednak rysunek ten ilustruje model zapewniający elastyczność i dający możliwość wielokrotnego wykorzystania elementów w późniejszych projektach, więc większość aplikacji korporacyjnych jest z nim w dużym stopniu zgodna.  Warstwa widoku obejmuje interfejs prezentowany końcowym użytkownikom systemu, za pośrednictwem którego komunikują się oni z systemem. Interfejs ten odpowiedzialny jest za prezentowanie użytkownikom wyników przetwarzania ich żądań oraz za udostępnianie mechanizmów, za pośrednictwem których użytkownicy mogą kierować żądania do systemu.  Warstwa poleceń i kontroli przetwarza żądania odbierane od użytkownika. Po analizie żądań deleguje je do warstwy logiki biznesowej, w ramach której odbywa się właściwe przetwarzanie niezbędne do wypełnienia żądania. Następnie warstwa poleceń i kontroli wybiera sposób prezentacji wyników przetwarzania. W praktyce często mamy do czynienia z połączeniem tej warstwy z warstwą widoku, tworząc warstwę prezentacji. Ale nawet w tych przypadkach rola czystej prezentacji widoku i rola rozprowadzania żądań do wywołań logiki biznesowej powinny być rozłączne.  Warstwa logiki biznesowej jest odpowiedzialna za właściwe przetwarzanie żądania. Wykonuje wszelkie niezbędne obliczenia i porządkuje wyniki.  Warstwa danych to cała reszta systemu, obejmująca mechanizmy utrwalania i pozyskiwania danych. W niektórych systemach warstwa poleceń i kontroli korzysta z warstwy danych celem pobierania z niej obiektów biznesowych, które mają ostatecznie zrealizować żądania. W innych systemach warstwa danych jest starannie ukrywana przed resztą. Jaki jest cel takiego podziału systemu na warstwy? Odpowiedź, tradycyjnie, tkwi w rozluźnianiu sprzęgania. Starając się izolować i uniezależniać warstwę logiki biznesowej od warstwy widoku, umożliwiamy na przykład proste uzupełnianie systemu o nowe interfejsy — proste, bo niewymagające daleko idących ingerencji w kod poza warstwą widoku. Wyobraźmy sobie system, którego zadanie polega na zarządzaniu listami imprez (pod koniec rozdziału będziemy już z tym przykładem za pan brat). Użytkownik końcowy wymaga oczywiście interfejsu HTML. Opiekunowie systemu chętniej korzystaliby zapewne z interfejsu z poziomu wiersza polecenia, dzięki któremu mogliby automatyzować procedury konserwacyjne. Równocześnie system ma być przystosowany do współpracy z telefonami komórkowymi i innymi urządzeniami przenośnymi. Zaczynamy nawet rozważać wdrożenie takich usług jak SOAP czy RESTful API. Jeśli u zarania projektu połączymy warstwę logiki systemu z warstwą widoku HTML (co nie jest po dziś dzień praktyką rzadką mimo licznych przeciwwskazań), opisane wymagania wymuszałyby ciągłe poprawki i przepisywanie kodu. Gdyby zaś zdecydować się od początku na wyróżnienie i izolację warstw, uzupełnienie systemu o nowe strategie prezentacji nie powinno wymagać żadnych ingerencji w warstwę logiki biznesowej systemu ani w jego warstwę danych. Równie zmienne mogą się okazać stosowane w systemie strategie utrwalania danych. I tu powinniśmy więc mieć możliwość przełączania systemu pomiędzy różnymi modelami składowania i pozyskiwania danych, bez zaburzania działania reszty systemu. Kolejnym dobrym powodem do wyróżnienia i oddzielenia warstw jest testowanie systemu. Testowanie aplikacji WWW to wyjątkowo niewdzięczne zadanie. Naturalna chęć automatyzacji testów boryka się z jednej strony z koniecznością analizy wyników zwracanych w formacie HTML, z drugiej zaś z koniecznością bezpośredniego operowania na otwartej bazie danych. Testy należy więc przeprowadzać jedynie na w pełni wdrożonym systemie, co z kolei wiąże się z ryzykiem uszkodzenia systemu, który przecież z testów powinien wyjść mocniejszy, a nie słabszy. W każdej warstwie klasy znajdujące się na styku z innymi warstwami są często napisane tak, aby rozszerzały abstrakcyjną klasę nadrzędną albo implementowały interfejs. Ów typ nadrzędny mógłby obsługiwać polimorfizm. W kontekście testów cała warstwa mogłaby zostać dzięki temu zastąpiona zbiorem obiektów-atrap; można by dzięki temu przetestować działanie warstwy logiki biznesowej w oparciu o atrapę warstwy danych, bez ryzykowania naruszenia właściwych zasobów utrwalonych w systemie. O testowaniu aplikacji dowiesz się więcej z rozdziału 18.

233

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Czy wyróżnienie warstw ma dla Ciebie jakąś wartość, jeśli uważasz, że poradzisz sobie z testowaniem, a system będzie po wsze czasy działał z jednym interfejsem? Tak, bo przez utworzenie warstw o odrębnych zakresach odpowiedzialności konstruujesz system, którego poszczególne elementy można łatwo rozszerzać i diagnozować. Ograniczasz też powielanie kodu, skupiając w jednym miejscu kod z pewnej dziedziny zadań (unikasz wtedy szpikowania systemu wciąż tymi samymi wywołaniami funkcji obsługi baz danych i funkcjami formatowania pozyskanych wyników). Rozbudowa systemu jest wtedy znacznie łatwiejsza, bo odbywać się będzie niejako wzdłuż, a nie w poprzek systemu. Wprowadzenie do systemu warstwowego nowej cechy może wymagać utworzenia nowego komponentu interfejsu, dodatkowego kodu obsługi żądań, pewnego nowego modułu biznesowego oraz pewnego uzupełnienia warstwy danych. Takie zmiany idą niejako „wzdłuż” projektu systemu. W systemie pozbawionym rozróżnienia warstwowego zmiana taka zazwyczaj kończy się na próbach odszukania tych pięciu (a może jednak sześciu?) stron, na których występowało odwołanie do uzupełnionej tabeli bazy danych, i całych tuzinów miejsc, w których potencjalnie może być wykorzystany nowy element interfejsu. Przekopać się trzeba wtedy przez cały system — nazywam to zmianą horyzontalną, zmianą w poprzek systemu. W praktyce oczywiście nie da się nigdy całkowicie wyeliminować zależności implikujących zmiany horyzontalne, zwłaszcza tam, gdzie chodzi o nawigacyjne elementy interfejsu. Ale i tak warstwowa architektura systemu pozwala na minimalizację takich zależności, a więc i redukcję zasięgu zmian horyzontalnych.  Uwaga Wiele z prezentowanych w tym rozdziale wzorców znanych jest od dłuższego już czasu (w końcu wzorce to przede wszystkim kodyfikacja dobrych praktyk), jednak ich nazewnictwo i rozgraniczenie zostały zapożyczone od Martina Fowlera z jego publikacji o wzorcach korporacyjnych Enterprise Application Architecture1, ewentualnie z Core J2EE Patterns2 autorstwa Alura i reszty. W miejscach nakładania się obydwu źródeł będę się trzymał nomenklatury proponowanej przez Martina Fowlera, ponieważ jego publikacja mniej skupia się na konkretnej technologii, a więc i ma szersze zastosowanie. Alur koncentruje się w swojej książce na technologii Enterprise Java Beans, co oznacza, że prezentowane w niej wzorce zostały dobrane i zoptymalizowane pod kątem architektur rozproszonych — a te w PHP są architekturami w najlepszym przypadku niszowymi. Jeśli uznasz ten rozdział za interesujący, polecałbym wykonanie następnego kroku i sięgnięcie po obie te książki. Jeśli nawet nie znasz Javy, to jako ukierunkowany obiektowo programista PHP nie powinieneś mieć problemów z rozszyfrowaniem przykładów z języka Java.

Wszystkie przykłady prezentowane w tym rozdziale będą ściślej lub mniej ściśle związane właśnie z owym fikcyjnym systemem rejestrującym i rozpowszechniającym informacje o imprezach kulturalnych i innych. System będzie nosił nazwę WOO (od „what’s on outside” czy też „wydarzenia okiem obytego”). Uczestnikami systemu będą placówki (teatry, kina, kluby), konkretne lokalizacje („pierwsze piętro”, „sala 10” czy „scena główna”) i właściwe imprezy (The Long Good Friday czy odczyt O istocie bycia sumiennym). Operacje, które będziemy teraz omawiać, obejmować będą wprowadzanie do systemu placówki, uzupełnianie jej o lokalizacje oraz wyświetlanie wykazu placówek wprowadzonych do systemu. Pamiętajmy, że celem tego rozdziału jest ilustrowanie podstawowych korporacyjnych wzorców projektowych, nie zaś konstruowanie działającego systemu. Większość przykładów będzie odzwierciedlała współzależności pomiędzy poszczególnymi wzorcami, często więc będziemy się odwoływać do już przytaczanych kodów i koncepcji. Ponieważ kod ma jedynie ilustrować użycie wzorców typowych dla aplikacji korporacyjnych, w znacznej części nie będzie spełniał wymogów odnośnie do kodu produkcyjnego; w szczególności brakuje w nim solidnej obsługi błędów — pozbyłem się jej całkowicie tam, gdzie wprowadzenie jej zmniejszałoby przejrzystość przykładu. Przykłady należy więc traktować jako ilustracje omawianych akurat wzorców, a nie kolejne gotowe klocki, z których w końcu można będzie złożyć działający szkielet aplikacji.

1 2

Wydanie polskie: Architektura systemów zarządzania przedsiębiorstwem. Wzorce projektowe, Helion, 2005 — przyp. tłum. Wydanie polskie: J2EE. Wzorce projektowe. Wydanie 2, Helion, 2004 — przyp. tłum.

234

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

Małe oszustwo na samym początku Większość wzorców omawianych w tej książce w naturalny sposób wpisuje się w warstwową architekturę charakterystyczną dla systemów korporacyjnych. Niektóre z wzorców są jednak tak podstawowe, że nie mieszczą się w takiej strukturze. Dobrym przykładem jest choćby wzorzec Registry — wzorzec ten to sposób na złamanie ograniczeń nakładanych przez podział warstwowy. Ale w końcu każda reguła wyjątkami stoi.

Wzorzec Registry Wzorzec Registry (rejestr) dotyczy udostępniania obiektów danych. Wzorzec ten to artykulacja wiary w niestosowność zmiennych globalnych. Niestety, dane globalne, jak wszystkie grzechy, są z wielu względów atrakcyjne. Doprowadziło to do sytuacji, w której architekci systemów obiektowych uznali za niezbędne ponowne wynalezienie danych globalnych, ale — by uniknąć potępienia — pod inną nazwą. Przykład tej determinacji mieliśmy w rozdziale 9., w którym opisywany był wzorzec Singleton. To prawda, że singletony nie podzielają słabości typowych dla zmiennych globalnych. W szczególności zaś nie da się takiego obiektu przez przypadek nadpisać czy zamazać. Singletony można więc uznać za „zdrowe” (dietetycy nazwaliby je pewnie „niskotłuszczowymi”) dane globalne. Mimo to pozostajemy wobec nich podejrzliwi, bo zachęcają do kotwiczenia klas w konkretnym systemie, zwiększając sprzęganie pomiędzy klasami. Jednak singletony okazują się od czasu do czasu na tyle użyteczne, że wielu programistów (w tej liczbie i ja) nie potrafi zrezygnować z ich stosowania.

Problem Jak Ci zapewne wiadomo, wiele systemów korporacyjnych cechuje się podziałem na warstwy, przy założeniu, że poszczególne warstwy komunikują się jedynie z warstwami bezpośrednio z nimi sąsiadującymi i tylko za pośrednictwem ściśle zdefiniowanych ścieżek komunikacji. Taki podział daje aplikacji elastyczność pozwalającą na zastępowanie i inne modyfikacje każdej z warstw z osobna, przy minimalnym oddźwięku zmian w pozostałych warstwach systemu. Co jednak w przypadku, kiedy w jednej warstwie pozyskamy informację, którą mamy potem wykorzystać w innej, i to nie sąsiedniej, warstwie? Załóżmy, że dane konfiguracyjne systemu pozyskujemy z klasy ApplicationHelper: // woo\controller\ApplicationHelper class ApplicationHelper { function getOptions() { if (!file_exists("data/woo_options_not_there.xml")) { throw new \woo\base\AppException("Nie można znaleźć pliku opcji"); } $options = simplexml_load_file("data/woo_options.xml"); $dsn = (string)$options->dsn; // co z tym zrobić? // … } }

Samo pozyskanie informacji okazuje się nieskomplikowane, gorzej z przekazaniem ich do warstwy danych, w której miałyby zostać spożytkowane. I nie tylko, bo rozmaite dane konfiguracyjne trzeba przekazać różnym elementom systemu. Rozwiązaniem problemu dystrybucji opcji może być przekazywanie informacji w systemie od obiektu do obiektu — od obiektu kontrolera, odpowiedzialnego za obsługę żądań, przez obiekty warstwy logiki biznesowej, po obiekty odpowiedzialne za komunikację z bazą danych. Taki model jest jak najbardziej wykonalny. Można przecież przekazywać pomiędzy obiektami systemu obiekt klasy ApplicationHelper, ewentualnie bardziej specjalizowany obiekt klasy Context. Tak czy inaczej w efekcie informacje kontekstowe zostaną rozprowadzone w systemie pomiędzy potrzebującymi ich obiektami.

235

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Wadą takiego podejścia jest to, że wymaga ono modyfikacji interfejsu wszystkich obiektów odwołujących się do obiektu kontekstu. Zdecydowanie przeczy to koncepcji jak najdalej idącego rozluźniania sprzęgnięć. Rozwiązanie alternatywne, choć niepozbawione innych wad, proponuje wzorzec Registry (rejestr). Rejestr to po prostu klasa, która udostępnia dane (zazwyczaj, choć nie wyłącznie, w postaci obiektów) za pośrednictwem metod statycznych (albo metod egzemplarza klasy, o ile jest to Singleton). Każdy obiekt w systemie ma wtedy dostęp do owych danych. Nazwa wzorca została zaproponowana przez Martina Fowlera w jego książce Patterns of Enterprise Application Architecture3, ale — jak to w przypadku wzorców — implementacje pojawiają się tu i tam pod różnymi nazwami. David Hunt i David Thomas (w książce Pragmatic Programmer4) porównują klasę Registry do policyjnej tablicy incydentów na posterunkach. Detektywi kończący zmianę zostawiają na niej dowody i szkice, przejmowane przez detektywów następnej zmiany. Registry występuje też często pod nazwą Whiteboard bądź Blackboard.

Implementacja Rysunek 12.2 prezentuje obiekt Registry, którego zadaniem jest składowanie i udostępnianie obiektów Request.

Rysunek 12.2. Prosty rejestr A tu mamy przykładową implementację klasy rejestru: class Registry { private static $instance; private $request; private function __construct() { } static function instance() { if (! isset(self::$instance)) { self::instance = new self(); } return self::$instance; } function getRequest() { if (is_null($this->request)) { $this->request = new Request(); } } } // Klasa-atrapa obiektu żądania: class Request {}

3 4

Wydanie polskie: Architektura systemów zarządzania przedsiębiorstwem. Wzorce projektowe, Helion, 2005 — przyp. tłum. Wydanie polskie: Pragmatyczny programista. Od czeladnika do mistrza, Helion, 2011 — przyp. tłum.

236

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

W takim układzie ten sam obiekt Request może być dostępny z dowolnego miejsca systemu: $reg = Registry::instance(); print_r($reg->getRequest());

Jak widać, rejestr to zwykły Singleton (jeśli nie pamiętasz dokładnie, co to takiego, zajrzyj do rozdziału 9.). Powyższy kod tworzy i pozyskuje egzemplarz klasy Registry za pośrednictwem statycznej metody instance(). Uzyskany obiekt można wykorzystać do ustawiania i odczytywania obiektu żądania — Request. Można też zrezygnować z ostrożności i zastosować rejestr bazujący na kluczach, jak tutaj: class Registry { private static $instance = null; private $values = array(); private function __construct() { } static function instance() { if (!is_null(self::$instance)) { self::instance = new self(); } return self::$instance; } function get($key) { if (isset($this->values[$key])) { return $this->values[$key]; } return null; } function set($key, $value) { $this->values[$key] = $value; } }

Takie rozwiązanie ma tę zaletę, że nie trzeba tworzyć osobnych metod rejestru dla każdego obiektu danych, który miałby być w rejestrze przechowywany i przezeń udostępniany. Wadą jest za to ponowne wprowadzenie (choć tylnymi drzwiami) zmiennych globalnych. Zastosowanie w roli kluczy dowolnych ciągów oznacza bowiem brak kontroli nad parami klucz i wartość i tym samym możliwość niekontrolowanego nadpisywania takich par przez różne części systemu korzystające z rejestru bez uzgodnienia. Taka struktura jest moim zdaniem użyteczna na etapie rozwoju systemu, ale w finalnej wersji najlepiej przejść na odpowiednio nazwane metody wymuszające typy argumentów, aby była jasność, jakie dane rejestr przechowuje i jakie udostępnia. Obiekty rejestru można również wykorzystywać w roli wytwórni dla powszechnie stosowanych obiektów w systemie. Zamiast przechowywać udostępniony obiekt klasa rejestru tworzy jego egzemplarz i zachowuje u siebie referencję do tego egzemplarza. Przy okazji klasa rejestru może zainicjować jakieś wstępne operacje na obiekcie, na przykład wczytać dane z pliku konfiguracyjnego albo złożyć ze sobą kilka potrzebnych obiektów. // klasa Registry... private $treeBuilder = null; private $conf = null; // .... function treeBuilder() { if (!is_null($this->treeBuilder)) { $this->treeBuilder = new TreeBuilder($this->conf()->get('treedir')); } return $this->treeBuilder; } function conf() {

237

PHP. OBIEKTY, WZORCE, NARZĘDZIA

if (!is_null($this->conf)) { $this->conf = new Conf(); } return $this->conf; }

Klasy TreeBuilder i Conf to zwyczajne atrapy, mające jedynie ilustrować sposób stosowania rejestru w roli wytwórni. Klasa kliencka potrzebująca obiektu TreeBuilder może zwyczajnie wywołać metodę Registry::treeBuilder() i nie musi zajmować się zawiłościami inicjalizacji obiektu — centralizacja operacji inicjalizacji to duża zaleta typowa dla wytwórni. Do takich zawiłości zaliczylibyśmy choćby konieczność utworzenia obiektu konfiguracji Conf; lepiej, żeby klasy klienckie nie musiały robić tego wszystkiego „na piechotę”. Obiekty rejestru są przydatne również przy testowaniu. Statyczna metoda instance() może wtedy posłużyć do podania klasy pochodnej Registry operującej wyłącznie na atrapach utworzonych pod kątem testów. Od strony metody instance() wyglądałoby to tak: static function testMode($mode=true) { self::$instance=null; self::$testmode=$mode; } static function instance() { if (is_null(self::$instance)) { if (self::$testmode) { self::$instance = new MockRegistry(); } else { self::$instance = new self(); } } return self::$instance; }

Kiedy trzeba przetestować system, wystarczy ustawić przełącznik trybu testowego i przełączyć się na atrapowy rejestr. W ten sposób można udostępniać atrapy (obiekty naśladujące faktyczne środowisko wykonawcze dla potrzeb testów) oraz imitacje (obiekty podobne, które również analizują wywołania na ich rzecz i sprawdzają ich poprawność). Registry::testMode(); $mockreg = Registry::instance();

O atrapach i imitacjach można przeczytać więcej w rozdziale 18. („Testy jednostkowe PHPUnit”).

Rejestr, zasięg a PHP Pojęcie „zasięg” często wykorzystuje się w opisie zakresu widoczności obiektu bądź wartości w kontekście struktur sterujących wykonaniem kodu. Zasięg zmiennej można również odmierzać czasem życia zmiennej. W tym ujęciu możemy rozróżnić trzy pojęcia zasięgu. Zasięg standardowy to czas obejmujący żądanie protokołu HTTP. PHP udostępnia za to zestaw wbudowanych zmiennych pozwalających na obsługę sesji. Są one serializowane i utrwalane w systemie plików bądź w bazie danych przy końcu żądania, a potem odtwarzane na początku następnego. Do rozróżniania sesji służy identyfikator przechowywany w „ciastku” albo przekazywany pomiędzy żądaniami. Można więc uznać, że niektóre zmienne mogą swoim zasięgiem obejmować nie tylko pojedyncze żądania, ale całe sesje. Można zatem utrwalać wybrane obiekty pomiędzy żądaniami, oszczędzając sobie potem samodzielnych wycieczek do bazy danych. Trzeba w takim układzie uważać, aby w systemie nie doszło do powołania do życia wielu wersji tego samego obiektu — można rozważyć wdrożenie pewnej strategii blokowania ze sprawdzaniem, czy obiekt wprowadzany do programu nie został wcześniej utrwalony w ramach sesji. W innych językach, jak Java i Perl (obsługiwany w serwerze Apache za pośrednictwem modułu ModPerl), mamy też pojęcie zasięgu aplikacji. Zmienne o takim zasięgu są dostępne we wszystkich egzemplarzach programu. Taki model jest obcy językowi PHP, ale w większych aplikacjach dostęp do przestrzeni aplikacji okazuje się niezwykle użyteczny, choćby właśnie w kontekście odczytywania parametrów konfiguracyjnych. Można co prawda w PHP skonstruować klasę rejestru emulującą zasięg aplikacji, ale trzeba to zrobić świadomie. 238

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

Możliwą strukturę klas hierarchii Registry, działających na trzech opisanych przeze mnie poziomach, prezentuje rysunek 12.3.

Rysunek 12.3. Implementacje klas hierarchii Registry dla różnych zasięgów Klasa bazowa definiuje dwie metody zabezpieczone: get() i set(). Nie są one dostępne dla użytkowników zewnętrznych, ponieważ dla operacji ustawiania i pozyskiwania danych chcemy narzucić wymuszanie typów konkretyzowanych w klasach pochodnych. Klasa bazowa może też definiować inne publiczne metody pomocnicze, jak isEmpty(), isPopulated() czy clear(), ale to pozostawię już jako ćwiczenie Czytelnikowi.  Uwaga W systemie wdrażanym rzeczywiście można by rozszerzyć tę strukturę tak, aby obejmowała kolejną warstwę dziedziczenia. Można na przykład zachować zabezpieczone implementacje metod get() i set(), za to specjalizować publiczne metody getAaa() i setAaa() w klasach obsługujących poszczególne dziedziny systemu. Nowe specjalizacje byłyby singletonami. W ten sposób można by wielokrotnie wykorzystywać w różnych aplikacjach rdzeń operacji pobierania i utrwalania danych w rejestrze.

Oto zarys kodu klasy abstrakcyjnej: namespace woo\base; abstract class Registry { abstract protected function get($key); abstract protected function set($key, $val); }

 Uwaga Zwróćmy uwagę na użycie przestrzeni nazw w kodzie przykładów. Ponieważ przy okazji omówienia zamierzamy zmontować kompletny (choć uproszczony) system, utworzenie hierarchii pakietów będzie bardzo pomocne, a przy okazji zilustruje prostotę i zwartość nazw wprowadzanych do projektu dzięki przestrzeniom nazw.

Klasa rejestru zasięgu żądania jest wyjątkowo prosta. W kolejnej wariacji poprzedniego przykładu zachowujemy po prostu egzemplarz Registry w ukryciu, udostępniając jedynie statyczne metody do rejestrowania i pozyskiwania obiektów Request. Reszta to po prostu obsługa tablicy asocjacyjnej.

239

PHP. OBIEKTY, WZORCE, NARZĘDZIA

namespace woo\base; // ... class RequestRegistry extends Registry { private $values = array(); private static $instance = null; private function __construct() {} static function instance() { if (!is_null(self::$instance)) { self::$instance = new self(); } return self::$instance; } protected function get($key) { if (isset($this->values[$key])) { return $this->values[$key]; } return null; } protected function set($key, $val) { $this->values[$key] = $val; } static function getRequest() { $inst = self::instance(); if (is_null($inst->get("request"))) { $inst->set('request', new \woo\controller\Request()); } return $inst->get("request"); } }

Implementacja dla zasięgu sesji korzysta już z wbudowanej w PHP obsługi sesji: namespace woo\base; // ... class SessionRegistry extends Registry { private static $instance = null; private function __construct() { session_start(); } static function instance() { if (!is_null(self::$instance)) { self::$instance = new self(); } return self::$instance; } protected function get($key) { if (isset($_SESSION[__CLASS__][$key])) { return $_SESSION[__CLASS__][$key]; } return null; } protected function set($key, $val) {

240

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

$_SESSION[__CLASS__][$key] = $val; } function setDSN($dsn) { self::instance()->set('dsn', $dsn); } function getDSN() { return self::instance()->get("dsn"); } }

Jak widać, tym razem zamiast tablicy asocjacyjnej mamy najbardziej globalną jak to możliwe zmienną $_SESSION. Sesję inicjujemy w ramach konstruktora wywołaniem metody session_start(). Jak to przy sesjach,

trzeba się upewnić, że przed zastosowaniem tej klasy do klienta HTTP nie zostanie przesłany żaden tekst. Zgodnie z oczekiwaniami najbardziej problematyczna będzie implementacja rejestru zasięgu aplikacji. Podobnie jak we wszystkich przykładach z tego rozdziału prezentowany w tej roli kod należy traktować raczej jako ilustrację niż kod produkcyjny: namespace woo\base; // ... class ApplicationRegistry extends Registry { private static $instance = null; private $freezedir = "data"; private $values = array(); private $mtimes = array(); private function __construct() { } static function instance() { if (!is_null(self::$instance)) { self::$instance = new self(); } return self::$instance; } protected function get($key) { $path = $this->freezedir . DIRECTORY_SEPARATOR . $key; if (file_exists($path)) { clearstatcache(); $mtime=filemtime($path); if (! isset($this->mtimes[$key])) { $this->mtimes[$key]=0; } if ($mtime > $this->mtimes[$key]) { $data = file_get_contents($path); $this->mtimes[$key]=$mtime; return ($this->values[$key]=unserialize($data)); } } if (isset($this->values[$key])) { return $this->values[$key]; } return null; } protected function set($key, $val) { $this->values[$key] = $val;

241

PHP. OBIEKTY, WZORCE, NARZĘDZIA

$path = $this->freezedir . DIRECTORY_SEPARATOR . $key; file_put_contents($path, serialize($val)); $this->mtimes[$key]=time(); } static function getDSN() { return self::instance()->get('dsn'); } static function setDSN($dsn) { return self::instance()->set('dsn', $dsn); } static function getRequest() { $inst = self::instance(); if (is_null($inst->request)) { $inst->request = new \woo\controller\Request(); } return $inst->request; } }

Niniejsza klasa zachowuje i odtwarza pojedyncze właściwości za pomocą mechanizmu serializacji. Metoda get() sprawdza obecność odpowiedniego pliku wartości właściwości; jeśli plik istnieje i został zmodyfikowany

od czasu ostatniego odczytu, metoda odserializowuje i zwraca jego zawartość. Ponieważ otwieranie pliku do odtworzenia wartości każdej zarządzanej w rejestrze zmiennej jest nieefektywne, taki mechanizm można zamienić na wydajniejszy, bazujący na przykład na pojedynczym pliku. Metoda set() zarówno zmienia lokalnie właściwość identyfikowaną przez $key (w egzemplarzu obiektu), jak i utrwala zmianę w pliku właściwości oraz aktualizuje właściwość $mtimes. Jest to tablica czasów modyfikacji pliku, wykorzystywana do sprawdzania aktualności wartości właściwości. Kiedy później dojdzie do wywołania metody get(), można będzie sprawdzić odpowiedni wpis w tablicy $mtimes i stwierdzić na tej podstawie, czy obiekt trzeba odtwarzać z pliku, czy można zwrócić zbuforowaną wartość. Zwróćmy uwagę na metodę getRequest() tutaj i w podobnie zorientowanej wersji prezentowanego wcześniej obiektu Registry. W obu przypadkach nie istnieje metoda ustawiająca setRequest(). Klienci zewnętrzni nie mogą konkretyzować i inicjalizować własnych obiektów Request — nasz mechanizm wyznacza Registry jako jedyne źródło współdzielonego obiektu Request i daje gwarancję, że w całym systemie będzie występował tylko jeden taki obiekt. Jest to wielce użyteczne w testach — przed ich uruchomieniem wystarczy spreparować odpowiedni obiekt Request w Registry. Pozwala to na ustawianie różnych warunków początkowych i obserwowanie zachowania i wyników systemu. Zauważmy też, że klasa ApplicationRegistry przechowuje obiekt Request w prostej właściwości $request, a nie w postaci utrwalonej w pliku. Jest to przecież jeden z tych obiektów systemowych, których nie chcemy utrwalać pomiędzy zapytaniami do aplikacji! Jeśli instalacja PHP została skompilowana z rozszerzeniem apc, operacje na plikach można zastąpić operacjami na segmentach pamięci współdzielonej SHM. Oto uproszczony przykład takiej implementacji: namespace woo\base; // ... class MemApplicationRegistry extends Registry { private static $instance = null; private $values=array(); private $id; private function __construct() {} static function instance() { if (!is_null(self::$instance)) { self::$instance = new self(); } return self::$instance;

242

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

} protected function get($key) { return \apc_fetch( $key); } protected function set($key, $val) { return \apc_store($key, $val); } static function getDSN() { return self::instance()->get("dsn"); } static function setDSN( $dsn ) { return self::instance()->set("dsn", $dsn); } }

Jeśli zamierzasz tworzyć własne odmiany powyższego przykładu, zapoznaj się jeszcze z następnym punktem, poświęconym konsekwencjom takich implementacji.  Uwaga Ponieważ rozszerzenie APC nie jest domyślnie instalowane z PHP, może zajść potrzeba ręcznej instalacji rozszerzenia. Instrukcje instalacji można znaleźć w podręczniku PHP na stronie http://www.php.net/manual/en/apc.installation.php.

Konsekwencje Ponieważ zarówno obiekty klas SessionRegistry, jak i ApplicationRegistry serializują dane i utrwalają je w systemie plików, trzeba przypomnieć, że obiekty pozyskiwane w różnych żądaniach są identycznymi kopiami i nie stanowią bynajmniej jednego obiektu. Nie ma to takiego znaczenia w przypadku SessionRegistry, bo tam w każdej instancji do obiektu odwołuje się ten sam użytkownik. Ale już w ApplicationRegistry może to stanowić poważniejszy problem. Jeśli nie będziesz powściągał utrwalania danych, możesz doprowadzić do konfliktu dwóch procesów. Spójrzmy choćby poniżej: Proces Proces Proces Proces Proces Proces

1 2 1 2 1 2

pozyskuje obiekt pozyskuje obiekt zmienia obiekt zmienia obiekt utrwala obiekt utrwala obiekt

Zmiany wprowadzone w obiekcie przez proces z numerem jeden zostaną w takim układzie zamazane przez zmiany inicjowane w procesie drugim. Jeśli więc poważnie myślisz o utworzeniu współużytkowanego przez procesy obszaru danych o zasięgu aplikacji, powinieneś uzupełnić klasę ApplicationRegistry o mechanizmy blokowania zapobiegające tego rodzaju kolizjom. Alternatywą może być potraktowanie ApplicationRegistry jako ogólnodostępnego (w zasięgu aplikacji) repozytorium zasobów niemodyfikowalnych. Sposób ten zastosujemy w przykładach w dalszej części rozdziału. Obiekty w repozytorium są inicjalizowane raz, a wszelkie odwołania do rejestru aplikacji mają charakter odczytu obiektów danych. Kod będzie obliczał nowe wartości danych jedynie wtedy, kiedy nie uda się odnaleźć pliku utrwalającego wartości poprzednie. Przeładowanie danych konfiguracyjnych wymusza się wtedy usunięciem pliku przechowującego obiekty danych. Samą klasę należałoby zaś uzupełnić tak, aby nie pozwalała na dostęp modyfikujący dane rejestru. Innym ważnym aspektem stosowania rejestru zasięgu aplikacji jest to, że nie każdy obiekt nadaje się do serializacji. W szczególności nie podlegają jej najróżniejsze zasoby systemowe, jak choćby uchwyty połączeń z bazami danych. Trzeba wtedy opracować strategie utrwalania uchwytów przy serializacji i ich odtwarzania przy operacji odwrotnej.

243

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Co więc wybrać? W praktyce niemal zawsze uciekam się do opcji najprostszej, to znaczy rejestru zasięgu żądania. Z pewnością unikam stosowania więcej niż jednego typu rejestru w systemie — nie ma prostszej drogi do rozmaitych, trudnych do wychwycenia błędów. Z kolei mechanizm buforowania zaimplementowany w przykładzie z ApplicationRegistry eliminuje jeden z problemów wzorca Front Controller, a mianowicie koszt przetwarzania skomplikowanych formatów konfiguracji dla każdego żądania z osobna. W praktyce zapewne wyodrębniłbym mechanizm buforowania konfiguracji i zachował prostszy rejestr zasięgu żądania. Natomiast na potrzeby omówienia zostaniemy jeszcze przy rejestrze zasięgu aplikacji.  Uwaga Jednym ze sposobów zarządzania serializacją jest implementacja specjalnych metod __sleep() i __wakeup(). Metoda __sleep() jest wywoływana na rzecz obiektu automatycznie, kiedy ten ma podlegać serializacji. Można w jej ramach wykonać wszelkie czynności, które mają poprzedzić utrwalenie obiektu. Metoda powinna zwrócić tablicę ciągów reprezentujących pola, które mają zostać zserializowane. Z kolei __wakeup() wywoływana jest podczas „odserializowania”. Tę parę metod można wykorzystać do odtwarzania wszelkich uchwytów plików i baz danych obiektu.

Choć serializacja jest w PHP realizowana dość efektywnie, warto mimo wszystko zachować powściągliwość w utrwalaniu obiektów danych. Niewinnie wyglądający obiekt może przecież zawierać referencję znacznej liczby innych obiektów, np. „wyciąganych” z bazy danych. Obiekty hierarchii Registry udostępniają swoje dane globalnie. Oznacza to, że każda klasa występująca w roli użytkownika rejestru będzie cechować się zależnością od tego rejestru, niekoniecznie jawnie deklarowaną w jej interfejsie. To z kolei może okazać się poważnym problemem, zwłaszcza jeśli rejestr stanie się powszechnym środkiem utrwalania danych w systemie. Obiekty Registry powinny być stosowane z rozwagą i tylko dla dobrze określonych zestawów danych.

Warstwa prezentacji Kiedy do systemu trafia żądanie, trzeba rozpoznać nim wymagania, przekazać je do warstwy logiki biznesowej, a następnie zwrócić klientowi inicjującemu żądanie właściwą odpowiedź. W przypadku prostych skryptów cały ten proces następuje zwykle w całości w samym widoku z ewentualnym wyodrębnieniem co cięższych gatunkowo funkcji i powtarzającego się kodu do bibliotek.  Uwaga Widok to pojedynczy element warstwy widoku. W języku PHP to najczęściej pojedyncza strona (albo kolekcja ułożonych elementów widoku), której podstawowym zadaniem jest wyświetlenie danych i udostępnienie mechanizmu generowania nowych żądań. W systemach opartych na szablonach (jak Smarty) widokiem może być też szablon.

W miarę rozrostu systemów taka strategia wykazała niewydolność i nie dało się już dłużej podtrzymywać powielania kodu przetwarzania żądań, kodu logiki biznesowej i kodu wyboru widoku w każdym z kolejnych widoków. W niniejszym podrozdziale przyjrzymy się więc strategiom zarządzania owymi trzema podstawowymi zadaniami warstwy prezentacji. Do „warstwy prezentacji” będziemy przy tym zaliczać to, co wcześniej określiliśmy jako warstwy poleceń i kontroli oraz warstwy widoku — bo i w praktyce są one najczęściej scalane ze sobą.

Wzorzec Front Controller Wzorzec ten stoi w zupełnej opozycji do tradycyjnych aplikacji języka PHP charakteryzujących się wieloma punktami wejścia. Wzorzec Front Controller (kontroler fasady) zakłada wyodrębnienie pojedynczego punktu dostępu dla wszystkich napływających żądań, ostatecznie delegując je do widoków celem zaprezentowania wyników użytkownikowi. W społeczności programistów aplikacji korporacyjnych w języku Java to wzorzec

244

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

bardzo dobrze znany i podstawowy. Został w szczegółach opisany w książce Core J2EE Patterns5, która jest po dziś dzień najważniejszym katalogiem wzorców korporacyjnych dla programistów Javy. Wzorzec kontrolera fasady nie jest szczególnie lubiany wśród programistów PHP, a to z racji narzutów, które wprowadza niekiedy jego inicjalizacja. Większość systemów, które tworzyłem, w ten czy inny sposób ciążyło ku wzorcowi kontrolera fasady — nie zawsze wdrażałem w nich ten wzorzec od początku, zawsze jednak miałem świadomość czynności niezbędnych do włączenia do systemu implementacji wzorca, często też okazywało się, że ich wykonanie owocowało elastycznością systemu.

Problem Tam, gdzie żądania są obsługiwane w wielu rozproszonych miejscach systemu, nietrudno o powielanie kodu. Zawsze trzeba bowiem uwierzytelnić użytkownika czy choćby po prostu odwołać się do wspólnych danych niezbędnych do realizacji żądania. Jeśli obsługa żądania wymaga wykonania powtarzających się czynności w wielu widokach, łatwo przyłapać się na procederze kopiowania i wklejania kodu. Bardzo utrudnia to późniejszą modyfikację kodu, bo najmniejszą poprawkę trzeba powtórzyć w wielu miejscach systemu. Łatwo wtedy o rozsynchronizowanie kodu. Oczywiście w takim układzie należałoby w pierwszym rzędzie spróbować scentralizować wspólne operacje w kodzie bibliotecznym, ale wciąż zostanie nam problem powielania w systemie wywołań funkcji i metod bibliotecznych. Kolejną trudnością, która może pojawić się w systemach zakładających rozproszenie kontroli między wieloma widokami, są problemy zarządzania progresją pomiędzy widokami. W złożonym systemie wysłanie danych z jednego widoku może prowadzić do szeregu rozmaitych stron wyników, zależnie od rodzaju przekazanych danych i efektów ich przetwarzania w warstwie logiki biznesowej. Przekazywanie ich pomiędzy widokami wprowadza bałagan, zwłaszcza jeśli jeden widok występuje w wielu ścieżkach przepływu sterowania.

Implementacja Wzorzec kontrolera fasady zasadniczo definiuje jeden punkt wejścia, wspólny dla wszystkich żądań. Zakłada on wstępne przetwarzanie żądania i wybieranie na jego podstawie operacji do wykonania. Operacje są często definiowane w postaci specjalizowanych obiektów poleceń, zgodnie ze wzorcem Command. Szkic implementacji wzorca kontrolera fasady prezentuje rysunek 12.4.

Rysunek 12.4. Klasa kontrolera i hierarchia klas poleceń Całość można by dodatkowo uzupełnić kilkoma klasami pomocniczymi, zacznijmy jednak od sedna. Oto prosta klasa Controller: namespace woo\controller; //... class Controller { private $applicationHelper; private function __construct(); 5

Wydanie polskie: J2EE. Wzorce projektowe. Wydanie 2, Helion, 2004 — przyp. tłum.

245

PHP. OBIEKTY, WZORCE, NARZĘDZIA

static function run() { $instance = new Controller (); $instance->init(); $instance->handleRequest(); } function init() { $applicationHelper = ApplicationHelper::instance(); $applicationHelper->init(); } function handleRequest() { $request = \woo\base\ApplicationRegistry::getRequest(); $cmd_r = new woo\command\CommandResolver(); $cmd = $cmd_r->getCommand($request); $cmd->execute($request); } }

Mimo prostoty i pominięcia w całości problematyki obsługi błędów jest to bardzo reprezentatywna implementacja klasy Controller. Zadaniem kontrolera jest bowiem usadowienie się „na czubku” systemu i wywoływanie innych klas. I większość pracy czeka nas właśnie w implementacji tych ostatnich. Metoda run() to jedynie dodatek mający ułatwić wywołania init() i handleRequest(). Jest to metoda statyczna, a konstruktor klasy jest prywatny, więc użytkownik klasy nie ma wielkiego wyboru — to jego jedyna możliwość skorzystania z systemu. Zwykle jej wywołanie umieszczam w pliku o nazwie index.php zawierającym dosłownie kilka wierszy: require("woo/controller/Controller.php"); woo\controller\Controller::run();

Rozróżnienie pomiędzy metodami init() a handleRequest() jest w PHP cokolwiek dziwaczne. W innych językach init() byłaby uruchamiana tylko raz, przy starcie aplikacji, a dla poszczególnych żądań wywoływano by jedynie handleRequest(). Warto więc i w PHP zachować taki podział, choć w tym języku każdemu żądaniu towarzyszy inicjalizacja. Metoda init() pozyskuje egzemplarz klasy o nazwie ApplicationHelper. Klasa ta zarządza danymi konfiguracyjnymi dla aplikacji jako całości. We wnętrzu init() następuje wywołanie metody klasy ApplicationHelper, również o nazwie init(), które inicjalizuje dane wykorzystywane w aplikacji. Metoda handleRequest() korzysta z kolei z obiektu klasy CommandResolver, pozyskując z niego stosowny obiekt polecenia (obiekt z hierarchii Command); potem następuje już wykonanie polecenia przez wywołanie metody execute() obiektu polecenia.

ApplicationHelper Klasa ApplicationHelper nie stanowi integralnej części wzorca kontrolera fasady, ale w większości implementacji trzeba przecież zapewnić jakiś sposób pozyskiwania podstawowych danych konfiguracyjnych, warto więc przyjrzeć się strategiom z tej dziedziny. Oto prosta klasa ApplicationHelper: namespace woo\controller; //... class ApplicationHelper { private static $instance = null; private $config = "/data/woo_options.xml"; private function __construct() {} static function instance() { if (is_null(self::$instance)) { self::$instance = new self(); }

246

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

return self::$instance; } function init() { $dsn = \woo\base\ApplicationRegistry::getDSN(); if (! is_null($dsn)) { return; } return $this->getOptions(); } private function getOptions() { $this->ensure(file_exists($this->config), "Nie można znaleźć pliku opcji"); $options = SimpleXml_load_file($this->config); $dsn = (string)$options->dsn; $this->ensure($dsn, "Brak DSN"); \woo\base\ApplicationRegistry::setDSN($dsn); } private function ensure($expr, $message) { if (!$expr) { throw new \woo\base\AppException($message); } } }

Klasa ta zajmuje się po prostu odczytywaniem pliku konfiguracji i udostępnianiem zapisanych w nim parametrów swoim użytkownikom. Jak widać, to kolejny singleton, sprawdzony sposób udostępniania obiektów klas w systemie. Równie dobrze można by tę klasę pomocniczą zdefiniować jako klasę standardową, i zadbać o przekazywanie jej obiektów pomiędzy wszystkimi zainteresowanymi obiektami systemu. Ale wady takiego rozwiązania już znamy nie tylko z tego rozdziału, ale i z rozdziału 9. Fakt zastosowania tutaj klasy ApplicationRegistry sugeruje możliwość refaktoryzacji. Być może należałoby uczynić ApplicationHelper rejestrem, zamiast wprowadzać do systemu dwa singletony o częściowo pokrywających się zadaniach. Wymagałoby to wykonania sugerowanej refaktoryzacji (wydzielenia funkcjonalności charakterystycznej dla ApplicationRegistry od utrwalania i pozyskiwania obiektów dziedziny). Zostawię to zadanie czytelnikom. Więc za wczytywanie danych konfiguracyjnych odpowiedzialna jest metoda init(). Kontroluje ona nawet klasę ApplicationRegistry, sprawdzając, czy ta zbuforowała już dane. Jeśli tak, init() nie robi nic. Sprawdza się to w systemach, w których potrzebna jest wielokrotna kosztowna inicjalizacja. Skomplikowana procedura inicjalizacji jest akceptowalna w językach, w których inicjalizacja aplikacji jest jednorazowa i oddzielona od żądań; w PHP trzeba jednak minimalizować narzut inicjalizacji obciążający kolejne żądania. Buforowanie jest tu przydatne o tyle, że pozwala na wyeliminowanie złożonych i czasochłonnych czynności inicjalizacyjnych, a konkretnie na ograniczenie ich do pierwszego przetwarzanego żądania (najprawdopodobniej prowokowanego w ramach rozruchu systemu) — wszelkie następne żądania mają szansę skorzystania z gotowej konfiguracji. Na razie zysków z buforowania nie było wyraźnie widać, ale w dalszej części rozdziału staną się one bardziej oczywiste. Jeśli mamy do czynienia z pierwszym uruchomieniem żądania (albo jeśli doszło do usunięcia pliku bufora — co jest brutalną, ale efektywną metodą wymuszenia odtworzenia albo uwzględnienia zmiany konfiguracji), w init() wywoływana jest metoda getOptions(). W praktyce przykład wymagałby zapewne jeszcze doszlifowania. Wersja ta ogranicza się bowiem do pozyskania ciągu DSN. Metoda getOptions() sprawdza najpierw, czy plik konfiguracyjny (wskazywany zmienną $config) w ogóle istnieje, a potem, czy da się go rozpracować, wyodrębniając z niego DSN.

247

PHP. OBIEKTY, WZORCE, NARZĘDZIA

 Uwaga W przykładowych klasach ApplicationRegistry i ApplicationHelper ścieżki dostępu do plików są na sztywno zapisane w kodzie. W prawdziwej aplikacji takie elementy konfiguracji byłyby oczywiście również pozyskiwane z rejestru albo innego obiektu konfiguracji. Właściwe ścieżki dostępu do plików mogą być też ustawiane w czasie instalacji aplikacji za pomocą narzędzi takich jak PEAR czy Phing (patrz rozdziały 15. i 19.).

Zauważmy, że do zgłaszania ewentualnych wyjątków klasa stosuje pewną sztuczkę. Zamiast bowiem szpikować kod instrukcjami warunkowymi i instrukcjami zgłaszającymi wyjątki, jak tutaj: if (! file_exists($this->config)) { throw new \woo\base\AppException( "Nie można znaleźć pliku opcji"); }

klasa centralizuje wyrażenie warunkowe i zgłoszenie wyjątku, przenosząc je do metody o nazwie ensure(). Sprawdza ona prawdziwość warunku, a jeśli nie jest on spełniony, zgłasza wyjątek. Całość da się wywołać w jednym wierszu kodu: $this->ensure(file_exists($this->config), "Nie można znaleźć pliku opcji");

Prezentowane tu podejście z buforowaniem godzi najlepsze elementy z dwóch światów. System może zarządzać łatwymi w stosowaniu plikami XML, ale buforowanie umożliwia efektywne odwoływanie się do wartości składowanych w tych plikach. Oczywiście jeśli użytkownicy końcowi będą przy okazji programistami, albo jeśli nie masz zamiaru często zmieniać konfiguracji systemu, możesz włączyć struktury danych PHP wprost do pomocniczej klasy konfiguracji (albo do osobnego pliku włączanego tam, gdzie następuje inicjalizacja obiektów). To nieco ryzykowne, ale na pewno najszybsze. Dla konfiguracji w postaci par klucz-wartość można też zastosować funkcję języka PHP parse_ini_file() do wczytania danych z pliku formatu .INI. Będzie to szybsze niż przetworzenie pliku XML, a dla prostych elementów konfiguracji jest zupełnie wystarczające. Niemniej jednak klasa ApplicationHelper będzie za chwilę operować na znacznie bardziej złożonych zbiorach danych.

CommandResolver Kontroler potrzebuje mechanizmu podejmowania decyzji o sposobie obsługi żądania HTTP, tak aby mógł na tej podstawie wywołać kod odpowiedni do realizacji żądania. Można by łatwo włączyć ów mechanizm do samej klasy Controller, ale osobiście preferuję wyodrębnienie do tego zadania osobnej, specjalistycznej klasy. Znakomicie ułatwia to ewentualną refaktoryzację kodu pod kątem polimorfizmu. Kontroler fasady często odwołuje się do logiki aplikacji za pośrednictwem obiektu polecenia (Command — patrz rozdział 11.). Polecenie wybierane jest zazwyczaj na podstawie wartości parametrów przekazanych w żądaniu, ewentualnie na podstawie struktury URL inicjującego żądanie (można na przykład zaangażować do podejmowania decyzji serwer Apache, aby dla konkretnych URL zwracał klucz będący podstawą wyboru polecenia). W poniższych przykładach rolę tę będzie pełnił pojedynczy parametr: cmd. Wybór polecenia na podstawie parametru można zrealizować na kilka sposobów. Można na przykład porównywać parametr z zawartością pliku konfiguracyjnego albo struktury danych (strategia wyboru logicznego). Można też jednak od razu na podstawie wartości parametru konstruować nazwę klasy do wywołania (albo nazwę pliku definiującego klasę — wtedy mamy wybór niejako fizyczny). Strategia wyboru logicznego jest bardziej elastyczna, ale i bardziej pracochłonna, zarówno w kontekście nakładów konfiguracyjnych, jak i konserwacyjnych. Przykład takiego podejścia prezentowany jest w podrozdziale „Wzorzec Application Controller”. Przykład wytwórni poleceń bazującej na strategii wyboru fizycznego prezentowany był w poprzednim rozdziale. Oto wariacja na ten temat, w celu zachowania bezpieczeństwa uzupełniona o wywołania interfejsu Reflection API. namespace woo\command; //... class CommandResolver { private static $base_cmd = null;

248

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

private static $default_cmd = null; function __construct() { if (is_null(self::$base_cmd)) { self::$base_cmd = \ReflectionClass("\woo\command\Command"); self::$default_cmd = new DefaultCommand(); } } function getCommand(woo\controller\Request $request) { $cmd = $request->getProperty('cmd'); $sep = DIRECTORY_SEPARATOR; if (!$cmd) { return self::$default_cmd; } $cmd=str_replace(array('.', $sep), "", $cmd); $filepath = "woo{$sep}command{$sep}{$cmd}.php"; $classname = "woo\\command\\{$cmd}"; if (file_exists($filepath)) { @require_once($filepath); if (class_exists($classname)) { $cmd_class = new ReflectionClass($classname); if ($cmd_class->isSubClassOf(self::$base_cmd)) { return $cmd_class->newInstance(); } else { $request->addFeedback("polecenie '$cmd' nie wchodzi w skład hierarchii Command"); } } } $request->addFeedback("nie znaleziono polecenia '$cmd'"); return clone self::$default_cmd; } }

Ta prosta klasa analizuje parametr żądania o nazwie cmd. Jeśli wchodzi on w skład żądania i odpowiada plikowi definicji klasy w katalogu poleceń oraz ów plik zawiera definicję klasy odpowiedniego rodzaju, metoda getCommand() tworzy i zwraca obiekt stosownej klasy. Jeśli którykolwiek z tych warunków nie zostanie spełniony, metoda getCommand() elegancko wychodzi z kłopotu, zwracając obiekt polecenia domyślnego. Można się zastanawiać, dlaczego ten kod polega tak ufnie na tym, że klasa hierarchii Command, którą odnajdzie, nie będzie wymagać przy konstrukcji żadnego parametru: if ($cmd_class->isSubClassOf(self::$base_cmd)) { return $cmd_class->newInstance(); }

Odpowiedź tkwi w sygnaturze samej klasy Command: namespace woo\command; //... abstract class Command { final function __construct(); function execute(woo\controller\Request $request) { $this->doExecute($request); } abstract function doExecute(woo\controller\Request $request); }

249

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Otóż deklarując konstruktor jako metodę finalną, uniemożliwiliśmy jej przesłanianie w klasach pochodnych. Nie może więc istnieć klasa dziedzicząca po Command — taka, która wymagałaby dodatkowych argumentów konstruktora. Pamiętajmy, że nie wolno przyjmować wypełnianych przez użytkowników formularzy żądania HTTP bez kontroli ich zawartości. W prezentowanym wyżej przykładzie uwzględniłem więc stosowny test mający zagwarantować, że ciąg cmd nie będzie zawierał ścieżki dostępu, aby nie dało się sprytnym spreparowaniem parametru żądania wyjść poza katalog poleceń (i odwołać się na przykład do pliku ../../../tmp/BadCommand.php). Kod trzeba by zabezpieczyć jeszcze mocniej, akceptując na przykład jedynie takie ciągi poleceń, które odpowiadają wartościom zdefiniowanym w pliku konfiguracyjnym. Przy tworzeniu klas poleceń należy starannie izolować je od logiki aplikacji. Gdy tylko zaczną one wykonywać operacje specyficzne dla logiki aplikacji, zamienią się w pokręcone skrypty transakcyjne i szybko doprowadzą do powielania kodu. Polecenia powinny być rodzajem stacji przekaźnikowych: interpretują żądanie, realizują je wywołaniami jakichś obiektów dziedzinowych, a następnie oddają dane do warstwy prezentacji. Jak tylko zaczną robić cokolwiek więcej, trzeba będzie poważnie myśleć o refaktoryzacji kodu. Przy czym ewentualna refaktoryzacja byłaby stosunkowo prosta: nie jest trudno namierzyć polecenia wykonujące nadmierną liczbę operacji, a rozwiązanie jest zwykle oczywiste: funkcjonalność trzeba przenieść do klasy dziedzinowej albo do fasady.

Żądanie Żądania są w magiczny sposób obsługiwane w PHP i elegancko podawane do wnętrza systemu (zajmuje się tym serwer HTTP) za pośrednictwem tablic globalnych. Łatwo tymczasem zauważyć, że my wciąż reprezentujemy żądanie specjalną klasą. Do obiektu CommandResolver, a potem do właściwego polecenia (obiektu z hierarchii Command) przekazywany jest bowiem obiekt klasy Request reprezentujący żądanie. Dlaczego by nie zezwolić tym klasom na odwoływanie się wprost do zmiennych tablicowych $_REQUEST, $_POST czy $_GET? Można by tak postąpić, ale centralizacja operacji na żądaniach daje znacznie więcej możliwości. Można na przykład poddawać przychodzące żądania działaniu rozmaitych filtrów. Albo jak w następnym przykładzie — pozyskiwać parametry żądania spoza samego żądania HTTP, pozwalając na uruchamianie aplikacji z poziomu wiersza polecenia czy z poziomu skryptu testowego. Dalej, jeśli w aplikacji wykorzystywane są sesje, trzeba na potrzeby wywołań inicjowanych z wiersza polecenia opracować osobny mechanizm podtrzymywania sesji. Sprawdziłby się w tym zadaniu wzorzec Registry, umożliwiając generowanie różnych klas rejestrów, w zależności od bieżącego trybu komunikowania się z aplikacją (HTTP albo wiersz poleceń). Reprezentacja żądania w obiekcie klasy Request przydaje się też jako repozytorium danych, które trzeba przekazywać do warstwy widoku. W tym kontekście Request może być odpowiedzialny za żądania całościowo, również w zakresie odpowiedzi na żądania. Oto przykładowa klasa żądania Request: namespace woo\controller; //... class Request { private $properties; private $feedback = array(); function __construct() { $this->init(); } function init() { if (isset( $_SERVER['REQUEST_METHOD'])) { $this->properties = $_REQUEST; return; } foreach($_SERVER['argv'] as $arg) { if (strpos($arg, '=')) { list($key, $val)=explode("=", $arg);

250

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

$this->setProperty($key, $val); } } } function getProperty($key) { if (isset($this->properties[$key])) { return $this->properties[$key]; } return null; } function setProperty($key, $val) { $this->properties[$key] = $val; } function addFeedback($msg) { array_push($this->feedback, $msg); } function getFeedback() { return $this->feedback; } function getFeedbackStrong($separator="\n") { return implode($separator, $this->feedback); } }

Jak widać, większość kodu klasy to implementacja mechanizmu ustawiania i pozyskiwania właściwości ($properties) żądania. Za wypełnienie prywatnej składowej $properties odpowiedzialna jest metoda init(). Zauważ, że klasa równie dobrze obsługuje parametry żądania HTTP, jak i wywołania z wiersza polecenia. Daje to nieocenione korzyści przy testowaniu i diagnostyce systemu. Po pozyskaniu obiektu żądania można za jego pośrednictwem odwoływać się do poszczególnych parametrów żądania HTTP; pośredniczy w tym metoda getProperty() obiektu przyjmująca ciąg określający nazwę parametru, a zwracająca jego wartość (wyciąganą z tablicy $properties). Można też za pośrednictwem metody setProperty() uzupełniać żądanie własnymi danymi. Klasa zarządza również tablicą $feedback. Jest ona prostym kanałem, za pośrednictwem którego klasy kontrolera mogą przekazywać komunikaty użytkownikom systemu.

Polecenia Znamy już klasę bazową hierarchii Command, znamy też dokładnie — z rozdziału 11. — wzorzec Command, więc nie ma potrzeby bardzo szczegółowego omawiania poleceń. Wystartujemy z prostym, konkretnym obiektem polecenia: namespace woo\command; //... class DefaultCommand extends Command { function doExecute(\woo\controller\Request $request) { $request->addFeedback("Witamy w WOO"); include("woo/view/main.php"); } }

Tak właśnie może wyglądać obiekt polecenia udostępniany przez wytwórnię CommandResolver, kiedy w żądaniu nie zostanie wyszczególnione żadne konkretne polecenie.

251

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Jak łatwo zauważyć, samą metodę execute() implementuje abstrakcyjna klasa bazowa hierarchii Command. Implementacja ta polega na wywołaniu metody doExecute() przesłanianej w klasach pochodnych. Dzięki temu mamy możliwość poprzedzania poleceń wspólnymi dla wszystkich poleceń operacjami wstępnymi i kończenia ich podobnie uniwersalnymi operacjami porządkowymi — wystarczy uzupełnić o te operacje implementację metody execute() w klasie bazowej. Metoda execute() przekazuje do obiektu klasy pochodnej obiekt klasy Request, udostępniając tym samym dane wprowadzone przez użytkownika i metodę addFeedback(). Obiekt polecenia domyślnego (DefaultCommand) czyni z niej użytek, przekazując do użytkownika komunikat powitalny. Ostatecznie polecenie przekazuje sterowanie do stosownego widoku, co polega na prostym wywołaniu include(). Sztywne osadzenie odwzorowania polecenia do widoku w klasie hierarchii Command to najprostszy mechanizm wyboru widoku, ale w mniejszych systemach zupełnie wystarczający. Elastyczniejsza strategia wyboru widoku prezentowana będzie w podrozdziale „Wzorzec Application Controller”. Plik main.php zawiera trochę kodu HTML oraz odwołanie do obiektu Request w celu sprawdzenia obecności danych zwrotnych (widokami zajmiemy się wkrótce). Teraz dysponujemy wszystkimi komponentami potrzebnymi do uruchomienia systemu. Efektem będzie:

Woo! Tu Woo!

Witamy w WOO


Jak widać, komunikat zwrotny ustawiony przez domyślne polecenie przedostał się na wyjście aplikacji. Prześledźmy całość procesu, który doprowadził do takiego przebiegu wykonania aplikacji.

Podsumowanie Możliwe, że szczegóły implementacji klas prezentowane w tym podrozdziale przesłoniły faktyczną prostotę i elegancję wzorca kontrolera fasady. Rysunek 12.5 prezentuje więc diagram sekwencji ilustrujący czas życia żądania według założeń wzorca.

Rysunek 12.5. Wzorzec kontrolera fasady w akcji

252

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

Jak widać, kontroler fasady deleguje inicjalizację do obiektu pomocniczego klasy ApplicationHelper (który celem wyeliminowania każdorazowego narzutu inicjalizacji buforuje dane konfiguracyjne). Następnie obiekt klasy Controller pozyskuje obiekt polecenia z wytwórni CommandResolver. Na koniec następuje wywołanie Command::execute() w celu uruchomienia właściwej logiki aplikacji. W tej implementacji wzorca polecenie jest samo odpowiedzialne za delegację do odpowiedniego widoku. Można oczywiście zaproponować rozwiązanie bardziej elastyczne — poznamy je w następnym podrozdziale.

Konsekwencje Wzorzec Front Controller nie jest wzorcem najprostszym w stosowaniu. Zanim uwidocznią się pierwsze korzyści wynikające z jego wdrożenia, trzeba mocno popracować. To poważna wada, jeśli projekt wymaga szybkiego wykonania albo jest na tyle prosty, że kontroler fasady byłby gatunkowo cięższy od całej reszty systemu. Należy jednak podkreślić, że gdy już raz wdrożysz kontroler fasady w projekcie, szybko się przekonasz, że jego przeniesienie do innych projektów jest zaskakująco proste. Większość jego funkcjonalności można wyodrębnić do kodu bibliotecznego, konstruując łatwy do stosowania szkielet tego fragmentu aplikacji. Pewną wadą jest niewątpliwie wymaganie, aby wszystkie informacje konfiguracyjne były wczytywane dla każdego żądania. Wszystkie metody mają z tego tytułu pewne kłopoty, jednak kontroler fasady wymaga jeszcze informacji dodatkowych, na przykład odwzorowania żądań do poleceń, a tych z kolei do widoków. Ten narzut może być efektywnie zniesiony przez buforowanie potrzebnych danych. Najbardziej efektywnym sposobem buforowania jest dodanie danych do systemu w postaci struktur macierzystych dla języka PHP. Metoda ta sprawdza się, kiedy jesteśmy jedynymi opiekunami systemu, ale jeśli mają z niego korzystać użytkownicy pozbawieni doświadczenia technicznego, trzeba będzie wyodrębnić stosowne pliki konfiguracyjne. Macierzyste podejście języka PHP też nadaje się do automatyzacji, trzeba jedynie utworzyć system, który wczytuje plik konfiguracyjny i na jego podstawie buduje struktury danych PHP, zapisywane następne do bufora. Po utworzeniu bufora system będzie z niego korzystał priorytetowo, odwołując się do pliku konfiguracji jedynie w przypadku zmiany jego zawartości i wynikającej z tego konieczności odświeżenia bufora. Mniej efektywne, ale za to prostsze podejście zaprezentowałem przy okazji omawiania klasy ApplicationRegistry — mowa o prostej serializacji danych. Po stronie zalet mamy centralizację logiki prezentacji danego systemu. Oznacza to, że możesz w jednym miejscu (a przynajmniej w jednym zestawie klas) scentralizować kontrolę nad sposobem przetwarzania przyjmowanych żądań i wyborem widoków prezentujących efekty tego przetwarzania. Eliminuje to niepotrzebne powielanie kodu i zmniejsza prawdopodobieństwo występowania błędów. Kontroler fasady jest też wzorcem mocno rozszerzalnym. Po zaimplementowaniu i uruchomieniu jego rdzenia można w prosty i szybki sposób uzupełniać system o nowe polecenia i widoki. W tym przykładzie polecenia miały własny sposób doboru widoków prezentacji wyników. Jeśli masz zamiar stosować wzorzec kontrolera fasady z dodatkowym obiektem ułatwiającym wybór widoku (a może i polecenia), wzorzec ujawni się jako skuteczny kontroler nawigacji, którą trudno elegancko zarządzać, kiedy warstwa prezentacji jest rozproszona po systemie. Obiektem takim zajmiemy się w następnym podrozdziale.

Wzorzec Application Controller Umożliwienie poleceniom samodzielnego wybierania widoków jest akceptowalne jedynie w mniejszych systemach. W większych najlepiej byłoby rozluźnić i to sprzęgnięcie, izolując polecenia od widoków tak skutecznie, jak to możliwe. Odpowiedzialność za odwzorowywanie żądań do poleceń, a tych z kolei do widoków, może przejąć wzorzec Application Controller, czyli kontroler aplikacji. Tego rodzaju rozprzężenie oznaczałoby znaczną łatwość przełączania się pomiędzy alternatywnymi zestawami widoków bez konieczności ingerowania w bazę kodu implementacji poleceń. Właściciel systemu mógłby również w prosty sposób zmieniać przepływ sterowania w aplikacji, znów bez potrzeby ingerowania we wnętrzności systemu. Przez wdrożenie logicznego wyboru poleceń wzorzec ten ułatwia również wykorzystywanie samych poleceń w różnych kontekstach w obrębie systemu.

253

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Problem Przypomnijmy naturę naszego problemu. Administrator musi mieć możliwość dodawania do systemu nowych placówek i definiowania w ich ramach konkretnych lokalizacji. System może więc obsługiwać polecenia, np. AddVenue i AddSpace. Wedle dotychczasowych przykładów polecenia te wybierane byłyby za pośrednictwem bezpośredniego, niemal fizycznego odwzorowania parametru żądania (cmd=AddVenue) do klasy polecenia (AddVenue). Ogólnie mówiąc, pomyślne wywołanie polecenia AddVenue powinno prowadzić system do początkowego wywołania polecenia AddSpace. Powiązanie to można by wyrazić w kodzie samych klas, gdyby w klasie AddVenue jawnie wywoływać AddSpace. W dalszej perspektywie polecenie AddSpace powinno obejmować widok zawierający formularz umożliwiający dodanie do placówki nowej lokalizacji. Oba polecenia można by powiązać przynajmniej z dwoma różnymi widokami: głównym widokiem wprowadzania danych i widokiem „błędu” bądź potwierdzenia wykonania polecenia. Wedle omówionej właśnie logiki klasy hierarchii Command powinny obejmować te widoki (stosując odpowiednie testy celem wybrania, który z tych widoków ma zostać w danych okolicznościach zaprezentowany). Wszystko to, zakodowane „na sztywno”, może się sprawdzać, o ile polecenia będą stosowane zawsze w ten sam sposób. Całość zacznie się jednak łamać, jeśli zechcemy uwzględnić specjalny widok dla polecenia AddVenue, dla pewnych szczególnych sytuacji oraz gdybyśmy chcieli zmienić logiczne powiązanie pomiędzy poleceniami (na przykład uzupełniając przepływ sterowania o dodatkowy widok pomiędzy pomyślnym wprowadzeniem do systemu placówki a rozpoczęciem wprowadzania danych o szczegółowej lokalizacji imprezy). Gdyby każde z poleceń było wykorzystywane tylko jednokrotnie, w jednym układzie wobec pozostałych poleceń i z jednym widokiem, można by śmiało powiązania te zakodować „sztywno”. We wszystkich innych przypadkach zachęcam do dalszej lektury. Kontrolę nad owymi powiązaniami może przejąć kontroler aplikacji, uwalniając klasy poleceń od konieczności wyboru widoków i tworzenia przepływów sterowania —polecenia mogą wtedy dotyczyć jedynie podstawowych zadań, którymi są: przetworzenie danych wejściowych, wywołanie logiki aplikacji i obsługa ewentualnych wyników.

Implementacja Jak zawsze klucz do wzorca tkwi w interfejsie. Kontroler aplikacji to klasa (albo zestaw klas), do której odwołuje się kontroler fasady celem pozyskiwania poleceń na bazie analizy żądań użytkowników oraz celem dopasowania odpowiedniego widoku prezentującego wyniki wykonanego polecenia. Powiązanie pomiędzy oboma kontrolerami widać wyraźnie na rysunku 12.6.

Rysunek 12.6. Application Controller (Kontroler aplikacji) Nie inaczej niż w przypadku pozostałych wzorców z tego rozdziału naszym celem jest uczynienie rzeczy maksymalnie prostymi dla kodu, który będzie je użytkował — stąd klasa kontrolera fasady. Za interfejsem trzeba jednak wdrożyć jakąś implementację. Proponowane tu podejście to tylko jeden z możliwych sposobów. W miarę brnięcia przez kolejne punkty pamiętaj, że esencja prezentowanych wzorców tkwi w sposobie, w jaki współpracują uczestnicy wzorca: sam kontroler aplikacji, polecenia i widoki — szczegóły implementacji są dla nas mniej interesujące. Zacznijmy od kodu, który miałby użytkować klasę (klasy) kontrolera aplikacji. 254

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

Front Controller Poniżej prezentowany jest sposób, w jaki klasa kontrolera fasady FrontController mogłaby odwoływać się do klasy AppController (klasy kontrolera aplikacji) — przy pewnym uproszczeniu i z pominięciem obsługi błędów: function handleRequest() { $request = \woo\base\ApplicationRegistry::getRequest(); $app_c = \woo\base\ApplicationRegistry::appController(); while($cmd = $app_c->getCommand($request)) { $cmd->execute($request); } $this->invokeView($app_c->getView($request)); } function invokeView($target) { include("woo/view/$target.php"); }

Jak widać, zasadniczą różnicą pomiędzy tym kodem a przykładem z wzorca kontrolera fasady jest to, że tutaj obiekty polecenia są pozyskiwane i uruchamiane w pętli. Kod odwołuje się też do obiektu klasy AppController celem pozyskania nazwy widoku, który ma zostać włączony do obsługi żądania. Zauważmy, że do pozyskania kontrolera AppController i obiektu Request użyliśmy obiektu rejestru. Jak więc przejść od polecenia kodowanego parametrem cmd do łańcucha poleceń z wybieranym dla niego widokiem?

Implementacja — przegląd Klasa Command może w różnych fazach działania żądać odmiennych widoków. Domyślny widok polecenia AddVenue byłby zapewne formularzem wprowadzania danych o placówce. Gdyby jednak użytkownik wprowadził niepoprawne dane, obsługa polecenia mogłaby polegać albo na ponowieniu wyświetlenia formularza, albo na wyświetleniu strony z komunikatem o błędzie. Gdyby zaś wszystko poszło dobrze, a system zostałby uzupełniony o kolejną placówkę, trzeba by było przejść do kolejnego polecenia w łańcuchu — w tym przypadku zapewne polecenia wprowadzania szczegółowej lokalizacji imprezy — AddSpace. Obiekty klas hierarchii Command prezentują systemowi swój bieżący stan za pośrednictwem znacznika stanu. Oto znaczniki rozpoznawane w takiej minimalnej implementacji (tu podczas ich ustawiania w klasie nadrzędnej Command): private static $STATUS_STRINGS = array ( 'CMD_DEFAULT' => 0, 'CMD_OK' => 1, 'CMD_ERROR' => 2, 'CMD_INSUFFICIENT_DATA' => 3 );

Kontroler aplikacji wyszukuje instancje właściwych klas hierarchii Command na podstawie obiektu żądania (obiektu klasy Request). Po uruchomieniu polecenia jest ono kojarzone ze stanem. Ta kombinacja stanu i polecenia może być porównywana ze strukturą danych określającą polecenie, które powinno zostać dla danego układu wykonane w następnej kolejności, a jeśli łańcuch zostanie wyczerpany, określającą, który widok wybrać do prezentacji.

Plik konfiguracyjny Właściciel systemu może określać interakcje pomiędzy poleceniami a widokami za pośrednictwem zestawu dyrektyw pliku konfiguracyjnego. Oto one:

main main error

255

PHP. OBIEKTY, WZORCE, NARZĘDZIA

listvenues

quickadd

addvenue

AddSpace

addspace

ListVenues

...

Powyższy uproszczony fragment XML-owego pliku konfiguracyjnego prezentuje jedną ze strategii wyodrębniania przepływu sterowania pomiędzy poleceniami i powiązań klas Command uczestniczących w łańcuchu poleceń z poszczególnymi widokami. Całość należy czytać od zewnątrz w głąb — elementy zewnętrzne to te najbardziej ogólne. Można je nadpisywać przez ich odpowiedniki wewnątrz elementów command. Pierwszy z elementów, view, definiuje więc domyślny widok dla wszystkich poleceń, obowiązujący zawsze, jeśli w elemencie polecenia nie zostanie zagnieżdżony inny element view. Pozostałe elementy view na tym samym poziomie deklarują atrybuty stanu odpowiadające znacznikom stanu ustawianym w klasach poleceń. Każdy stan reprezentuje znacznik, który może zostać ustawiony w obiekcie klasy Command celem sygnalizowania postępu obsługi żądania. Ponieważ te elementy uszczegóławiają pierwszy element view, mają przed nim pierwszeństwo. Jeśli polecenie ustawi znacznik CMD_OK, wtedy do roli widoku wybierany będzie ten o nazwie menu, chyba że wybór ten zostanie przesłonięty jeszcze bardziej zagnieżdżonym elementem view. Po ustawieniu widoków domyślnych dokument wymienia elementy command. Domyślnie elementy te odwzorowują tutaj konkretne klasy hierarchii Command (i przy okazji pliki definicji klas w systemie plików) wedle projektu z przykładu z wytwórnią CommandResolver. Jeśli więc parametr cmd żądania będzie miał wartość AddVenue, klasa polecenia będzie szukana w pliku AddVenue.php. System pozwala na definiowanie aliasów poleceń. Można więc ustawić cmd na QuickAddVenue, co skieruje nas do następującego elementu:

quickadd

Mamy tu element QuickAddVenue, który nie jest kojarzony z plikiem definicji osobnej klasy. Skojarzenia z właściwą klasą dokonuje się za pośrednictwem elementu classroot. Dzięki temu można odwoływać się do klasy AddVenue w kontekście różnych przepływów sterowania i różnych widoków — bo dla aliasów można ustawiać widoki niezależne. Elementy command, jako zagnieżdżone i uszczegóławiające konfigurację, przesłaniają ustawienia domyślne. Można więc w obrębie elementu command ustawić element view, przypisując tym samym do polecenia widok, niekoniecznie taki jak domyślny.

256

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

addvenue

AddSpace

Tutaj z poleceniem AddVenue skojarzony został widok addvenue. Oznacza to, że kiedy wywołane zostanie polecenie AddVenue, włączany będzie każdorazowo plik addvenue.php. Każdorazowo, ale pod warunkiem dopasowania stanu polecenia. Jeśli więc klasa AddVenue ustawi znacznik CMD_OK, element ten przesłoni domyślny widok dla poleceń jako takich. Element status mógłby po prostu zawierać zagnieżdżony element widoku, który byłby dobierany dla danego stanu tego konkretnego polecenia. Tu mamy jednak zagnieżdżony inny element: forward, który definiuje przejście do następnego polecenia w łańcuchu. Odpowiedzialność za dobór widoku jest tym samym delegowana do elementu opisującego następne polecenie łańcucha.

Przetwarzanie pliku konfiguracji Zaproponowaliśmy dość skuteczny model kontrolowania widoków i przepływu sterowania pomiędzy poleceniami. Jednak nie mamy chyba zamiaru przetwarzać pliku konfiguracji dla każdego żądania z osobna. Rozwiązanie problemu narzutów obciążających żądania już znamy — pomoże nam klasa ApplicationHelper udostępniająca mechanizm buforowania danych konfiguracyjnych. Oto zarys pomysłu: private function getOptions() { $this->ensure(file_exists($this->config), "Nie można znaleźć pliku opcji"); $options = \simplexml_load_file($this->config); // ustawienie DSN… $map = new ControllerMap(); foreach($options->control->view as $default_view) { $stat_str = trim($default_view['status']); $status = \woo\command\Command::statuses($stat_str); $map->addView((string)$default_view, 'default', $status); } // …reszta kodu analizy konfiguracji… \woo\base\ApplicationRegistry::setControllerMap($map); }

Przetwarzanie pliku XML, nawet za pomocą znakomitego pakietu SimpleXML, to zadanie żmudne, ale niespecjalnie wymagające, więc większość szczegółów tego procesu zdecydowałem się pominąć. Sęk w tym, żeby zauważyć, że metoda getOptions() wywoływana jest jedynie wtedy, kiedy konfiguracja nie była jeszcze buforowana w obiekcie rejestru aplikacji (obiekcie klasy ApplicationRegistry).

Utrwalanie danych konfiguracyjnych Buforowanym obiektem ma być w tym przypadku ControllerMap, który zasadniczo obejmuje trzy tablice. Można by oczywiście zastosować „gołe” tablice bez otoczki obiektowej, ale zastosowanie specjalnej klasy daje cenną gwarancję zachowania konkretnego formatu tablic. Oto klasa ControllerMap: namespace woo\controller; //... class ControllerMap { private $viewMap = array(); private $forwardMap = array(); private $classrootMap = array(); function addClassroot($command, $classroot) {

257

PHP. OBIEKTY, WZORCE, NARZĘDZIA

$this->classrootMap[$command] = $classroot; } function getClassroot($command) { if (isset($this->classrootMap[$command])) { return $this->classrootMap[$command]; } return $command; } function addView($view, $command = 'default', $status = 0) { $this->viewMap[$command][$status] = $view; } function getView($command, $status) { if (isset($this->viewMap[$command][$status])) { return $this->viewMap[$command][$status]; } return null; } function addForward($command, $status = 0, $newCommand) { $this->forwardMap[$command][$status] = $newCommand; } function getForward($command, $status) { if (isset($this->forwardMap[$command][$status])) { return $this->forwardMap[$command][$status]; } return null; } }

Składowa $classroot to prosta tablica asocjacyjna odwzorowująca uchwyty poleceń (tzn. nazwy elementów command z pliku konfiguracji) na nazwy klas z hierarchii Command (czyli np. AddVenue). Tablica ta służy do wyszukiwania klas docelowych dla aliasów definiowanych w pliku konfiguracji. Jest ona wypełniana w czasie przetwarzania pliku konfiguracyjnego wywołaniami metody addClassroot(). Tablice $forwardMap i $viewMap są tablicami dwuwymiarowymi, w których wyszukuje się wartości na podstawie znanego polecenia i statusu. Wróćmy do fragmentu pliku konfiguracji:

addvenue

AddSpace

Oto wywołanie, które powinno pojawić się w kodzie analizującym plik konfiguracyjny celem dodania odpowiedniego elementu do składowej $viewMap: $map->addView('addvenue', 'AddVenue', 0);

A to wywołanie wypełniające tablicę składowej $forwardMap: $map->addForward('AddVenue', 1, 'AddSpace');

Klasa kontrolera aplikacji odwołuje się do tak zakodowanego odwzorowania w ściśle określonym porządku wyszukiwania. Załóżmy, że polecenie AddVenue zwróciło status CMD_OK (czyli wartość 1 — zero zostało przypisane do CMD_DEFAULT). Kontroler aplikacji będzie przeszukiwać tablicę $forwardMap od najbardziej specyficznej kombinacji

258

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

polecenia i znacznika stanu do kombinacji najbardziej ogólnej (angażującej już wyłącznie wartości domyślne). Wartością zwracaną będzie zaś pierwsza dopasowana kombinacja: $viewMap['AddVenue'][1]; $viewMap['AddVenue'][0]; $viewMap['default'][1]; $viewMap['default'][0];

// AddVenue CMD_OK [dopasowana kombinacja] // AddVenue CMD_DEFAULT // DefaultCommand CMD_OK // DefaultCommand CMD_DEFAULT

Podobne wyszukiwanie realizowane jest w celu dopasowania widoku. Oto kod klasy kontrolera aplikacji: namespace woo\controller; //.. class AppController { private static $base_cmd = null; private static $default_cmd = null; private $controllerMap; private $invoked = array(); function __construct(ControllerMap $map) { $this->controllerMap = $map; if (is_null(self::$base_cmd)) { self::$base_cmd = new \ReflectionClass("\woo\command\Command"); self::$default_cmd = new \woo\command\DefaultCommand(); } } function reset() { $this->invoked = array(); } function getView(Request $req) { $view = $this->getResource($req, "View"); return $view; } private function getForward(Request $req) { $forward = $this->getResource($req, "Forward"); if ($forward) { $req->setProperty('cmd', $forward); } return $forward; } private function getResource(Request $req, $res) { $cmd_str = $req->getProperty('cmd'); $previous = $req->getLastCommand(); $status = $previous->getStatus(); if (!isset($status) || !is_int($status)) { $status = 0; } $acquire = "get$res"; $resource = $this->controllerMap ->$acquire($cmd_str, $status); if (is_null($resource)) { $resorce = $this->controllerMap ->$acquire($cmd_str, 0); }

259

PHP. OBIEKTY, WZORCE, NARZĘDZIA

}

if (is_null($resource)) { $resorce = $this->controllerMap ->$acquire('default', $status); } if (is_null($resource)) { $resorce = $this->controllerMap ->$acquire('default', 0); } return $resource;

function getCommand(Request $req) { $previous = $req->getLastCommand();

}

}

if (!$previous) { $cmd = $req->getProperty('cmd'); if (is_null($cmd)) { $req->setProperty('cmd', 'default'); return self::$default_cmd; } } else { $cmd = $this->getForward($req); if (is_null($cmd)) { return null; } } $cmd_obj = $this->resolveCommand($cmd); if (is_null($cmd_obj)) { throw new \woo\base\AppException( "nie można znaleźć '$cmd'"); } $cmd_class = get_class($cmd_obj); if (isset($this->invoked[$cmd_class])) { throw new \woo\base\AppException( "pętla w łańcuchu"); } $this->invoked[$cmd_class]=1; return $cmd_obj;

function resolveCommand($cmd) { $classroot = $this->controllerMap->getClassroot($cmd); $filepath = "woo/command/$classroot.php"; $classname = "\\woo\\command\\$classroot"; if (file_exists($filepath)) { require_once($filepath); if (class_exists($classname)) { $cmd_class = new \ReflectionClass($classname); if ($cmd_class->isSubClassOf(self::$base_cmd)) { return $cmd_class->newInstance(); } } } return null; }

Metoda getResource() implementuje wyszukiwanie zarówno następnego polecenia w łańcuchu, jak i widoku. Jest ona wywoływana z wnętrza funkcji getView() i getForward(). Warto zwrócić uwagę na sposób wyszukiwania dopasowania — od najbardziej specyficznej kombinacji ciągu polecenia i znacznika stanu po (w ostateczności) kombinację wartości domyślnych.

260

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

Metoda getCommand() odpowiedzialna jest za zwrócenie wszystkich poleceń, które w pliku konfiguracyjnym zostały włączone do wspólnego łańcucha. Działa ona następująco: jeśli bieżące polecenie ma być pierwsze w ramach żądania, nazwę polecenia można odczytać z parametru cmd, a żądanie nie powinno nosić śladów wykonywania wcześniejszych poleceń (rejestr wykonanych poleceń prowadzi obiekt żądania). Jeśli mimo braku śladów poprzednich poleceń żądanie nie ustawia wartości parametru cmd, metoda zwraca obiekt domyślnego polecenia. Pozyskanie obiektu odbywa się za pośrednictwem metody resolveCommand(), do której przekazywany jest ciąg nazwy polecenia: $cmd. Kiedy metoda getCommand() zostanie wywołana po raz drugi (i kolejne) dla tego samego żądania, obiekt żądania będzie przechowywał referencję obiektu ostatnio wykonanego polecenia. Wtedy metoda getCommand() sprawdza, czy dla danej kombinacji polecenia i jego stanu w łańcuchu przewidziane jest następne polecenie; szuka go metoda getForward(). Jeśli ta ostatnia dopasuje następne polecenie, zwróci ciąg, na podstawie którego można pozyskać i zwrócić do kontrolera kolejny obiekt polecenia. W metodzie getCommand() na uwagę zasługuje jeszcze specjalny test zapobiegający zapętleniu łańcucha poleceń. Otóż obiekt utrzymuje tablicę rejestrującą wywołane polecenia, indeksowaną nazwami klas hierarchii Command. Kiedy dodawany element jest już obecny, możemy wnioskować, że to samo polecenie zostało pozyskane poprzednio; w ten sposób pojawia się ryzyko zapętlenia w pętli nieskończonej, co jest bardzo niepożądane — więc po wykryciu takiej sytuacji lepiej rzucić wyjątek. Strategie kontrolera aplikacji odnośnie do pozyskiwania widoków i poleceń mogą być bardzo różne, wszystko sprowadza się do ukrycia tego procesu decyzyjnego przed resztą systemu. Proces, w ramach którego klasa Front Controllera używa klasy kontrolera aplikacji do pozyskania obiektu polecenia, a potem widoku, ilustruje rysunek 12.7.

Rysunek 12.7. Użycie kontrolera aplikacji do pozyskiwania poleceń i widoków

Klasa bazowa hierarchii poleceń — Command Nie sposób nie zauważyć, że działanie klasy AppController opiera się między innymi na istnieniu w klasie żądania śladu poprzednio wykonanego polecenia. Utrwaleniem tego śladu w żądaniu zajmuje się klasa bazowa hierarchii Command: namespace woo\command; //... abstract class Command { private static $STATUS_STRING = array ( 'CMD_DEFAULT' => 0,

261

PHP. OBIEKTY, WZORCE, NARZĘDZIA

'CMD_OK' => 1, 'CMD_ERROR' => 2, 'CMD_INSUFFICIENT_DATA' => 3 ); private $status = 0; final function __construct() {} function execute(\woo\controller\Request $request) { $this->status = $this->doExecute($request); $request->setCommand($this); } function getStatus() { return $this->status; } static function statuses($str = 'CMD_DEFAULT') { if (isset(self::$STATUS_STRINGS[$str])) { return self::$STATUS_STRING[$str]; } throw new \woo\base\Exception("unknown status: $str"); } abstract function doExecute(\woo\controller\Request $request); }

Klasa Command definiuje tablicę ciągów i kodów stanów (mocno obciętą dla potrzeb przykładu). Udostępnia też metodę statuses() konwertującą ciąg opisujący stan (np. "CMD_OK") na postać kodu tego stanu oraz metodę getStatus() zwracającą znacznik bieżącego stanu obiektu klasy Command. Ponieważ zwracanie nieoczekiwanych wyników mogłoby wprowadzać trudne do wykrycia błędy, metoda statuses() w przypadku niepowodzenia zrzuca wyjątek. Dalej, metoda execute() korzysta z wartości zwracanej abstrakcyjnej metody doExecute() celem ustawienia znacznika bieżącego stanu polecenia; zajmuje się też utrwaleniem obiektu polecenia w obiekcie żądania.

Konkretna klasa polecenia Oto jak można by zaimplementować klasę polecenia AddVenue: namespace woo\command; //... class AddVenue extends Command { function doExecute( \woo\controller\Request $request ) { $name = $request->getProperty("venue_name"); if (is_null($name)) { $request->addFeedback("nie podano nazwy"); return self::statuses('CMD_INSUFFICIENT_DATA'); } else { $venue_obj = new \woo\domain\Venue(null, $name); $request->setObject('venue', $venue_obj); $request->addFeedback("dodano '$name' ({$venue_obj->getId()})"); return self::statuses('CMD_OK'); } } }

262

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

Część powyższego kodu nabierze więcej sensu w kontekście dalszego omówienia. Prześledźmy wygląd szkieletu klasy Venue operującej na tym poleceniu: namespace woo\domain; class Venue { private $id; private $name; function __construct($id, $name) { $this->name = $name; $this->id = $id; } function getName() { return $this->name; } function getId() { return $this->id; } }

Wracając do polecenia: najważniejsze na razie jest to, że metoda doExecute() zwraca znacznik stanu wykonania polecenia, który w klasie bazowej jest przepisywany do składowej obiektu. Decyzja o sposobie reagowania na fakt, że wywołany został dany obiekt, i że jego wykonanie zakończyło się takim stanem, jest podejmowana na bazie pliku konfiguracyjnego. Wedle naszego przykładowego pliku XML: jeśli obiekt polecenia AddVenue zwróci CMD_OK, mechanizm łańcucha poleceń spowoduje konkretyzację obiektu następnego polecenia, tym razem klasy AddSpace. Taki łańcuch poleceń jest wyzwalany jedynie wtedy, kiedy pierwotne żądanie ma parametr cmd=AddVenue. Gdyby zaś żądanie miało parametr cmd o wartości QuickAddVenue, łańcuch poleceń nie byłby kontynuowany, a zamiast widoku właściwego dla polecenia AddVenue zaprezentowany zostałby widok quickaddvenue. Zauważmy, że niniejszy przykład nie zawiera kodu potrzebnego do zapisania obiektu Venue w bazie danych — tym zajmiemy się w następnym rozdziale.

Konsekwencje Przygotowanie pełnoprawnego wcielenia wzorca Application Controller będzie żmudne, ponieważ trzeba na początku choćby zdefiniować powiązania pomiędzy poleceniami i żądaniami, poleceniami i widokami czy wreszcie między rozmaitymi poleceniami. Trzeba te powiązania wyrazić i wdrożyć mechanizm ich odczytywania. Z tego względu osobiście staram się implementować coś na ten wzór dopiero wtedy, kiedy aplikacja wyraźnie tego wymaga — dochodzę do takiego wniosku, kiedy widzę, że muszę uzupełniać polecenia o warunku wybierające widoki albo wywołujące kolejne polecenia w zależności od okoliczności. Mniej więcej wtedy mam już bowiem jasność co do tego, że przepływ sterowania pomiędzy poleceniami i dobór widoków zaczyna wymykać się spod mojej kontroli. Rzecz jasna kontroler aplikacji może budować swoje powiązania z poleceniami i widokami za pomocą dowolnych mechanizmów: nie musimy ograniczać się do środków ilustrowanych w przykładach. Nawet jeśli początkowo powiązanie będzie stałe, np. przez proste odwzorowanie ciągu żądania na nazwę polecenia i widok — nawet w takim przypadku będziemy korzystać z zalet kontrolera aplikacji, który nam to całe odwzorowanie skoncentruje. A potem, kiedy trzeba będzie zaszyć tam bardziej skomplikowaną logikę, obecność kontrolera aplikacji pozwoli łatwo ująć dodatkową złożoność bez konieczności znaczącej przebudowy reszty systemu.

263

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Wzorzec Page Controller Choć osobiście lubię wzorzec Front Controller (kontroler fasady), nie zawsze jest on najlepszym z możliwych. Taka inwestycja w projekt fasady daje pozytywne efekty w bardziej rozbudowanych aplikacjach, nie sprawdza się jednak w prostszych projektach, które trzeba jak najszybciej uruchomić. Wzorzec Page Controller jest zapewne już znany wielu czytelnikom — to dość popularna strategia. Tak czy inaczej warto poświęcić mu osobne omówienie i przyjrzeć się jego zaletom i wadom.

Problem Nasz problem wciąż polega na zarządzaniu relacjami zachodzącymi pomiędzy żądaniami, logiką dziedziny a prezentacją. Jest on zresztą w projektach korporacyjnych swego rodzaju stałą. Różnice występują raczej w ograniczeniach nałożonych na projektanta. W relatywnie prostym projekcie, zwłaszcza tam gdzie nadmiernie ambitne planowanie architektury zagroziłoby krótkim terminom wykonawczym, a nie zwiększyłoby istotnie jakości produktu, Page Controller jest wcale rozsądnym mechanizmem zarządzania żądaniami i widokami. Załóżmy, że chcemy prezentować w ramach aplikacji stronę wyświetlającą listę placówek wprowadzonych do systemu Woo. To proste zadanie mimo dysponowania kodem pobierającym stosowne dane z bazy danych bez kontrolera strony będzie dość żmudne. Pożądany widok ma zawierać listę placówek i żądanie dotyczy listy placówek. Dopuszczamy błędy, a żądanie nie ma prowadzić do nowego widoku, jak to się dzieje w bardziej złożonych zadaniach. Najprościej będzie powiązać widok z kontrolerem — nawet w obrębie tej samej strony.

Implementacja Wzorzec kontrolera strony jest stosunkowo prosty, choć wbrew pozorom wykorzystujące go projekty potrafią w praktyce mocno się zagmatwać. Wzorzec zakłada powiązanie kontroli z widokiem albo zestawem widoków. W najprostszym przypadku oznacza to, że kontrola jest osadzona w samym widoku, choć może zostać również wyodrębniona, zwłaszcza wtedy, kiedy widok jest ściśle połączony z innymi (np. kiedy w różnych okolicznościach zachodzi potrzeba przejścia do różnych stron). Oto najprostsze wydanie wzorca Page Controller:

Placówki

Placówki






264

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

Dokument ten zawiera dwa elementy. Element widoku obsługuje wyświetlanie, podczas gdy element kontrolera zarządza obsługą żądania, wywołując do jego realizacji logikę aplikacji. I choć kontroler i widok zebrane są na tej samej stronie, są od siebie starannie oddzielone. Przykład ten jest bardzo uproszczony (jeśli pominąć odbywającą się w tle komunikację z bazą danych, o której powiem więcej w następnym rozdziale). Blok kodu PHP u szczytu dokumentu próbuje pobrać listę placówek, zapisując ją potem w globalnej zmiennej $venues. Jeśli dojdzie do błędu, sterowanie zostanie przekazane do strony error.php (za pośrednictwem stosownej dyrektywy include() uzupełnionej o wywołanie exit() celem przerwania przetwarzania bieżącej strony). Osobiście wolę taki mechanizm przekazywania sterowania od klasycznego mechanizmu przekierowania na inną stronę HTML, ponieważ ten ostatni jest znacznie bardziej kosztowny i nie pozwala na podtrzymanie środowiska, które już zostało utworzone w pamięci. Jeśli po drodze nie nastąpi włączenie strony z komunikatem o błędzie, wtedy użytkownikowi zostanie odesłany dokument HTML generowany w drugim bloku kodu PHP (bloku widoku) — patrz rysunek 12.8.

Rysunek 12.8. Kontrolery stron osadzone w widokach Taka implementacja nadaje się do prostego testowania warstwy danych, ale z pewnością w każdym systemie o nieco większej skali złożoności trzeba będzie czegoś więcej. Kod kontrolera strony został poprzednio niejawnie oddzielony od widoku. Poniżej prezentuję więc zgrubny zarys klasy bazowej kontrolera strony: namespace woo\controller; //... abstract class PageController { abstract function process(); function forward($resource) { include($resource); exit(0); } function getRequest() { return \woo\base\ApplicationRegistry::getRequest(); } }

Klasa ta odwołuje się do narzędzi już wcześniej prezentowanych, w szczególności do klas Request i RequestRegistry. Podstawowym zadaniem klasy PageController jest udostępnianie obiektu żądania i zarządzanie włączaniem widoków. W rzeczywistych projektach ta skromna lista zadań szybko się rozrasta, w miarę jak przy dodawaniu kolejnych klas pochodnych ujawniają się potrzeby implementacji wspólnych dla nich funkcji. Klasa pochodna może egzystować wewnątrz widoku i wtedy wyświetlać go w sposób domyślny albo zostać od widoku odseparowana. Ten drugi sposób jest moim zdaniem bardziej elegancki, więc przyjrzymy się mu w następnej kolejności. Oto specjalizacja klasy PageController próbująca dodać do systemu nową placówkę: namespace woo\controller; //... class AddVenueController extends PageController { function process() { try { $request = $this->getRequest();

265

PHP. OBIEKTY, WZORCE, NARZĘDZIA

$name = $request->getProperty('venue_name'); if (is_null($request->getProperty('submitted'))) { $request->addFeedback("wybierz nazwę placówki"); $this->forward('add_venue.php'); } else if (is_null($name)) { $request->addFeedback("nazwa jest polem wymaganym"); $this->forward('add_venue.php'); } $venue = new \woo\domain\Venue(null, $name); // wstawienie do bazy danych $this->forward("ListVenues.php"); } catch (Exception $e) { $this->forward('error.php'); } } } $controller = new AddVenueController(); $controller->process();

Klasa AddVenueController implementuje jedynie metodę process(). Metoda ta jest odpowiedzialna za sprawdzenie danych przekazanych przez użytkownika. Jeśli użytkownik nie wypełni formularza albo wypełni go niewłaściwie, do strony włączony zostanie widok domyślny (add_venue.php) prezentujący użytkownikowi stosowny komunikat i formularz. Jeśli w końcu uda się skutecznie wprowadzić placówkę do systemu, wywołana zostanie metoda forward() przekazująca sterowanie do kolejnego kontrolera strony — ListVenues (wyświetlającego listę placówek). Chciałbym zwrócić uwagę na format, jaki zastosowałem w nazwach widoków. Nazwy plików klas staram się bowiem odróżniać od nazw plików widoków przez stosowanie w tych ostatnich wyłącznie małych liter (w nazwach plików klas stosuję zaś konwencję wyróżniania członów wielkimi literami). Oto widok skojarzony z klasą AddVenueController:

Dodaj placówkę

Dodaj placówkę








266

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

Jak można zauważyć, widok nie realizuje żadnego zadania poza wyświetleniem danych i udostępnieniem mechanizmu generowania nowego żądania (formularza). Żądanie jest kierowane do obiektu klasy PageController, nie zaś z powrotem do widoku, bo to kontroler strony jest odpowiedzialny za przetwarzanie żądań. Schemat tego nieco bardziej zaawansowanego wcielenia kontrolera strony prezentowany jest na rysunku 12.9.

Rysunek 12.9. Hierarchia klas kontrolera strony z relacjami włączania

Konsekwencje To podejście ma wielką zaletę: jest oczywiste dla wszystkich programistów mających nawet symboliczną styczność z aplikacjami WWW. Żądamy pliku venues.php i otrzymujemy to, co chcemy. Nawet ewentualne błędy są akceptowalne, bo komunikaty „server error” czy „page not found” stanowią jednak codzienność sieci WWW. Sprawa komplikuje się nieco, kiedy oddzielimy widok od kontrolera strony, ale wciąż zachodząca pomiędzy uczestnikami wzorca relacja jeden-do-jednego pozostaje dość oczywista. Potencjalne źródło problemów tkwi we włączaniu widoków. Kontroler strony włącza stosowny dla żądania widok jednokrotnie, po zakończeniu przetwarzania żądania. W pewnych okolicznościach może jednak za pośrednictwem tego samego kodu włączającego włączyć inny kontroler strony. Jeśli na przykład uda się w AddVenue skutecznie dodać placówkę do systemu, nie trzeba już wyświetlać formularza wprowadzania nowej placówki, więc sterowanie jest przekazywane do innego kontrolera strony zarządzającego listą placówek — ListVenues. Trzeba mieć przy tym jasność co do tego, kiedy włączany jest widok, a kiedy inny kontroler strony. Choć klasa kontrolera strony mogłaby delegować żądania do obiektów poleceń, zalety takiej obsługi żądań nie są tu tak oczywiste jak w przypadku kontrolera fasady. Klasy tego ostatniego muszą samodzielnie określać cel żądania — kontroler strony cel ten zna z góry. Analiza żądania i wywołania warstwy logiki, które byłyby realizowane przy poleceniach, są już gotowe w klasie kontrolera strony, odpada więc problem wyboru poleceń do obsługi żądania. Problemem może być powielanie kodu, ale i to da się ograniczyć przez wyodrębnienie bazowej klasy kontrolerów stron. Jest też kwestia oszczędności czasu wdrożenia, bo przy kontrolerze strony można uniknąć wczytywania danych, które w bieżącym kontekście zupełnie nie będą potrzebne. Oczywiście można taką oszczędność osiągnąć również przy kontrolerze fasady, ale trzeba by było w nim wdrożyć znacznie bardziej skomplikowany proces rozstrzygania o tym, co jest potrzebne, a co zbędne. Zasadnicza wada omawianego wzorca ujawnia się wtedy, kiedy rośnie komplikacja ścieżek przechodzenia pomiędzy widokami — zwłaszcza kiedy ten sam widok pełni w różnych okolicznościach odmienne role (dobrym przykładem jest tu formularz dodawania placówek, pod względem widoku identyczny z formularzem edycji placówki). Sprawdzanie owych okoliczności może stać się uciążliwe. Nie da się jednak zacząć projektu z kontrolerem strony, a potem przejść płynnie do kontrolera fasady — zwłaszcza jeśli w użyciu będzie klasa nadrzędna PageController. Gdybym oszacował, że dany system może zostać ukończony w ciągu mniej więcej tygodnia, a w przyszłości nie będzie znacząco rozbudowywany, wybrałbym przy jego realizacji kontroler strony, ciesząc się z szybkiego wdrożenia. Gdybym jednak pracował nad bardziej rozbudowanym projektem, o dłuższym przewidywanym okresie działania, a więc i rozwoju, cechującym się skomplikowaną logiką widoków, wybrałbym bezwzględnie kontroler fasady. 267

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Wzorce Template View i View Helper Template View (szablon widoku) to coś, czym zasadniczo dysponujemy w PHP domyślnie w tym sensie, że możemy mieszać znaczniki prezentacji (HTML) z właściwym kodem systemu (PHP). Jak już wspomniałem, możliwość ta jest równocześnie cenna i niebezpieczna, ponieważ łatwość łączenia odrębnych logicznie warstw jest bardzo kusząca, a konsekwencje ma fatalne. Dlatego w PHP głównym problemem w programowaniu widoku jest powściągliwość. Jeśli dany kod nie jest ściśle związany z wyświetlaniem, należałoby podejść do jego zastosowania w widoku z największą podejrzliwością. Z tego punktu widzenia przydatny staje się wzorzec View Helper (pomocnik widoku) przewidujący klasę pomocniczą, która może być specyficzna dla danego widoku albo dzielona przez wiele widoków, ułatwiając dowolne zadania, które wymagają więcej niż minimalnych ilości kodu.

Problem Zapytania SQL i logikę biznesową osadzoną wprost w stronach widoków widuje się już coraz rzadziej, ale wciąż się jednak widuje. Wynikające z takiego połączenia zło przedstawiałem już w poprzednich rozdziałach, nie będę się więc nad nim ponownie rozwodził. Strony WWW zawierające zbyt dużą ilość kodu utrudniają pracę projektantom samych stron, zwłaszcza kiedy komponenty prezentacyjne są wciągane do pętli i wyrażeń warunkowych. Osadzenie logiki biznesowej w warstwie prezentacji ściśle wiąże system z jednym interfejsem. Nie można wtedy łatwo wdrażać nowych interfejsów dostępu do systemu bez daleko idących ingerencji również w kod logiki aplikacji. Systemy z widokiem oddzielonym od logiki są też prostsze w testowaniu, bo testy mogą obejmować funkcjonalność logiki biznesowej w oddzieleniu od szumu warstwy prezentacji. Ponadto w systemach osadzających logikę w warstwie prezentacji mamy często do czynienia z zagrożeniami dla bezpieczeństwa. W takich systemach, gdzie zapytania do bazy danych i kod obsługi danych wprowadzanych przez użytkownika są wymieszane z tabelkami i formularzami, znalezienie i rozpoznanie potencjalnych zagrożeń jest wyjątkowo żmudne. Kolejnym problemem jest kwestia powielania kodu na stronach — w nieunikniony sposób prowadząca do kłopotów przy konserwacji i rozbudowie systemu. Aby temu wszystkiemu zapobiec, należałoby przesunąć przetwarzanie charakterystyczne dla logiki aplikacji poza widoki, a tym ostatnim pozostawiać jedynie zarządzanie prezentacją. Można to osiągnąć, czyniąc widoki wyłącznie pasywnymi odbiorcami danych. Tam, gdzie widok powinien odpytać system, należałoby udostępnić mu obiekt pomocnika widoku (View Helper), który wykonałby wszelkie związane z komunikacją z systemem zadania w imieniu widoku, ale na własną rękę.

Implementacja Po utworzeniu rozleglejszego szkieletu aplikacji implementacja warstwy widoku nie jest szczególnym wyzwaniem programistycznym. Oczywiście konieczny jest pewien wysiłek projektowania architektury informacji, ale to temat na osobną książkę. Wzorzec Template View został tak nazwany przez Martina Fowlera. Jest to wzorzec łącznikowy stosowany przez większość programistów aplikacji korporacyjnych. W niektórych językach jego implementacja angażuje systemy szablonowe, tłumaczące znaczniki na wartości ustawiane przez system. Taką opcję mamy też w PHP. Możemy wykorzystać gotowy mechanizm szablonowy, choćby wyśmienity Smarty. Według mnie warto bowiem korzystać z istniejącej infrastruktury PHP, o ile zachowa się przy tym odpowiednią staranność. Aby widok miał coś, na czym mógłby działać, musi mieć możliwość pozyskiwania danych. Do tej roli wyznaczam zazwyczaj pomocnika widoku (View Helper). Za jego pośrednictwem widok ma dostęp do obiektu żądania, a za pośrednictwem tego obiektu — do wszelkich innych obiektów potrzebnych mu w realizacji jego zadania. Oto prosta klasa pomocnika widoku: namespace woo\view; class ViewHelper{ static function getRequest() { return \woo\base\ApplicationRegistry::getRequest(); } }

268

ROZDZIAŁ 12.  WZORCE KORPORACYJNE

Jedynym zadaniem tej klasy jest udostępnienie obiektu klasy Request (obiektu żądania). W miarę rozwoju systemu można ją jednak uzupełnić dodatkowymi funkcjami pomocniczymi. Wszystko, co trzeba zrobić w ramach widoku, a co zajmuje więcej niż kilka wierszy kodu, można spróbować włączyć do zakresu odpowiedzialności pomocnika widoku. W większych aplikacjach można rozważyć udostępnienie wielu obiektów pomocników widoku w stosownej hierarchii dziedziczenia specjalizującej narzędzia i funkcje pomocnicze pod kątem wymagań różnych części systemu. Oto prosty widok robiący użytek z pomocnika widoku i obiektu żądania:

Wprowadź lokalizację w placówce

Wprowadź lokalizację w placówce




git-shell-commands/no-interactive-login chmod 755 git-shell-commands/no-interactive-login

Powyżej przełączamy się na konto użytkownika git. Zauważmy, że ponieważ już zmieniliśmy domyślną powłokę sesji zdalnych dla konta git, więc przełączając się na konto, musimy jawnie uruchomić powłokę bash. W niej tworzymy katalog git-shell-commands oraz prosty skrypt wypisujący komunikat o niedostępności sesji interaktywnych; skrypt na końcu trzeba oznaczyć jako wykonywalny. Teraz próba zalogowania się na konto git serwera repozytorium, nawet przez użytkownika o dostępie uwierzytelnianym kluczem publicznym, kończy się krótkim komunikatem i zamknięciem sesji: $ ssh git@my-git-server Enter passphrase for key '/home/mattz/.ssh/id_rsa': Last login: Mon Oct 21 19:03:58 2013 from some-remote-server Sorry. No interactive access. Connection to my-git-server closed.

Rozpoczynamy projekt Skoro mamy już zdalny serwer Git oraz dostęp do niego z lokalnego konta, pora dodać do repozytorium megaquiz wyniki swojej pracy z kodem projektu. Przedtem warto przyjrzeć się dobrze zawartości katalogu roboczego i usunąć z niego znalezione wszystkie pliki tymczasowe i pozostałości po sesji pracy. Wpychanie do repozytorium tego rodzaju artefaktów jest dużym uprzykrzeniem dla innych użytkowników repozytorium. Elementy tymczasowe projektu to między innymi automatycznie generowane pliki (np. pliki wyjściowe programu phpDocumentor), pliki dziennika instalatora i tym podobne.  Uwaga Pliki i katalogi, które powinny być trwale ignorowane, można zdefiniować jawnie i przez wzorce dopasowań w pliku .gitignore we wnętrzu repozytorium. W systemie Linux przykłady takich definicji można znaleźć w dokumentacji man gitignore, pokazującej sposoby stosowania symboli wieloznacznych do wykluczania rozmaitych plików i katalogów będących pozostałościami i plikami pośrednimi edytorów tekstu, systemów kompilacji i środowisk programistycznych. Dokumentacja ta jest dostępna również online  na stronie http://git-scm.com/docs/gitignore.

Zanim przejdziemy dalej, powinniśmy jeszcze przedstawić się Gitowi — pozwoli to łatwiej identyfikować autora zmian zatwierdzanych do repozytorium: $ git config --global user.name "matt z" $ git config --global user.email "[email protected]"

Skoro zdefiniowaliśmy już swoją tożsamość w kontekście repozytorium i wyczyściliśmy lokalną kopię roboczą projektu, możemy zainicjalizować katalog kopii roboczej jako katalog repozytorium Gita: $ cd megaquiz $ git init Initialized empty Git repository in /home/mattz/work/megaquiz/.git/

Teraz do repozytorium można dodać pliki projektu: $ git add .

367

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Od tego momentu wszystkie pliki dodane do repozytorium megaquiz będą monitorowane przez Gita. Pliki wchodzące w skład projektu mogą pozostawać w trzech stanach: nietknięte, zmodyfikowane oraz dodane do zmiany (ang. staged). Stany poszczególnych plików wypisuje polecenie git status: $ git status # # # # # # # # # # # # # # #

On branch master Initial commit Changes to be committed: (use "git rm --cached ..." to unstage) new new new new new new new

file: file: file: file: file: file: file:

command/Command.php command/CommandContext.php command/FeedbackCommand.php command/LoginCommand.php main.php quizobjects/User.php quiztools/AccessManager.php

Dzięki wydanemu uprzednio poleceniu git add wszystkie pliki są opisane jako włączone do zmiany. Teraz możemy tę zmianę zatwierdzić jako nową wersję projektu poleceniem commit: $ git commit -m 'my first commit' [master (root-commit) f4454af] my first commit 7 files changed, 214 insertions(+), 0 deletions(-) create mode 100755 command/Command.php create mode 100755 command/CommandContext.php create mode 100755 command/FeedbackCommand.php create mode 100755 command/LoginCommand.php create mode 100755 main.php create mode 100755 quizobjects/User.php create mode 100755 quiztools/AccessManager.php

Komunikat opisujący zmianę można podać za opcją -m. W przypadku pominięcia tej opcji Git sam uruchomi edytor tekstu, w którym można będzie wpisać komunikat opisu zmiany. Osoby przyzwyczajone do pracy z systemami kontroli wersji takimi jak CVS czy Subversion pomyślałyby, że na tym proces zatwierdzania zmiany się kończy. Faktycznie, możemy przejść do kolejnej rundy edytowania plików kodu, włączania zmodyfikowanych plików do kolejnej zmiany i zatwierdzania tej zmiany, ale wszystko to odbywa się na razie lokalnie. Jeśli postanowimy podzielić się wynikami swojej pracy z innymi uczestnikami, powinniśmy opublikować swoją zmianę (swoją wersję) w repozytorium centralnym. Wkrótce się przekonamy, że Git pozwala na łatwe prowadzenie prac w wielu gałęziach projektu; pozwala to na przykład na utrzymywanie osobnych gałęzi reprezentujących odrębne produkcyjne wydania kodu, oddzielnie od gałęzi, w których prototypujemy i szlifujemy kod dla następnych wydań. Kiedy zaczynamy pracę z projektem, Git tworzy w repozytorium lokalnym tylko gałąź główną (domyślną) projektu, zwaną zwyczajowo master. Listę aktualnie utrzymywanych gałęzi można wypisać poleceniem git branch: $ git branch -a * master

Opcja -a nakazuje Gitowi wypisanie wszystkich gałęzi (inaczej gałęzie zdalne zostałyby pominięte). Na wykazie gałęzi widać na razie tylko gałąź główną master.

368

ROZDZIAŁ 17.  ZARZĄDZANIE WERSJAMI PROJEKTU Z SYSTEMEM GIT

Do tej pory nie podjęliśmy żadnych kroków, aby skojarzyć lokalną gałąź projektu z repozytorium zdalnym. Pora nadrobić ten brak: $ git remote add --track master origin git@ mygitserver :/var/git/megaquiz

Powyższe polecenie jest w wypisywanych komunikatach mało efektowne, biorąc pod uwagę, ile dzieje się pod spodem. Mianowicie nakazaliśmy Gitowi skojarzyć nazwę origin ze wskazanym repozytorium zdalnym. Co więcej, ustanowiliśmy relację podległości pomiędzy lokalną gałęzią master a jej zdalnym odpowiednikiem (na razie w repozytorium zdalnym gałąź ta jest pusta). Wszystko to można potwierdzić, sprawdzając powiązania zdalne gałęzi: $ git remote -v origin git@mygitserver:/var/git/megaquiz (fetch) origin git@mygitserver:/var/git/megaquiz (push)

Wciąż nie wysłaliśmy do serwera zdalnego repozytorium Git ani jednego pliku; niniejszym to zrobimy: $ git push origin master Enter passphrase for key '/home/mattz/.ssh/id_rsa': Counting objects: 12, done. Delta compression using up to 2 threads. Compressing objects: 100% (9/9), done. Writing objects: 100% (12/12), 2.65 KiB, done. Total 12 (delta 1), reused 0 (delta 0) To git@mygitserver:/var/git/megaquiz * [new branch] master -> master

Ponowne wykonanie polecenia git branch potwierdza, że w repozytorium zdalnym pojawiła się gałąź master: $ git branch -a * master remotes/origin/master

 Uwaga Postarałem się, aby powyżej ustanowić relację „śledzenia” pomiędzy gałęziami lokalną i zdalną; powiązanie to oznacza, że gałęzie zdalna i lokalna są traktowane jako bliźniacze, i kiedy nakazujemy Gitowi wypchnąć zmianę z gałęzi, ma ona trafić do skojarzonej gałęzi zdalnej (tutaj origin/master). Git automatycznie ustanawia taką relację przy klonowaniu repozytorium z serwera zdalnego.

Klonowanie repozytorium Na potrzeby niniejszego rozdziału wykoncypowaliśmy członka zespołu programistycznego o imieniu Bob. Bob udziela się razem z nami w projekcie MegaQuiz. Oczywiście Bob chce pracować na własnej, lokalnej kopii kodu. Jego klucz publiczny znajduje się już w konfiguracji dostępu SSH po stronie serwera Git, więc Bob ma już dostęp do repozytorium. Klonowanie repozytorium zdalnego do repozytorium lokalnego odbywa się za pośrednictwem polecenia git clone: $ git clone git@mygitserver:/var/git/megaquiz

369

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Cloning into 'megaquiz'... Enter passphrase for key '/home/bob/.ssh/id_rsa': remote: Counting objects: 12, done. remote: Compressing objects: 100% (9/9), done. remote: Total 12 (delta 1), reused 0 (delta 0) Receiving objects: 100% (12/12), done. Resolving deltas: 100% (1/1), done.

Teraz również Bob może lokalnie modyfikować stan projektu i po osiągnięciu stanu godnego utrwalenia podzielić się swoją pracą z resztą zespołu za pośrednictwem repozytorium zdalnego.

Wprowadzanie i zatwierdzanie zmian Bob jest, rzecz jasna, utalentowanym programistą i ma tylko jedną irytującą cechę — wciąż wtrąca się do kodu pozostałych członków grupy. Bob jest inteligentny i wścibski, łatwo zapala się do nowych koncepcji programistycznych i gorliwie optymalizuje nowy kod. W efekcie gdziekolwiek by się zwrócić w projekcie, wszystko nosi ślady aktywności Boba. Bob dodał swoje trzy grosze do naszej dokumentacji i zaimplementował pomysł, o którym rozmawialiśmy przy kawie. Być może Boba trzeba będzie kiedyś zlikwidować, ale na razie należy pogodzić się z tym, że cały kod, nad którym pracujemy, musi być uzgadniany z propozycjami Boba. Oto plik quizobjects/User.php. Na razie nie zawiera on prawie nic, poza szczątkową definicją klasy:

W tej sesji pracy nad projektem zdecydowaliśmy się na uzupełnienie pliku o stosowną dokumentację. Z poprzedniego rozdziału pamiętasz zapewne, że dokumentacją powinniśmy opatrywać tak pliki, jak i poszczególne klasy. Zaczniemy od komentarza dokumentującego plik:

Pamiętajmy, że pliki wchodzące w skład projektu mogą znajdować się w jednym z trzech stanów: nietknięte, zmodyfikowane i zakwalifikowane do zatwierdzenia. Plik User.php zmienił stan z nietkniętego na zmodyfikowany, co można potwierdzić poleceniem git status: $ git status # On branch master # Changes not staged for commit: # (use "git add ..." to update what will be committed) # (use "git checkout -- ..." to discard changes in working directory) # # modified: quizobjects/User.php # no changes added to commit (use "git add" and/or "git commit -a")

370

ROZDZIAŁ 17.  ZARZĄDZANIE WERSJAMI PROJEKTU Z SYSTEMEM GIT

Plik User.php, choć zmodyfikowany, nie został zakwalifikowany do zbioru modyfikacji składających się na nową zmianę (wersję) do zatwierdzenia. Można to zmienić, dodając modyfikację pliku do zmiany poleceniem git add. $ git add quizobjects/User.php $ git status # # # # # #

On branch master Changes to be committed: (use "git reset HEAD ..." to unstage) modified: quizobjects/User.php

Teraz zmianę można już zatwierdzić: $ git commit -m'added documentation' quizobjects/User.php [master 2302dd8] added documentation 1 files changed, 5 insertions(+), 0 deletions(-)

Zatwierdzenie zmiany dotyczy zawsze tylko lokalnego repozytorium Git. Dopiero jeśli jesteśmy gotowi podzielić się zmianą ze światem, publikujemy ją, wypychając do repozytorium zdalnego: $ git push Enter passphrase for key '/home/mattz/.ssh/id_rsa': Counting objects: 7, done. Delta compression using up to 2 threads. Compressing objects: 100% (3/3), done. Writing objects: 100% (4/4), 409 bytes, done. Total 4 (delta 1), reused 0 (delta 0) To git@mygitserver:/var/git/megaquiz f4454af..2302dd8 master -> master

W międzyczasie i zupełnie niezależnie od nas nasz niestrudzony Bob we własnej kopii roboczej również gorliwie utworzył komentarz dokumentujący klasę:

Teraz kolej Boba, żeby dodać modyfikacje do zmiany, zatwierdzić zmianę i wypchnąć ją do repozytorium zdalnego. Ponieważ operacje dodawania modyfikacji i zatwierdzania zmiany są często wykonywane łącznie, Git oferuje skróconą komendę łączącą te dwie operacje: $ git commit -a -m'my great documentation' [master 2d880e6] my great documentation 1 files changed, 3 insertions(+), 0 deletions(-)

Teraz mamy dwie różne wersje pliku User.php: wersję wypchniętą przez nas do repozytorium zdalnego i wersję Boba, zatwierdzoną u niego lokalnie, ale jeszcze nie wypchniętą. Co się stanie, kiedy Bob spróbuje wypchnąć swoją lokalną zmianę do repozytorium zdalnego? 371

PHP. OBIEKTY, WZORCE, NARZĘDZIA

$ git push Enter passphrase for key '/home/bob/.ssh/id_rsa': To git@mygitserver:/var/git/megaquiz ! [rejected] master -> master (non-fast-forward) error: failed to push some refs to 'git@mygitserver:/var/git/megaquiz' To prevent you from losing history, non-fast-forward updates were rejected Merge the remote changes (e.g. 'git pull') before pushing again. See the 'Note about fast-forwards' section of 'git push --help' for details.

Jak widać, Git nie pozwoli wypchnąć kolidującej zmiany, jeśli nie potrafi sam rozstrzygnąć konfliktu wersji (a potrafi, jeśli konflikt jest trywialny). Bob musi więc najpierw wciągnąć naszą zmianę pliku User.php do swojego repozytorium i uzgodnić ją ze swoją wersją: $ git pull Enter passphrase for key '/home/bob/.ssh/id_rsa': remote: Counting objects: 7, done. remote: Compressing objects: 100% (3/3), done. remote: Total 4 (delta 1), reused 0 (delta 0) Unpacking objects: 100% (4/4), done. From mygitserver:/var/git/megaquiz f4454af..2302dd8 master -> origin/master Auto-merging quizobjects/User.php CONFLICT (content): Merge conflict in quizobjects/User.php Automatic merge failed; fix conflicts and then commit the result.

Git skutecznie scali zmiany wprowadzone do danego pliku lokalnie ze zmianami z repozytorium, o ile te zmiany nie będą ze sobą kolidować. Git nie ma żadnej możliwości samodzielnego obsłużenia kolizji dotyczącej tych samych wierszy pliku. Jak niby automat miałby zdecydować, czyje zmiany są ważniejsze? Czy repozytorium powinno zamazać zmiany Boba, czy odwrotnie: zmiany Boba powinny mieć pierwszeństwo przed zmianami innych programistów? Czy zmiany mogą współistnieć? Która z nich powinna być pierwsza? Jedyną operacją, jaką Git może wtedy skutecznie wykonać, jest powiadomienie użytkownika o kolizji i pozostawienie decyzji samemu zainteresowanemu. Po otwarciu pliku Bob zobaczy w nim następujące oznakowanie:

Git pokaże komentarz Boba oraz kolidującą z nim zmianę z repozytorium wraz z metadanymi pomocnymi w sprawdzeniu, skąd pochodzą dane fragmenty kodu. Informacje o kolizji są oddzielane wierszem znaków równości. Modyfikacje Boba są oznaczane wierszem znaków mniejszości z etykietą „HEAD”. Modyfikacje wciągnięte z repozytorium zdalnego znajdują się pod wierszem znaków równości, aż do wiersza znaków większości z etykietą opisującą identyfikator zmiany z repozytorium zdalnego.

372

ROZDZIAŁ 17.  ZARZĄDZANIE WERSJAMI PROJEKTU Z SYSTEMEM GIT

Skoro Bob zidentyfikował konflikt, może wyeliminować kolizję modyfikacji przez uzgodnienie ręczne:

Kolejny krok rozstrzygania konfliktu to zakwalifikowanie i zatwierdzenie nowej wersji pliku: $ git add quizobjects/User.php $ git commit -m'documentation merged' [master a407692] documentation merged

Teraz już Bob może skutecznie wypchnąć swoją zmianę, z naniesionymi zmianami już obecnymi w repozytorium zdalnym: $ git push

Dodawanie i usuwanie plików i katalogów Projekty w czasie swojego rozwoju ulegają zmianom, to oczywiste. Oprogramowanie kontroli wersji musi brać pod uwagę różne aspekty tych zmian, pozwalając użytkownikom również na dodawanie do projektu nowych plików i katalogów oraz usuwanie tych, z których rozwijania już zrezygnowano.

Dodawanie pliku Znamy już polecenie git add z kilku przykładów; stosowaliśmy je w toku przygotowania pierwotnej wersji projektu do dodania całości kodu do pustego repozytorium megaquiz, a potem do kwalifikowania modyfikacji do zatwierdzenia. Wykonanie polecenia git add na nie monitorowanym jeszcze pliku albo katalogu oznacza chęć dodania pliku (katalogu) do projektu i równocześnie zakwalifikowanie do zmiany. Oto polecenie dodające do projektu plik Question.php: $ touch quizobjects/Question.php $ git add quizobjects/Question.php

W praktyce zaczęlibyśmy taką operację od wypełnienia pliku Question.php jakimś kodem. Tutaj zaczęliśmy od utworzenia pustego pliku poleceniem touch. Po dodaniu pliku z kopii roboczej do repozytorium wciąż konieczne jest wykonanie polecenia commit (polecenie add modyfikuje jedynie lokalną kopię roboczą). $ git commit -m'initial checkin' [master 96da108] initial checkin 0 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 quizobjects/Question.php

Teraz plik Question.php znajduje się na dobre w lokalnym repozytorium.

373

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Usuwanie pliku Kiedy okaże się, że początkowe rozproszenie projektu pomiędzy plikami jest nadmierne i można będzie kilku plików się pozbyć, będziemy mieli do dyspozycji polecenie wewnętrzne Git o nazwie rm: $ git rm quizobjects/Question.php rm 'quizobjects/Question.php'

Ponownie do utrwalenia zmiany w repozytorium konieczne jest polecenie commit. Jak zwykle zawartość zatwierdzanej zmiany można sprawdzić poleceniem git status: $ git status # # # # # #

On branch master Changes to be committed: (use "git reset HEAD ..." to unstage) deleted: quizobjects/Question.php

$ git commit -m'removed Question' [master 0d571b2] removed Question 0 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 quizobjects/Question.php

Dodawanie katalogu Poleceniami add i rm możemy również dodawać do projektu i usuwać z niego katalogi. Powiedzmy, że Bob chciałby udostępnić w projekcie nowy katalog: $ mkdir resources $ touch resources/blah.gif $ git add resources/ $ git status # # # # # #

On branch master Changes to be committed: (use "git reset HEAD ..." to unstage) new file: resources/blah.gif

Zauważmy, że do repozytorium dodana została automatycznie również cała zawartość katalogu resources. Teraz Bob może już zwyczajnie zatwierdzić, a następnie wypchnąć całość do zdalnego repozytorium.

Usuwanie katalogów Jak można się spodziewać, do usuwania katalogów z projektu służy polecenie rm. W kontekście katalogu trzeba jednak poinformować Gita, że faktycznie zamierzamy usunąć katalog z całą zawartością, przekazując do polecenia opcję -r. Powiedzmy, że nie zgodziłem się z decyzją Boba odnośnie do katalogu resources i postanowiłem go usunąć z repozytorium: $ git remove resources/ rm 'resources/blah.gif'

374

ROZDZIAŁ 17.  ZARZĄDZANIE WERSJAMI PROJEKTU Z SYSTEMEM GIT

Etykietowanie wersji Jeśli harmonogram będzie realizowany, projekt kiedyś osiągnie stan gotowości do wdrożenia i zechcemy go czy to wdrożyć, czy udostępnić klientom. Każda wersja uznana za stabilną powinna zostać w repozytorium oznaczona specjalną etykietą (ang. tag), aby później zawsze można było się do niej odwołać i przywrócić stan projektu z wyznaczonego momentu. Łatwo się domyślić, że etykietę wersji kodu tworzy się poleceniem git tag: $ git tag -a 'version1.0' -m 'release 1.0'

Etykiety skojarzone ze zmianami w repozytorium można wypisać za pomocą gołego polecenia git tag: $ git tag version1.0

Aż do tego momentu pracowaliśmy lokalnie. Aby zdefiniowane lokalnie etykiety trafiły do repozytorium zdalnego, trzeba przy poleceniu git push użyć opcji --tags: $ git push --tags Enter passphrase for key '/home/mattz/.ssh/id_rsa': Counting objects: 1, done. Writing objects: 100% (1/1), 160 bytes, done. Total 1 (delta 0), reused 0 (delta 0) To git@mygitserver:/var/git/megaquiz * [new tag] version1.0 -> version1.0

Skoro potrafimy oznaczać wybrane wersje kodu etykietami, warto się zastanowić, jak się potem do tych wersji odwołać. Ale najpierw musimy zapoznać się z czymś, w czym Git naprawdę celuje, to znaczy z zarządzaniem gałęziami projektów.

Rozgałęzianie projektu Udało się nam już wydać pierwszą wersję naszego projektu, możemy więc spokojnie przejść do następnego, prawda? Przecież projekt był oprogramowany tak elegancko, że nie spodziewamy się wykrycia żadnych błędów, a i tak przemyślany, że żadnemu z użytkowników nie zabraknie funkcji! Ale w rzeczywistym świecie ukończony pozornie kod wciąż wymaga pracy, przynajmniej z dwóch względów. Po pierwsze, należy oczekiwać zgłoszeń o zaobserwowanych błędach, po drugie zaś, równie szybko pojawią się postulaty uzupełnienia następnej wersji o parę choćby funkcji i opcji. Jak pogodzić te wymagania? Błędy trzeba poprawiać na bieżąco i powinny mieć pierwszeństwo. Moglibyśmy co prawda stosować poprawki w ramach normalnego cyklu życia projektu i opublikować je dopiero wraz z następną wersją, ale wielu użytkowników nie będzie mogło pozwolić sobie na tak długie oczekiwanie. To po prostu niedopuszczalne. Z drugiej strony, moglibyśmy przy okazji publikowania poprawek uzupełniać produkt o nowe funkcje, ale te będą nieprzetestowane i jako niestabilne z pewnością nie przyczynią się do zwiększenia jakości produktu. Najwyraźniej potrzebujemy więc dwóch niezależnych ścieżek rozwoju projektu. Prototypowanie i wprowadzanie nowych, niesprawdzonych funkcji będziemy wykonywać w gałęzi głównej, ale ustabilizowane wydania, do których zamierzamy już tylko wprowadzać ewentualne poprawki, powinny otrzymać swoje własne, niezależne gałęzie. Do tworzenia gałęzi i przełączania się pomiędzy nimi służy polecenie git checkout. Spójrzmy na bieżący stan gałęzi projektu: $ git branch -a * master remotes/origin/master

375

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Na wykazie widać tylko gałąź główną i jej odpowiednik w repozytorium zdalnym. Utworzymy więc nową gałąź i od razu się do niej przełączymy: $ git checkout -b megaquiz-branch1.0 Switched to a new branch 'megaquiz-branch1.0'

Prace w gałęziach będziemy ćwiczyli na pliku command/FeedbackCommand.php. Zdaje się, że to była już najwyższa pora na utworzenie gałęzi dla poprawek, bo użytkownicy zaczęli zgłaszać błędy w działaniu mechanizmu sygnałów zwrotnych. Błąd został już zlokalizowany: //... $result = $msgSystem->dispatch($email, $msg, $topic); if (!$user) { $this->context->setError($msgSystem->getError()); //...

Zamiast sprawdzać ustawienie zmiennej $user, powinniśmy testować zmienną $result. Oto poprawka: //... $result = $msgSystem->dispatch($email, $msg, $topic); if (!$result) { $this->context->setError($msgSystem->getError()); //...

Skoro pracujemy w gałęzi megaquiz-branch1.0, możemy w niej zatwierdzić poprawkę: $ git add command/FeedbackCommand.php $ git commit -m'bugfix' [megaquiz-branch1.0 5c963b6] bugfix 1 files changed, 1 insertions(+), 1 deletions(-)

Oczywiście zatwierdzenie jest lokalne; żeby udostępnić poprawkę w repozytorium zdalnym, trzeba użyć polecenia git push do wypchnięcia zmiany poprawkowej do repozytorium zdalnego: $ git push -u origin megaquiz-branch1.0 Enter passphrase for key '/home/mattz/.ssh/id_rsa': Counting objects: 7, done. Delta compression using up to 2 threads. Compressing objects: 100% (4/4), done. Writing objects: 100% (4/4), 431 bytes, done. Total 4 (delta 2), reused 0 (delta 0) To git@mygitserver:/var/git/megaquiz * [new branch] megaquiz-branch1.0 -> megaquiz-branch1.0 Branch megaquiz-branch1.0 set up to track remote branch megaquiz-branch1.0 from origin.

Pamiętajmy, że origin to w istocie namiar na repozytorium zdalne. Opcja -u gwarantuje, że gałąź repozytorium lokalnego będzie na potrzeby tej operacji wypchnięcia skojarzona z pasującą gałęzią (rozpoznaną po nazwie) repozytorium zdalnego. Jeśli chcemy to powiązanie zdefiniować trwale, możemy ustanowić relację pomiędzy gałęzią lokalną i zdalną następującym poleceniem: $ git branch --set-upstream megaquiz-branch1.0 origin/megaquiz-branch1.0

A co z dziełami naszego kolegi Boba? Na pewno też zastanowi się nad niektórymi zgłoszeniami o błędach i będzie chciał wprowadzić poprawki. Najpierw powinien wykonać polecenie git pull, które poinformuje go o pojawieniu się nowej gałęzi repozytorium zdalnego:

376

ROZDZIAŁ 17.  ZARZĄDZANIE WERSJAMI PROJEKTU Z SYSTEMEM GIT

$ git pull Enter passphrase for key '/home/bob/.ssh/id_rsa': remote: Counting objects: 7, done. remote: Compressing objects: 100% (4/4), done. remote: Total 4 (delta 2), reused 0 (delta 0) Unpacking objects: 100% (4/4), done. From mygitserver:/var/git/megaquiz * [new branch] megaquiz-branch1.0 -> origin/megaquiz-branch1.0 Already up-to-date.

Teraz Bob powinien utworzyć lokalną gałąź śledzącą nową gałąź zdalną: $ git checkout -b megaquiz-branch1.0 origin/megaquiz-branch1.0 Branch megaquiz-branch1.0 set up to track remote branch megaquiz-branch1.0 from origin. Switched to a new branch 'megaquiz-branch1.0'

Teraz Bob może przystąpić do pracy i wprowadzać oraz lokalnie zatwierdzać własne poprawki, a potem wypychać je do repozytorium zdalnego, gdzie wylądują w odpowiedniej gałęzi. W międzyczasie, znużeni naprawianiem błędów, siadamy do zaprogramowania najnowszego efektownego rozszerzenia funkcjonalności projektu — oczywiście w gałęzi głównej. Spójrzmy ponownie na stan gałęzi z perspektywy repozytorium lokalnego: $ git branch -a master * megaquiz-branch1.0 remotes/origin/master

Do przełączania się pomiędzy już istniejącymi gałęziami służy polecenie git checkout, ale bez opcji -b: $ git checkout master Switched to branch 'master'

Kiedy teraz zajrzymy do pliku command/FeedbackCommand.php, zauważymy, że nasza poprawka wyparowała. Ale pamiętamy, że jest utrwalona w gałęzi megaquiz-branch1.0 — i w dowolnym momencie będziemy mogli ją nanieść na kod gałęzi głównej. Na razie więc możemy się skupić na dodawaniu nowego kodu: class FeedbackCommand extends Command { function execute(CommandContext $context) { // w tej gałęzi wprowadzamy nowe i niesprawdzone jeszcze funkcje $msgSystem = ReceiverFactory::getMessageSystem(); $email = $context->get('email'); $msg = $context->get('pass'); $topic = $context->get('topic'); $result = $msgSystem->dispatch($email, $msg, $topic); if (!$user) { $this->context->setError($msgSystem->getError()); return false; } $context->addParam("user", $user); return true; } }

377

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Nasza nowa funkcjonalność jest na potrzeby przykładu reprezentowana jedynie przez fikcyjny komentarz zapowiadający przyszłe efektowne implementacje. Niniejszym operujemy na dwóch równoległych gałęziach rozwoju kodu. Prędzej czy później zechcemy wszystkie poprawki wprowadzone do gałęzi wydania stabilnego megaquiz-branch1.0 nanieść na gałąź rozwojową. Okazuje się to bardzo proste, a to za sprawą polecenia git merge: $ git checkout master Switched to branch 'master' $ git merge megaquiz-branch1.0 Updating 102966e..5c963b6 Fast-forward command/FeedbackCommand.php | 2 +1 files changed, 1 insertions(+), 1 deletions(-)

 Uwaga Scalać czy nie scalać? Decyzja nie zawsze jest oczywista. W niektórych sytuacjach poprawka błędu może być obejściem tymczasowym, które ma i tak zostać wyeliminowane przez refaktoryzację o większym zasięgu; poprawka może też w wersji rozwojowej być nieaktualna choćby z powodu zmian w specyfikacji projektu. Decyzja jest więc często trudna; w większości zespołów, w których zdarzyło mi się pracować, w miarę możliwości decydowano się na scalanie zmian w gałęzi głównej, starając się za to ograniczać pracę w gałęziach pobocznych do minimum. Sprawdza się to zwłaszcza tam, gdzie postanowiono rozwijać projekt w gałęzi głównej i gdzie przyjęto zasadę częstych i wczesnych wydań.

Kiedy teraz obejrzymy kod w katalogu kopii roboczej gałęzi głównej, zobaczymy, że poprawka faktycznie została naniesiona: class FeedbackCommand extends Command { function execute(CommanControl $context) { // tu nowe i ryzykowne elementy kodu $msgSystem = ReceiverFactory::GetMessageSystem(); $email = $context->get('mail'); $msg = $context->get('pass'); $topic = $context->get('topic'); $result = $msgSystem->dispatch($email, $msg, $topic); if (!$result) { $this->context->setError($msgSystem->getError()); return false; } $context->addParam("user", $user); return true; } }

Metoda execute() zawiera teraz zarówno świeży kod rozwojowy, jak i poprawkę przeniesioną z gałęzi poprawkowej. Gałąź stabilną utworzyliśmy w momencie wydania wersji MegaQuiz 1.0 i w tej gałęzi wykonywaliśmy poprawki. Pamiętajmy też jednak, że wraz z wydaniem utworzyliśmy etykietę. Była mowa o tym, że wkrótce nauczymy się pełniej korzystać z etykiet. W rzeczywistości już to zrobiliśmy; możemy tworzyć lokalne gałęzie biorące swój początek w etykiecie, tak samo jak Bob utworzył swoją lokalną wersję gałęzi poprawek. Różnica sprowadza się do tego, że taka nowa gałąź utworzona na podstawie etykiety jest zupełnie świeża — nie śledzi istniejącej już gałęzi zdalnej:

378

ROZDZIAŁ 17.  ZARZĄDZANIE WERSJAMI PROJEKTU Z SYSTEMEM GIT

$ git checkout -b new-version1.0-branch version1.0 Switched to a new branch 'new-version1.0-branch'

Tak utworzona gałąź może być oczywiście wypchnięta i udostępniona innym członkom projektu  podobnie jak robiliśmy to wcześniej.

Podsumowanie Git obejmuje nadzwyczajną liczbę narzędzi, z których każde rozpoznaje mnóstwo opcji i realizuje wiele funkcji. Siłą rzeczy pojedynczy rozdział może zawierać jedynie podstawowe wprowadzenie użytkowania takiego systemu we własnych projektach. Ale już wykorzystanie w swoich projektach choćby tylko zaprezentowanych tu funkcji daje znakomite efekty, nie tylko redukując ryzyko utraty danych (przez nadpisanie działającego kodu nowymi wersjami), ale i bardzo ułatwiając współpracę w zespole. Niniejszy rozdział stanowił skrócony przegląd najbardziej podstawowych cech Git. Przyjrzeliśmy się konfigurowaniu Git, importowaniu zawartości projektu do repozytorium, klonowaniu repozytorium, wykonywaniu, zatwierdzaniu zmian i wypychaniu kodu do repozytorium, rozstrzyganiu kolizji pomiędzy zmianami wprowadzanymi przez różne osoby, a wreszcie etykietowaniu i eksportowaniu wersji projektu. Na koniec zajęliśmy się rozgałęzianiem projektu, ilustrując jego przydatność na przykładzie gałęzi poprawek wyodrębnionej z głównego nurtu rozwoju projektu. W całym omówieniu przemilczeliśmy pewną ważną kwestię. Otóż ustanowiliśmy zasadę wyciągania z repozytorium własnych wersji projektu. W lokalnych „piaskownicach” można z powodzeniem rozwijać projekt, ale nie sposób testować wprowadzonych zmian. Testowanie to wymaga bowiem zazwyczaj choćby częściowego wdrożenia projektu. Czasem polega ono jedynie na przerzuceniu paru plików pomiędzy katalogami, kiedy indziej wymaga wyczerpującej konfiguracji systemu. Całość najlepiej byłoby zautomatyzować, czym zajmiemy się w następnym rozdziale.

379

PHP. OBIEKTY, WZORCE, NARZĘDZIA

380

ROZDZIAŁ 18 

Testy jednostkowe z PHPUnit

Nieprzerwane i płynne działanie każdego z komponentów systemu zależne jest od spójności operacji i interfejsów komponentów współpracujących. Z definicji proces rozwoju programowania narusza tę spójność. Ulepszając klasy i pakiety, trzeba pamiętać o przystosowaniu do zmian wszelkiego kodu, który do tych klas się odwołuje. Niektóre zmiany mogą w ten sposób pociągnąć kaskadę poprawek, wymuszając zmiany nawet komponentów bardzo z pozoru odległych od źródła zmian. Problem ten pomagają opanować: sokole oko, nieustanna czujność i encyklopedyczna wiedza o wnętrznościach systemu. Wszystko to piękne cnoty, ale systemy mają zwyczaj rozrastać się tak, że nie sposób już przewidzieć wszystkich ubocznych efektów; także dlatego, że poszczególne części systemu pozostają zazwyczaj pod opieką różnych programistów. Dobrze by więc było zduszać problemy w zarodku, regularnie testując wszystkie komponenty. Jako zadanie żmudne, pracochłonne i powtarzalne kandyduje ono oczywiście do automatyzacji. Wśród narzędzi do testowania kodu przeznaczonych dla języka PHP PHPUnit wyraźnie się wyróżnia — jest bodaj narzędziem najpowszechniej stosowanym, a na pewno najbardziej kompletnym. Warto opanować jego stosowanie i takie zadanie stawiamy sobie w niniejszym rozdziale. Będziemy mówić o:  Instalacji — jak zainstalować PHPUnit z repozytorium PEAR.  Pisaniu testów — czyli o tworzeniu przypadków testowych i o metodach asercji.  Obsłudze wyjątków — o strategiach wychwytywania błędów.  Uruchamianiu pakietów testów — a także o gromadzeniu testów w pakiety, tzw. zestawy testowe.  Konstruowaniu logiki asercji — czyli o ograniczeniach będących kryteriami testów.  Komponentach-atrapach — a konkretnie o atrapach i imitacjach.  Testowaniu aplikacji WWW — z użyciem i bez użycia dodatkowych narzędzi testujących.

Testy funkcjonalne i testy jednostkowe Testowanie to zasadnicza część życia każdego projektu. Jeśli nawet proces testowania nie zostanie wcale sformalizowany, trzeba przecież choćby dla własnych potrzeb opracować listę czynności, które system powinien wykonywać. Szybko jednak staje się to uciążliwe, a znużenie testami prowadzi do zwiększania ryzyka utrwalania błędów w projektach. Testowanie można rozpocząć na poziomie interfejsu użytkownika, symulując różne sposoby, na jakie użytkownik może wchodzić w interakcje z systemem. Testy takie wykonuje się zazwyczaj ręcznie, choć istnieją też narzędzia pozwalające na ich częściowe choćby zautomatyzowanie. Owe testy funkcjonalne noszą też miano testów akceptacyjnych, ponieważ lista pomyślnie przeprowadzonych operacji może służyć jako kryterium zakończenia fazy projektu. W takim podejściu system traktowany jest jako czarna skrzynka — testy są pisane

PHP. OBIEKTY, WZORCE, NARZĘDZIA

w oderwaniu od wiedzy o szczegółach implementacji wewnętrznej poszczególnych komponentów składających się na testowany system. Testy funkcjonalne operują z zewnątrz, natomiast testy jednostkowe (których przede wszystkim dotyczy ten rozdział) działają niejako od wewnątrz. Testy jednostkowe koncentrują się na pojedynczych klasach, z metodami testowymi zgrupowanymi w przypadki testowe. Każdy przypadek testowy (ang. test case) oznacza sprawdzenie poprawności pojedynczej klasy poddawanej rozmaitym operacjom — celem jest sprawdzenie, czy wszystkie metody klasy działają zgodnie z oczekiwaniem oraz czy w przypadku błędnych wywołań poprawnie identyfikują i reagują na błędy. Chodzi o to, aby przetestować każdy z komponentów systemu w możliwie ścisłej izolacji, w oderwaniu od zwyczajnego kontekstu użycia. Sama możliwość przeprowadzenia szeroko zakrojonych testów jednostkowych świadczy zaś o skutecznym rozdzieleniu komponentów, co zawsze jest pożądaną cechą rozbudowanych systemów. Testy mogą być uruchamiane w toku procesu kompilacji projektu, bezpośrednio z wiersza poleceń albo nawet za pośrednictwem strony WWW. W tym rozdziale skoncentrujemy się na uruchamianiu ich z poziomu wiersza poleceń. Testowanie jednostkowe to znakomity sposób zapewniania jakości systemu. Testy ujawniają zakresy odpowiedzialności poszczególnych klas i funkcji. Niektórzy programiści propagują nawet podejście dające tym testom priorytet, twierdząc, że testy takie należałoby sformułować jeszcze przed zaimplementowaniem klasy. Ma to zapewnić klasom przejrzysty interfejs i odpowiednie skupienie metod. Osobiście nigdy nie miałem ambicji osiągnięcia tego stopnia czystości kodu — to zwyczajnie nie pasuje do mojego stylu programowania. Niemniej jednak staram się pisać testy równolegle z programowaniem klas; utrzymywanie tak powstającej infrastruktury testowej zabezpiecza mnie na wypadek przyszłych refaktoryzacji kodu. Mogę swobodnie wyciągać i zastępować całe pakiety komponentów, wiedząc, że ewentualne błędy refaktoryzacji zostaną szybko wychwycone.

Testowanie ręczne W poprzednim podrozdziale była mowa o tym, że testy są kluczowym elementem prowadzenia każdego projektu. Można powiedzieć mocniej, że testy są nieodłącznym elementem projektu. Wszyscy testujemy kod. Tragedia polega raczej na tym, że często te testy zarzucamy — nie zachowujemy ich na przyszłość. Spróbujmy więc utworzyć kilka klas do przetestowania. Oto klasa, która przechowuje i udostępnia informacje o użytkownikach. Dla uproszczenia demonstracji zamiast obiektów klasy User (normalnie oczekiwalibyśmy takiego efektu działania klasy) będzie ona generowała tablice z danymi o użytkownikach: class UserStore { private $users = array(); function addUser($name, $mail, $pass) { if ( isset($this->users[$mail])) { throw new Exception( "Konto {$mail} już istnieje w systemie"); } if (strlen($pass) < 5) { throw new Exception( "Hasło musi mieć co najmniej 5 liter"); } $this->users[$mail] = array( 'pass' => $pass, 'mail' => $mail, 'name' => $name ); return true; } function notifyPasswordFailure($mail) { if (isset( $this->users[$mail])) { $this->users[$mail]['failed']=time();

382

ROZDZIAŁ 18.  TESTY JEDNOSTKOWE Z PHPUNIT

} } function getUser($mail) { return($this->users[$mail]); } }

Klasa ta przyjmuje dane użytkownika do utrwalenia za pośrednictwem metody addUser(), a zwraca je przez metodę getUser(). Kluczem wyszukiwania wpisów użytkowników jest adres poczty elektronicznej użytkownika. Solidni programiści będą w miarę rozwoju kodu pisać również przykładowe implementacje choćby po to, żeby sprawdzić, czy całość zachowuje się zgodnie z oczekiwaniem; chodzi o proste fragmenty kodu, w rodzaju poniższego: $store=new UserStore(); $store->addUser("bob williams", "[email protected]", "12345"); $user = $store->getUser("[email protected]"); print_r($user);

Taki prosty kod weryfikujący zwykłem umieszczać na końcu pliku klasy. Weryfikacja testu jest przeprowadzana ręcznie, to znaczy sam oglądam wyniki i potwierdzam, że dane zwrócone przez metodę UserStore::getUser() odpowiadają danym przekazanym do obiektu UserStore za pomocą metody addUser(). Tak czy inaczej, jest to jakiś test. Poniżej mamy klasę użytkującą klasę zdefiniowaną wyżej. Odwołuje się ona do klasy UserStore celem potwierdzenia, że użytkownik przedstawił prawidłowe informacje uwierzytelniające. class Validator { private $store; public function __construct(UserStore $store) { $this->store = $store; } public function validateUser($mail, $pass) { if (!is_array($user = $this->store->getUser($mail))) { return false; } if ($user['pass'] == $pass) { return true; } $this->store->notifyPasswordFailure($mail); return false; } }

Konstruktor klasy wymaga przekazania obiektu klasy UserStore, który zachowuje w składowej $store. Składowa ta jest potem wykorzystywana w metodzie validateUser() celem upewnienia się, czy wskazany adresem poczty elektronicznej użytkownik jest obecny w pamięci systemu i że podane w jego imieniu hasło zgadza się z tym zachowanym w pamięci. Jeśli oba te warunki są spełnione, metoda validateUser() zwraca wartość true. I ponownie: gotową (czy też jeszcze wstępną) implementację możemy od razu przetestować: $store = new UserStore(); $store->addUser("bob williams", "[email protected]", "12345"); $validator = new Validator($store); if ($validator->validateUser("[email protected]", "12345")) { print "zaliczone!\n"; } Kod tworzy egzemplarz klasy UserStore, zasila go danymi użytkownika, a następnie przekazuje dane wydobyte z powrotem z obiektu do egzemplarza obiektu weryfikującego Validator. W ten sposób można łatwo potwierdzić

poprawność zarządzania nazwami kont i haseł.

383

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Kiedy prace uznamy za zakończone, testowe fragmenty kodu możemy usunąć z pliku lub oznaczyć komentarzem. Jest to jednak straszne marnotrawstwo wysiłku i cennych zasobów. Testy te przecież mogłyby stanowić podstawę infrastruktury testowej rozwijanego kodu. Jednym z narzędzi pomocnych w utrzymaniu tej infrastruktury jest PHPUnit.

PHPUnit PHPUnit to element całej rodziny narzędzi testowania xUnit. Wszystkie wywodzą się z SUnit — frameworku opracowanego przez Kenta Becka do testowania systemów pisanych w języku Smalltalk. xUnit był narzędziem popularnym, ale dopiero implementacja dla języka Java (jUnit) i upowszechnienie się metodologii szybkiego prowadzenia projektów w rodzaju Extreme Programming czy Scrum wyniosło znaczenie testów na zupełnie nowy poziom. Obecne wcielenie PHPUnit to dzieło Sebastiana Bergmanna, który na początku 2007 roku zmienił nazwę pakietu z PHPUnit2 i przeniósł projekt z kanału pear.php.net do pear.phpunit.de. Z tego względu program pear musi być przy instalowaniu jawnie poinstruowany co do lokalizacji frameworku PHPUnit: $ pear config-set auto_discover 1 $ pear install --alldeps pear.phpunit.de/phpunit

 Uwaga Polecenia wprowadzane przez użytkownika są wyróżniane pogrubieniem dla odróżnienia ich od prezentowanych ewentualnie wyników generowanych przez polecenie.

Tworzenie przypadku testowego Kiedy mamy już PHPUnit, możemy pisać testy dla klasy UserStore. Testy przeznaczone dla każdego pojedynczego komponentu systemu powinny zostać ujęte w pojedynczej klasie rozszerzającej klasę PHPUnit_Framework_TestCase (to jedna z klas udostępnianych przez pakiet PHPUnit). Oto przykładowa minimalna klasa przypadku testowego: require_once 'PHPUnit/Framework/TestCase.php'; class UserStoreTest extends PHPUnit_Framework_TestCase { public function setUp() { } public function tearDown() { } // … }

Klasę testu jednostkowego opatrzyłem nazwą UserStoreTest. Stosowanie nazwy klasy testowanej w nazwie klasy testującej nie jest obowiązkowe, ale właśnie taką konwencję nazewniczą wybiera wielu programistów. Przyjęcie podobnej konwencji nazewniczej wydatnie zwiększa dostępność infrastruktury testowej, zwłaszcza przy wzroście liczby testowanych komponentów. Typową praktyką jest również grupowanie testów w katalogach odwzorowujących strukturę katalogów właściwych klas systemu. Przy utrzymaniu takiej równoległej, logicznej struktury uruchamianie z poziomu wiersza poleceń jest znacznie prostsze: polecenia uruchamiające testy można wprowadzać niemal na ślepo, bez sprawdzania, gdzie leży jaki plik! Każdy test w klasie przypadku testowego jest uruchamiany w izolacji względem innych testów w tej samej klasie. Każda metoda testująca jest poprzedzana wywołaniem metody setUp(), powalającej na ustanowienie stabilnego środowiska testowego dla każdego kolejnego testu. Po uruchomieniu każdej metody testowej wywoływana jest z kolei metoda tearDown(). Jeśli test zmienia środowisko systemu, można w ramach tej metody przywrócić stan sprzed uruchomienia testu. To stabilne środowisko testów, odtwarzane przez metody setUp() i tearDown(), nazwiemy konfiguracją testu (ang. fixture).

384

ROZDZIAŁ 18.  TESTY JEDNOSTKOWE Z PHPUNIT

Do testowania klasy UserStore będziemy potrzebować jej egzemplarza. Możemy go utworzyć w ramach metody setUp() i przypisać go do prywatnej składowej klasy testującej. Utwórzmy też pierwszą metodę testującą: require_once('UserStore.php'); require_once('PHPUnit/Framework/TestCase.php'); class UserStoreTest extends PHPUnit_Framework_TestCase { private $store; public function setUp() { $this->store = new UserStore(); } public function tearDown() { } public function testGetUser() { $this->store->addUser("bob williams", "[email protected]", "12345"); $user = $this->store->getUser("[email protected]"); $this->assertEquals($user['mail'], "[email protected]"); $this->assertEquals($user['name'], "bob williams"); $this->assertEquals($user['pass'], "12345"); } }

Metody testujące powinny być opatrzone nazwami rozpoczynającymi się od przedrostka test i nie powinny przyjmować żadnych argumentów. Klasa testująca podlega bowiem manipulacji za pośrednictwem interfejsu analitycznego Reflection API.  Uwaga Interfejs introspekcji Reflection API omawiany jest dokładniej w rozdziale 5.

Obiekt uruchamiający testy przegląda wszystkie metody w klasie i wywołuje tylko te z nich, które pasują do zadanego wzorca (to znaczy wywołuje tylko metody zaczynające się od „test”). W naszym przykładzie przetestowaliśmy pobieranie informacji o użytkowniku. Nie musieliśmy przy tym tworzyć dla każdego testu egzemplarza UserStore, ponieważ załatwiliśmy to w metodzie setUp(). Jest ona wywoływana przed uruchomieniem każdego z testów, więc możemy mieć pewność, że w każdej metodzie testowej składowa $store będzie zawierała nowo zainicjalizowany obiekt UserStore. W obrębie metody testGetUser() najpierw zasilamy repozytorium użytkowników fikcyjnymi danymi (UserStore::addUser()), a następnie pobieramy dane o nowym użytkowniku i porównujemy je ze źródłem.

Metody asercji Asercja w programowaniu oznacza wyrażenie albo metodę pozwalające na sprawdzenie prawdziwości założeń odnośnie do wybranych aspektów stanu systemu. Stosowanie asercji polega zwykle na zdefiniowaniu oczekiwania, na przykład że w danym miejscu kodu zmienna $cheese powinna mieć wartość "blue" albo że zmienna $pie powinna mieć wartość "apple". Jeśli oczekiwanie nie zostanie potwierdzone, dojdzie do wygenerowania pewnego rodzaju ostrzeżenia. Asercje są dobrym mechanizmem zwiększania bezpieczeństwa systemu; niektóre języki programowania mają wbudowane mechanizmy asercji i pozwalają sterować ich zachowaniem, to znaczy np. wyłączać asercje w kodzie produkcyjnym (przykładem takiego języka jest Java). PHPUnit obsługuje asercje na bazie zestawu metod statycznych. W poprzednim przykładzie użyłem odziedziczonej metody statycznej assertEquals(). Metoda ta porównuje argumenty wywołania, sprawdzając ich równoważność. Jeśli nie są sobie równoważne, metoda testowa zostanie przerwana, a test uznany za niezaliczony. W klasach pochodnych PHPUnit_Framework_TestCase mamy dostęp do całego zestawu metod asercji; niektóre z nich wymienia tabela 18.1.

385

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Tabela 18.1. Metody asercji klasy PHPUnit2_Framework_TestCase Metoda

Opis

assertEquals($w1, $w2, $tol, $komunikat)

Wykrywa błąd, jeśli $w1 jest różne od $w2 o więcej niż $tol.

assertFalse($wyr, $komunikat)

Szacuje wartość wyrażenia $wyr. Wykrywa błąd, jeśli ma ono wartość różną od false.

assertTrue($wyr, $komunikat)

Szacuje wartość wyrażenia $wyr. Wykrywa błąd, jeśli ma ono wartość różną od true.

assertNotNull($w, $komunikat)

Wykrywa błąd, jeśli $w ma wartość null.

assertNull($w, $komunikat)

Wykrywa błąd, jeśli $w ma wartość różną od null.

assertSame($w1, $w2, $komunikat)

Wykrywa błąd, jeśli $w1 i $w2 nie są referencjami tego samego obiektu lub jeśli są zmiennymi o różnych typach albo wartościach.

assertNotSame($w1, $w2, $komunikat)

Wykrywa błąd, jeśli $w1 i $w2 są referencjami do tego samego obiektu lub jeśli są zmiennymi o identycznych typach i wartościach.

assertRegExp($wyrReg, $w, $komunikat)

Wykrywa błąd, jeśli $w nie pasuje do wyrażenia regularnego $wyrReg.

assertType($typ, $w, $komunikat)

Wykrywa błąd, jeśli $w nie jest wartością typu opisywanego przez $type.

assertAttributeSame($w, $atrybut, $klasa, $komunikat)

Wykrywa błąd, jeśli $w ma typ lub wartość różne niż $klasa::$atrybut.

fail($komunikat)

Bezwarunkowy błąd testu.

Testowanie wyjątków Programista skupia się na tym, żeby jego kod działał — i działał dobrze. Często identyczna mentalność przekrada się również do testów, zwłaszcza jeśli testujemy swój własny kod. Pojawia się silna pokusa, żeby testem dowieść poprawności działania kodu; zapominamy, że równie ważne jest testowanie sytuacji błędnych. Bo ile jest warta metoda, która nie sprawdza błędów? Czy metoda zrzuca wyjątek, kiedy trzeba? Czy jest to właściwy wyjątek? Czy metoda po wykryciu błędu w połowie realizowanej operacji porządkuje stan obiektu? Tester powinien sprawdzić również te aspekty poprawności kodu. Na szczęście i tu PHPUnit jest pomocne. Oto test, który sprawdza zachowanie klasy UserStore w przypadku błędu operacji: //... public function testAddUser_ShortPass() { try { $this->store->addUser( "bob williams", "[email protected]", "ff"); $this->fail("Oczekiwany wyjątek zbyt krótkiego hasła"); } catch ( Exception $e ) { } } //...

Cofając się do metody UserStore::addUser(), zobaczymy, że zrzuca ona wyjątek, kiedy próbujemy dodać użytkownika ze zbyt krótkim hasłem (krótszym niż 5 znaków). Nasza próba testowa ma potwierdzić takie zachowanie klasy. Próbujemy dodać użytkownika z hasłem w obrębie klauzuli try. Jeśli metoda addUser() zrzuci oczekiwany wyjątek, sterowanie zostanie przerzucone do klauzuli catch, zgodnie z oczekiwaniem. Jeżeli jednak metoda addUser() nie zrzuci oczekiwanego wyjątku, program dotrze do wywołania metody fail().

386

ROZDZIAŁ 18.  TESTY JEDNOSTKOWE Z PHPUNIT

Innym sposobem przetestowania wyjątków jest skorzystanie z metody asercji setExpectedException(), która przyjmuje nazwę klasy wyjątku oczekiwanego w ramach testu (może to być klasa Exception albo dowolna z jej klas pochodnych). Jeśli metoda testująca zakończy działanie bez zrzucenia odpowiedniego wyjątku, asercja zostanie uznana za niespełnioną i test nie będzie zaliczony. Oto szybka przeróbka poprzedniego testu z użyciem metody asercji oczekiwanego wyjątku: require_once('PHPUnit/Framework/TestCase.php'); require_once('UserStore.php'); class UserStoreTest extends PHPUnit_Framework_TestCase { private $store; public function setUp() { $this->store = new UserStore(); } public function testAddUser_ShortPass() { $this->setExpectedException('Exception'); $this->store->addUser( "bob williams", "[email protected]", "ff"); } }

Uruchamianie zestawów testów Testując klasę UserStore, wypadałoby przetestować również klasę weryfikacji Validator. Oto (okrojona) wersja klasy o nazwie ValidateTest, sprawdzającej metodę Validator::validateUser(): require_once('UserStore.php'); require_once('Validator.php'); require_once('PHPUnit/Framework/TestCase.php'); class ValidatorTest extends PHPUnit_Framework_TestCase { private $validator; public function setUp() { $store = new UserStore(); $store->addUser("bob williams", "[email protected]", "12345"); $this->validator = new Validator($store); } public function tearDown() { } public function testValidate_CorrectPass() { $this->assertTrue($this->validator->validateUser("[email protected]", "12345"), "Spodziewano się poprawności testu"); } }

Skoro mamy już więcej niż jeden przypadek testowy, jak możemy ułatwić sobie ich uruchamianie? Najlepiej byłoby umieścić obie klasy testujące w katalogu o nazwie test. Wtedy moglibyśmy przekazać ten katalog w wywołaniu PHPUnit, a ten uruchomiłby wszystkie testy znalezione w tym katalogu i jego podkatalogach: $ phpunit test/ PHPUnit 3.7.24 by Sebastian Bergmann. .... Time: 104 ms, Memory: 3.75Mb OK (5 tests, 10 assertions)

387

PHP. OBIEKTY, WZORCE, NARZĘDZIA

W większym projekcie należałoby zorganizować testy w odpowiednio rozbudowanej strukturze katalogów — najlepiej, żeby stanowiła odzwierciedlenie struktury katalogów właściwych klas i pakietów projektu. Wtedy można w wywołaniu PHPUnit wskazywać dowolne podkatalogi, ograniczając testy do wybranych komponentów systemu.

Ograniczenia W większości przypadków w testach wystarczające jest stosowanie gotowych asercji z klasy nadrzędnej PHPUnit. Ba, samą metodą asercji assertTrue() można sporo zdziałać. Ale w PHPUnit 3.0 klasa PHPUnit_Framework_TestCase zawiera zestaw metod wytwórczych zwracających obiekty PHPUnit_Framework_Constraint. Obiekty te można ze sobą komponować i przekazywać do PHPUnit_Framework_TestCase::assertThat(); w ten sposób można konstruować własne asercje. Pora na krótki przykład. Nasz obiekt UserStore nie powinien dopuszczać do wprowadzania duplikatów adresów e-mail. Oto test, który sprawdza skuteczność tego zabezpieczenia: class UserStoreTest extends PHPUnit_Framework_TestCase { private $store; public function setUp() { $this->store = new UserStore(); } //.... public function testAddUser_duplicate() { try { $ret = $this->store ->addUser("bob williams", "[email protected]", "123456"); $ret = $this->store ->addUser("bob stevens", "[email protected]", "123456"); self::fail("Oczekiwany wyjątek"); } catch (Exception $e) { $const = $this->logicalAnd( $this->logicalNot($this->contains("bob stevens")), $this->isType('array')); self::assertThat( $this->store->getUser("[email protected]"), $const); } }

W tym teście dodajemy do UserStore użytkownika, a potem drugiego, z inną nazwą, ale tym samym adresem e-mail. Test ma więc potwierdzać zrzucenie wyjątku dla przypadku duplikatu adresu e-mail w wywołaniu metody addUser() W bloku catch za pomocą metod udostępnionych w klasie nadrzędnej PHPUnit montujemy obiekt ograniczenia, czyli obiekt typu PHPUnit_Framework_Constraint. Spróbujmy przeanalizować sposób zmontowania ograniczenia: $this->contains("bob stevens")

To wywołanie zwróci obiekt PHPUnit_Framework_Constraint_TraversableContains. Obiekt ten przekazany do assertThat() sprowokuje błąd, jeśli podmiot testu nie zawiera elementu pasującego do podanej wartości ("bob stevens"). Ale wynik tego testu negujemy, przekazując ograniczenie do kolejnego: PHPUnit_Framework_Constraint_Not (negacja). Ponownie używamy metody pomocniczej dostępnej w klasie TestCase (a konkretnie w klasie nadrzędnej Assert): $this->logicalNot($this->contains("bob stevens"))

Teraz asercja assertThat() zostanie uznana za niespełnioną, kiedy wartość testowana zawiera element pasujący do ciągu "bob stevens". W ten sposób można zmontować z ograniczeń całkiem złożone struktury logiczne. Ostateczną wersję naszego ograniczenia można opisać słownie: »w porządku, jeśli wartość testowana

388

ROZDZIAŁ 18.  TESTY JEDNOSTKOWE Z PHPUNIT

jest tablicą i nie zawiera ciągu "bob stevens"«. Konfrontacja ograniczenia z testowaną wartością następuje poprzez przekazanie obu do wywołania metody asercji assertThat(). Rzecz jasna podobny efekt można by osiągnąć za pośrednictwem standardowych metod asercji, ale ograniczenia mają kilka istotnych przewag. Mianowicie, można z nich komponować przejrzyste bloki logiczne, z jasnymi powiązaniami pomiędzy komponentami (choć aby taka konstrukcja była czytelna, warto zadbać o odpowiednie formatowanie kodu). A co więcej, raz zmontowane ograniczenie można wykorzystywać wielokrotnie. Możemy więc przygotować sobie w klasie testowej całą bibliotekę skomplikowanych ograniczeń i odwoływać się do nich w poszczególnych testach — a nawet łączyć je tam w jeszcze bardziej złożone ograniczenia: $const = $this->logicalAnd($a_complex_constraint, $another_complex_constraint);

Tabela 18.2 wymienia niektóre z metod do tworzenia ograniczeń dostępne w klasie TestCase. Tabela 18.2. Some Constraint Methods Metoda

Ograniczenie niespełnione, chyba że…

greaterThan($liczba)

Sprawdzana wartość jest większa niż $liczba.

contains($w)

Sprawdzana wartość (kolekcja) zawiera element pasujący do $w.

identicalTo($w)

Sprawdzana wartość jest referencją do tego samego obiektu, co $w, lub zmienną tego samego typu i o tej samej wartości.

greaterThanOrEqual($liczba)

Sprawdzana wartość jest nie mniejsza niż $liczba.

lessThan($liczba)

Sprawdzana wartość jest mniejsza niż $liczba.

lessThanOrEqual($liczba)

Sprawdzana wartość jest nie większa niż $liczba.

equalTo($w, $delta=0, $gl=10)

Sprawdzana wartość jest równa $w. Jeśli podano, $delta określa margines błędu porównania (dla porównań liczbowych), a $gl określa głębokość porównań rekurencyjnych dla obiektów i tablic.

stringContains($ciag, $wielkoscliter=true)

Sprawdzana wartość zawiera $ciag. Domyślnie dopasowanie odbywa się z uwzględnieniem wielkości liter.

matchesRegularExpression($wyrReg)

Sprawdzana wartość pasuje do wyrażenia regularnego $wyrReg.

logicalAnd( PHPUnit_Framework_Constraint $ogr1 [, $ogrN..])

Wszystkie przekazane ograniczenia zostały spełnione (iloczyn logiczny ograniczeń).

logicalOr( PHPUnit_Framework_Constraint $ogr1, [, $ogrN..])

Przynajmniej jedno z przekazanych ograniczeń zostało spełnione (suma logiczna ograniczeń).

logicalNot( PHPUnit_Framework_Constraint $ogr)

Przekazane ograniczenie nie zostało spełnione (negacja ograniczenia).

Atrapy i imitacje Testy jednostkowe mają na celu sprawdzenie działania komponentu w możliwie ścisłej izolacji od reszty systemu. Ale mało który komponent działa w próżni. Nawet przy elegancko rozdzielonych zadaniach klasy muszą korzystać z innych obiektów, choćby w postaci argumentów wywołań metod danej klasy. Wiele klas operuje wprost na bazie danych czy systemie plików i nie sposób ich z tego środowiska wyizolować, bo to sedno ich działalności. Wiemy już, jak można sobie z tym poradzić. Do zarządzania środowiskiem testu (do którego możemy zaliczyć np. połączenia z bazą danych, miejsce na dysku i inne zasoby zewnętrzne wobec klasy, zainicjalizowane i gotowe do użycia) można użyć metody setUp() i tearDown().

389

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Inny sposób polega na fałszowaniu kontekstu wykonawczego klasy. Chodzi o utworzenie obiektów udających zasoby i obiekty wykonawcze, z którymi normalnie współpracuje testowana klasa. Na przykład do konstruktora obiektów możemy przekazać fałszywy obiekt odwzorowania do bazy danych. Ów fałszywy obiekt będzie tego samego typu, co prawdziwy obiekt odwzorowania (będzie rozszerzał tę samą klasę bazową albo nawet przesłaniał faktyczną klasę odwzorowania), testowana klasa da się w ten sposób oszukać. Taką fałszywkę można nawet wypełnić prawdziwymi danymi. Komplet obiektów realizujących środowisko wykonawcze nazwiemy imitacją. Imitacje mogą być przydatne, bo pozwalają skoncentrować test na pojedynczej klasie bez równoczesnego, niekoniecznie pożądanego testowania pozostałych komponentów systemu (które na przykład mogą jeszcze nie być gotowe). Fałszywe obiekty można zresztą podnieść na wyższy poziom; ponieważ obiekt testowany najpewniej będzie w jakiś sposób odwoływał się do podstawionych mu fałszywek, możemy rejestrować obecność takich odwołań i porównywać je z oczekiwanymi. Obiekt stanowiący imitację otoczenia może więc pełnić rolę śledczą i ujawniać wewnętrzne zachowanie obiektu testowanego — w ten sposób możemy weryfikować zachowanie testowanego komponentu. I ten aspekt będzie odróżniał imitacje od atrap. Atrapy buduje się, pisząc klasy, które zwracają wartości zakodowane na sztywno i raportują wywołania swoich metod. Jest to bardzo proste, ale bywa czasochłonne. PHPUnit daje dostęp do rozwiązania łatwiejszego i bardziej dynamicznego: może generować obiekty atrap „w locie”. Polega to na analizowaniu klasy kandydującej do roli atrapy i zmontowaniu klasy pochodnej, która przesłania metody właściwej klasy. Po wygenerowaniu obiektu takiej klasy można wywoływać jego metody, uzupełniając je o dane i warunki powodzenia operacji. Sprawdźmy teorię na przykładzie. Klasa UserStore zawiera metodę o nazwie notifyPasswordFailure(), która ustawia pole dla danego użytkownika. Metoda ta powinna być wołana przez obiekt Validator przy nieudanej próbie ustawienia hasła. Zmontujemy więc atrapę klasy UserStore tak, aby zarówno udostępniała dane dla obiektu Validator, jak i potwierdzała oczekiwane wywołanie swojej metody notifyPasswordFailure(): class ValidatorTest extends PHPUnit_Framework_TestCase { //... public function testValidate_FalsePass() { $store = $this->getMock("UserStore"); $this->validator = new Validator($store); $store->expects($this->once()) ->method('notifyPasswordFailure') ->with($this->equalTo('[email protected]')); $store->expects($this->any()) ->method("getUser") ->will($this->returnValue( array("name"=>"[email protected]", "pass"=>"right"))); $this->validator->validateUser( "[email protected]", "wrong"); } }

Wygodne obiekty atrap posiadają interfejs kaskadowy, to znaczy taki, który umożliwia wywołania kolejnych metod na rzecz wartości zwracanych z metod poprzednich. Taki interfejs łatwiej docenić w praktyce, niż opisać jego zalety. Kaskada wywołań stanowi bardzo czytelny rejestr operacji, czytany od lewej do prawej; konstrukcja wywołań jest wtedy bardzo wygodna, gorzej z diagnostyką błędów (w kaskadzie nie ma miejsca na sprawdzanie błędów pomiędzy wywołaniami). W poprzednim przykładzie wywołaliśmy metodę getMock() klasy PHPUnit_Framework_TestCase, przekazując do niej ciąg "UserStore", czyli nazwę klasy, której zamierzamy użyć w roli atrapy. Metoda getMock() dynamicznie generuje klasę i tworzy jej obiekt. Zachowujemy go w zmiennej $store i przekazujemy do testowanego obiektu Validator. Nie dojdzie do błędu, ponieważ nowo wygenerowany obiekt jest typu pochodnego względem UserStore. Udało się nam więc oszukać Validator, podsuwając mu atrapę na przeszpiegi.

390

ROZDZIAŁ 18.  TESTY JEDNOSTKOWE Z PHPUNIT

Obiekty atrap generowane przez PHPUnit posiadają metodę expects(). Metoda ta wymaga obiektu dopasowania (powinien być to obiekt typu PHPUnit_Framework_MockObject_Matcher_Invocation, na szczęście metody pomocnicze klasy TestCase pozwalają na łatwe wygenerowanie takich obiektów). Obiekt dopasowania definiuje oczekiwane zachowanie obiektu testowanego względem atrapy, to znaczy określa oczekiwaną liczbę wywołań metody atrapy. Metody wytwórcze obiektów dopasowań dostępne w klasie testCase wymienia tabela 18.3. Tabela 18.3. Some Matcher Methods Metoda

Dopasowanie nieudane, chyba że…

any()

Badana metoda zostanie wywołana dowolną liczbę razy (albo nie zostanie wywołana wcale); przydatne w przypadku imitacji, które zwracają wartości, ale nie weryfikują wywołań.

never()

Nie dojdzie do żadnego wywołania badanej metody.

atLeastOnce()

Dojdzie do co najmniej jednego wywołania badanej metody.

once()

Dojdzie do jednokrotnego wywołania badanej metody.

exactly($liczba)

Dojdzie do $liczba wywołań badanej metody.

at($liczba)

Badana metoda zostanie wywołana jako $liczba z kolei (każde wywołanie metody w atrapie jest rejestrowane i indeksowane).

Po zdefiniowaniu oczekiwania co do wywołań i utworzeniu obiektu dopasowania trzeba określić badaną metodę. Metoda expects() zwraca obiekt (typu PHPUnit_Framework_MockObject_Builder_InvocationMocker, jeśli ktoś koniecznie chce wiedzieć) posiadający metodę o nazwie method(). To wystarczy, żeby zainicjować śledzenie: $store = $this->getMock("UserStore"); $store->expects($this->once()) ->method('notifyPasswordFailure');

Pójdziemy jednak dalej i zweryfikujemy również parametry wywołania badanej metody notifyPasswordFailure(). Metoda InvocationMocker::method() zwraca egzemplarz obiektu, na rzecz którego nastąpiło wywołanie. Klasa InvocationMocker zawiera też metodę o nazwie with(), przyjmującą zmienną liczbę argumentów określających dopasowanie. Przyjmuje też obiekty ograniczeń, dzięki czemu parametry wywołań badanych metod można sprawdzać także zakresowo. Z takim oprzyrządowaniem możemy dokończyć instrukcję i zapewnić sprawdzanie obecności w wywołaniu notifyPasswordFailure() również kompletu oczekiwanych argumentów. $store->expects($this->once()) ->method('notifyPasswordFailure') ->with($this->equalTo('[email protected]'));

Nie trzeba już chyba tłumaczyć idei interfejsu kaskadowego. Wywołania kaskadowe czyta się niemal jak zdania w języku naturalnym: „obiekt $store oczekuje pojedynczego wywołania metody notifyPasswordFailure() z argumentem '[email protected]'”. Do wywołania metody with() przekazaliśmy obiekt ograniczenia. Zasadniczo nie jest to konieczne — wewnętrznie wszelkie argumenty zostaną i tak zamienione na ciągi znaków, równie dobrze moglibyśmy więc napisać: $store->expects($this->once()) ->method('notifyPasswordFailure') ->with('[email protected]');

Niekiedy atrap generowanych przez PHPUnit chcemy użyć jedynie w roli imitacji, to znaczy obiektów zwracających wartości, bez rejestrowania i weryfikowania wywołań. W takich przypadkach użyjemy metody InvocationMocker::will() na rzecz wartości zwróconej z method(). Metoda will() wymaga określenia wartości zwracanej z metody obiektu imitacji (albo wartości zwracanych, jeśli badana metoda ma być wywoływana wielokrotnie). Wartość zwracaną imitacji określa się albo poprzez metodę TestCase::returnValue()

391

PHP. OBIEKTY, WZORCE, NARZĘDZIA

(dla pojedynczej wartości zwracanej), albo poprzez metodę TestCase::onConsecutiveCalls() (dla wielokrotnych wywołań). I znów łatwiej tego użyć niż to opisać. Oto fragment poprzedniego przykładu, w którym odbywa się ustalenie wartości zwracanej przez metodę imitacji UserStore: $store->expects($this->any()) ->method("getUser") ->will($this->returnValue( array("name"=>"bob williams", "mail"=>"[email protected]", "pass"=>"right")));

Atrapa UserStore została przygotowana na dowolną liczbę wywołań getUser() — obecnie chodzi nam tylko o udostępnienie danych do przeprowadzenia testu klasy Validator, nie zamierzamy weryfikować liczby i przebiegu odwołań do UserStore. Następnie wywołujemy metodę will() z wartością zwróconą z wywołania metody TestCase::returnValue() dla danych, które metoda ma zwrócić (obiekt zwracany z returnValue() to obiekt klasy PHPUnit_Framework_MockObject_Stub_Return; może i warto to wiedzieć, chociaż znacznie wygodniej skorzystać z metod wytwórczych udostępnionych w klasie TestCase). Alternatywnie można by do metody will() przekazać wynik wywołania TestCase::onConsecutiveCalls(). Metoda onConsecutiveCalls() przyjmuje dowolną liczbę parametrów, które będą kolejno zwracane w kolejnych wywołaniach imitowanej metody.

Dobry test to oblany test Zgodzimy się, że testowanie to zacna rzecz, ale prawdziwą miłością do testów można zapałać dopiero po tym, kiedy testy uratują nas raz i drugi przed wpadką produkcyjną. Spróbujmy zasymulować sytuację, w której zmiana w jednej części systemu ma nieoczekiwany negatywny wpływ na inny komponent. Klasa UserStore jest już w użyciu od jakiegoś czasu, ale na etapie przeglądu kodu okazało się, że byłoby lepiej, gdyby zamiast tablic asocjacyjnych generowała obiekty klasy User. Mamy więc nową wersję klasy: class UserStore { private $users = array(); function addUser($name, $mail, $pass) { if (isset($this->users[$mail])) { throw new Exception( "Konto {$mail} już istnieje w systemie"); } $this->users[$mail] = new User($name, $mail, $pass); return true; } function notifyPasswordFailure($mail) { if (isset($this->users[$mail])) { $this->users[$mail]->failed(time()); } } function getUser($mail) { if (isset($this->users[$mail])) { return ($this->users[$mail]); } return null; } }

Oto i nasza klasa User: class User {

392

ROZDZIAŁ 18.  TESTY JEDNOSTKOWE Z PHPUNIT

private private private private

$name; $mail; $pass; $failed;

function __construct($name, $mail, $pass) { if (strlen($pass) < 5) { throw new Exception( "Hasło musi mieć co najmniej 5 znaków"); } $this->name = $name; $this->mail = $mail; $this->pass = $pass; } function getName() { return $this->name; } function getMail() { return $this->mail; } function getPass() { return $this->pass; } function failed($time) { $this->failed = $time; } }

Rzecz jasna klasa UserStoreTest również musi zostać odpowiednio zmodyfikowana. Niniejszym kod przystosowany do pracy na tablicach: public function testGetUser() { $this->store->addUser("bob williams", "[email protected]", "12345"); $user = $this->store->getUser("[email protected]"); $this->assertEquals($user['mail'], "[email protected]"); // ...

zamieniamy na operujący na obiektach: public function testGetUser() { $this->store->addUser("bob williams", "[email protected]", "12345"); $user = $this->store->getUser("[email protected]"); $this->assertEquals($user->getMail(), "[email protected]"); // ...

Kiedy przyjdzie do uruchamiania testów, otrzymamy wreszcie pożytek z całej tej infrastruktury: ostrzeżenie, że kod nie jest jeszcze gotowy: $ phpunit test/ PHPUnit 3.7.24 by Sebastian Bergmann. ...FF Time: 359 ms, Memory: 3.75Mb There were 2 failures:

393

PHP. OBIEKTY, WZORCE, NARZĘDZIA

1) ValidatorTest::testValidate_CorrectPass Spodziewano się poprawności testu Failed asserting that false is true. /.../test/ValidatorTest.php:22 2) ValidatorTest::testValidate_FalsePass Expectation failed for method name is equal to when invoked 1 time(s). Method was expected to be called 1 times, actually called 0 times. FAILURES! Tests: 6, Assertions: 5, Failures: 2.

Wywołujemy tu metodę getUser() obiektu UserStore. Z tym że obecnie zamiast tablicy ta metoda zwraca obiekt; co gorsza, metoda validateUser() nie generuje żadnego ostrzeżenia. To dlatego, że pierwotnie wywołanie getUser() zwracało tablicę albo wartość pustą w przypadku błędu i poprawność wartości zwracanej była testowana funkcją is_array(). Obecnie getUser() zwraca obiekt, więc metoda validateUser() zawsze zwróci false. Gdyby nie framework testów, Validator zwyczajnie odrzucałby wszystkie konta jako niepoprawne, a administratorzy aplikacji nie otrzymaliby nawet śladu ostrzeżenia o tym, że coś idzie nie tak. Wyobraźmy sobie efekt wdrożenia takiej (drobnej przecież) zmiany bez testów w piątek wieczorem. Co za groza odebrać w restauracji, pubie czy na własnej kanapie telefon z pretensjami: „Coś ty narobił! Zablokowało nam wszystkich klientów!”. Najbardziej wredne błędy wcale nie prowokują ostrzeżeń i błędów interpretera. Ukrywają się w zupełnie poprawnym (składniowo) kodzie i po cichu „rozwalają” logikę aplikacji. Wiele błędów w ogóle nie manifestuje się przy naszej pracy; są tu, ale ich wpływ objawia się zupełnie gdzie indziej, niekiedy dni albo i tygodnie później. Framework testowy może pozwolić wychwycić przynajmniej część z nich — lepiej możliwie wcześnie zapobiegać krzepnięciu błędów w systemie, niż je potem stamtąd wykorzeniać. Warto więc pisać testy wraz z właściwym kodem i warto uruchamiać je jak najczęściej. Jeśli ktoś zgłasza błąd, najpierw dopisz do frameworku test, który może go potwierdzić, a dopiero potem zaaplikuj poprawkę i sprawdź, czy test jest teraz zaliczany. Błędy mają zabawny zwyczaj powracania z czasem; pisanie testów przed wprowadzeniem poprawki pozwala zabezpieczyć się przed przyszłym powracaniem błędu — takie zabezpieczenie określa się mianem testów regresyjnych. Przy okazji: jeśli do testów regresyjnych wydzielimy osobny katalog, testy w nim umieszczane warto opatrywać znaczącymi nazwami; w jednym z projektów mój zespół postanowił nazywać testy za numerami zgłoszeń z Bugzilli, więc po jakimś czasie dopracowaliśmy się 400 plików testów regresyjnych o nazwach w rodzaju test_973892.php. Odszukanie konkretnego było doprawdy żmudne!

Testy dla aplikacji WWW Każda aplikacja WWW powinna być przygotowana tak, żeby dała się wywołać również z poziomu wiersza poleceń albo innego interfejsu. W rozdziale 12. omawiane były pewne techniki pozwalające na osiągnięcie tej elastyczności. Zwłaszcza jeśli tworzymy klasę Request ujmującą żądanie protokołu HTTP, to obiekt tej klasy możemy równie dobrze zainicjować wartościami parametrów żądania, jak i wypełnić na podstawie listy zmiennych albo argumentów wywołania komponentu w wierszu poleceń. A po zainicjalizowaniu żądania system powinien przetworzyć je niezależnie od kontekstu wykonania. Jeśli dany system nie daje się łatwo uruchomić w różnych kontekstach, można podejrzewać błąd projektowy. Na przykład jeśli w komponentach zaszyte są liczne ścieżki dostępu, najprawdopodobniej mamy do czynienia z nadmiernym sprzężeniem komponentów. Należałoby rozważyć przeniesienie elementów wiążących komponenty z kontekstem do obiektów hermetyzujących zależności, które można pozyskać z repozytorium centralnego względem aplikacji. Przydaje się tu wzorzec Registry, również omawiany w rozdziale 12. Kiedy już system będzie mógł być uruchamiany bezpośrednio, okaże się, że pisanie wysokopoziomowych testów aplikacji WWW jest stosunkowo proste i obywa się bez dodatkowych narzędzi.

394

ROZDZIAŁ 18.  TESTY JEDNOSTKOWE Z PHPUNIT

Niekiedy jednak nawet najlepiej przemyślany projekt będzie wymagał mniejszej bądź większej refaktoryzacji celem dostosowania do testowania. Z mojego doświadczenia wynika, że niemal zawsze owocuje to ulepszeniem samego projektu. Zademonstruję to na przykładzie WOO zapożyczonym z rozdziałów 12. i 13., przystosowując tę aplikację do testów jednostkowych.

Przygotowanie aplikacji WWW do testów Przykład WOO pozostawiliśmy w dość dobrym stanie, przynajmniej z punktu widzenia testera. System używa bowiem pojedynczego kontrolera Front Controller i posiada prosty interfejs. Mamy też prosty plik uruchamiający Runner.php: require_once( "woo/controller/Controller.php"); \woo\controller\Controller::run();

Test jednostkowy da się łatwo dopisać, prawda? Ale co z argumentami wywołania? Do pewnego stopnia są one już obsługiwane w klasie Request: // klasa \woo\controller\Request function init() { if (isset($_SERVER['REQUEST_METHOD'])) { $this->properties = $_REQUEST; return; } foreach($_SERVER['argv'] as $arg) { if (strpos( $arg, '=' )) { list($key, $val)=explode("=", $arg); $this->setProperty($key, $val); } } }

Metoda init() wykrywa, czy żądanie zostało zainicjowane w kontekście serwera, i odpowiednio wypełnia tablicę $properties (albo bezpośrednio, albo za pośrednictwem setProperty()). Zadziała to również dla wywołania z wiersza poleceń, a więc możemy żądanie uruchomić tak: $ php runner.php cmd=AddVenue venue_name=bob

i otrzymać w odpowiedzi:

Dodaj miejsce dla bob

Dodaj miejsce dla 'bob'
dodano 'bob' (5)
Proszę podać nazwę dla miejsca
[dodaj miejsce]





395

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Działamy z wiersza polecenia, ale wciąż trudno przekazać argumenty za pośrednictwem wywołania metody. Rozwiązaniem (nieeleganckim) byłoby ręczne ustawienie tablicy $argv przed wywołaniem metody run() kontrolera. Niespecjalnie mi się to podoba: manipulowanie tablicami systemu to zły pomysł, tak jak i manipulowanie gołymi ciągami wymagane po obu stronach. Przyglądając się bliżej klasie Controller, dostrzeżemy jednak decyzję projektową, która będzie tu pomocna: function handleRequest() { $request = \woo\base\ApplicationRegistry::getRequest(); $app_c = \woo\base\ApplicationRegistry::appController(); while($cmd = $app_c->getCommand($request)) { $cmd->execute($request); } //\woo\domain\ObjectWatcher::instance()->performOperations(); $this->invokeView($app_c->getView($request)); }

Metoda została zaprojektowana do wywołania z poziomu statycznej metody run(). Zauważmy, że obiekt klasy Request nie jest tworzony wprost, ale za pośrednictwem rejestru ApplicationRegistry. Podczas gdy rejestr przechowuje wewnętrznie pojedynczą instancję obiektu w rodzaju Request, możemy pozyskać referencję do tego obiektu i załadować go danymi testowymi jeszcze przed uruchomieniem systemu poprzez wywołanie kontrolera. W ten sposób możemy symulować zawartość testowanego żądania do aplikacji WWW. Ponieważ nasz system wykorzystuje obiekt Request wyłącznie w roli interfejsu do faktycznego żądania HTTP, jest niezależny od faktycznego źródła danych. I dopóki zawartość obiektu Request jest w miarę poprawna, dla systemu nie ma znaczenia to, czy jego zawartość pochodzi ze strumienia HTTP serwera WWW, czy ze spreparowanych danych testowych. Pozyskiwanie obiektów z rejestru uważam przez to za sposób właściwy i stosuję wszędzie, gdzie się da. Dzięki temu później, po rozszerzeniu implementacji statycznej metody wytwórczej (tutaj jest nią ApplicationRegistry::instance()), mogę tworzyć rejestry wypełnione sztucznymi komponentami np. w zależności od ustawienia znacznika, co zdecydowanie upraszcza tworzenie całkowicie sztucznego środowiska wykonawczego. Wspaniale jest móc „oszukać” system. Tutaj zapoznamy się jednak z bardziej konserwatywną sztuczką, a mianowicie załadowaniem obiektu Request danymi testowymi.

Proste testy aplikacji WWW Oto przypadek testowy realizujący podstawowy test systemu WOO: class AddVenueTest extends PHPUnit_Framework_TestCase { function testAddVenueVanilla() { $this->runCommand( "AddVenue", array("venue_name"=>"bob")); } function runCommand($command=null, array $args=null) { $request = \woo\base\ApplicationRegistry::getRequest(); if (! is_null($args)) { foreach($args as $key=>$val) { $request->setProperty($key, $val); } } if (! is_null($command)) { $request->setProperty('cmd', $command); } woo\controller\Controller::run(); } }

396

ROZDZIAŁ 18.  TESTY JEDNOSTKOWE Z PHPUNIT

W istocie test ten nie sprawdza niczego, dowodząc raczej samej możliwości wywołania systemu. Faktyczna praca odbywa się w obrębie metody runCommand(). Sam test jest wybitnie prosty: pozyskujemy obiekt żądania z rejestru RequestRegistry i wypełniamy go kluczami i wartościami podanymi w wywołaniu metody testującej. Ponieważ kontroler będzie sięgał po obiekty żądań do tego samego źródła, mamy pewność, że będzie operował na tych samych danych, które tu ustawiamy. Uruchomienie testu potwierdza, że wszystko jest w porządku. Widzimy wyjście, którego oczekiwaliśmy. Problem w tym, że wyjście jest wypisywane przez widok, i bez interpretera HTML ciężko je odczytać. Można to całkiem łatwo poprawić, buforując wyjście: class AddVenueTest extends PHPUnit_Framework_TestCase { function testAddVenueVanilla() { $output = $this->runCommand( "AddVenue", array("venue_name"=>"bob")); self::assertRegexp("/dodano/", $output); } function runCommand( $command=null, array $args=null ) { ob_start(); $request = \woo\base\ApplicationRegistry::getRequest(); if ( ! is_null( $args ) ) { foreach( $args as $key=>$val ) { $request->setProperty( $key, $val ); } } if ( ! is_null( $command ) ) { $request->setProperty( 'cmd', $command ); } woo\controller\Controller::run(); $ret = ob_get_contents(); ob_end_clean(); return $ret; } }

Przechwytując wyjście do bufora, możemy zwrócić je z metody runCommand(). Na wartości zwracanej mamy prostą asercję sprawdzającą skuteczność obsługi żądania na bazie zawartości komunikatu w buforze wyjściowym. Oto przebieg testu z poziomu wiersza poleceń: $ phpunit test/AddVenueTest.php PHPUnit 3.7.24 by Sebastian Bergmann. Time: 215 ms, Memory: 3.25Mb OK (1 test, 1 assertion)

Jeśli zamierzasz przeprowadzać większą liczbę testów, takie podejście sprawdza się znacznie lepiej niż ręczne przeglądanie wyjścia z widoku. A jeszcze lepszym pomysłem będzie utworzenie klasy nadrzędnej interfejsu WWW. Oczywiście pomijamy tu liczne szczegóły, którym przyjdzie stawić czoła przy własnych testach. Między innymi trzeba zapewnić, żeby system działał z konfigurowalną pamięcią trwałą: nie chcemy przeprowadzać testów na tym samym repozytorium danych, którego używamy programistycznie (lub produkcyjnie!). To kolejna okazja do polepszenia projektu systemu: szukajmy zakodowanych na sztywno ścieżek dostępu i ciągów połączeniowych i przenieśmy je do rejestru; wartości tych elementów konfiguracyjnych będziemy mogli potem ustawiać w ramach metod setUp() i tearDown(). Warto też pomyśleć o utworzeniu atrapy całego rejestru (MockRequestRegistry), którą można będzie zasilić atrapami, imitacjami i innymi fałszywkami.

397

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Tego rodzaju metodologia sprawdza się dobrze przy testowaniu wejścia i wyjścia aplikacji WWW. Ma jednak swoje ograniczenia: tą metodą nie sprawdzimy zachowania przeglądarki. Tam, gdzie aplikacja korzysta z technologii JavaScript, Ajax i innych mechanizmów strony klienta, weryfikowanie tekstu generowanego nie jest jeszcze dowodem na to, że użytkownik widzi sensowny obraz interfejsu aplikacji. Ale i w tym przypadku jest rozwiązanie.

Selenium Selenium (http://seleniumhq.org/) składa się z zestawu poleceń do definiowania testów WWW. Udostępnia także narzędzia i interfejsy (API) do tworzenia i uruchamiania testów przeglądarkowych. W tym skrótowym wprowadzeniu utworzymy szybko test dla systemu WOO utworzonego w rozdziale 12. Test będzie współdziałał z serwerem Selenium za pośrednictwem interfejsu o nazwie php-webdriver.

Skąd wziąć Selenium Komponenty Selenium można pobrać spod adresu http://seleniumhq.org/download/. Na potrzeby niniejszego przykładu będziemy musieli zainstalować komponent Selenium Server. Po pobraniu pakietu powinieneś mieć plik o nazwie selenium-server-standalone-2.42.2.jar (numer wersji zapewne będzie się różnił). Plik należy skopiować do jakiegoś centralnego katalogu. Potrzebne będzie jeszcze środowisko wykonawcze języka Java. Po zainstalowaniu Javy można już uruchomić serwer Selenium. U siebie skopiowałem archiwum JAR serwera do katalogu /usr/local/lib. Następnie uruchomiłem serwer: $ cp selenium-server-standalone-2.35.0.jar /usr/local/lib/ $ java -jar /usr/local/lib/selenium-server-standalone-2.35.0.jar 2014-06-17 09:54:55 org.openqa.grid.selenium.GridLauncher main INFO: Launching a standalone server 09:54:55.912 INFO - Java: Sun Microsystems Inc. 23.25-b01 09:54:55.914 INFO - OS: Linux 3.11.0-23-generic amd64 09:54:55.944 INFO - v2.42.2, with Core v2.42.2. Built from revision 6a6995d 09:54:56.068 INFO - Default driver org.openqa.selenium.ie.InternetExplorerDriver registration is skipped: registration capabilities Capabilities [{platform=WINDOWS, ensureCleanSession=true, browserName=internet explorer, version=}] does not match with current platform: LINUX 09:54:56.150 INFO - RemoteWebDriver instances should connect to: http://127.0.0.1:4444/wd/hub 09:54:56.152 INFO - Version Jetty/5.1.x 09:54:56.154 INFO - Started HttpContext[/selenium-server/driver,/selenium-server/driver] 09:54:56.155 INFO - Started HttpContext[/selenium-server,/selenium-server] 09:54:56.155 INFO - Started HttpContext[/,/] 09:54:56.195 INFO - Started org.openqa.jetty.jetty.servlet.ServletHandler@1ae4b3b8 09:54:56.195 INFO - Started HttpContext[/wd,/wd] 09:54:56.202 INFO - Started SocketListener on 0.0.0.0:4444 09:54:56.202 INFO - Started org.openqa.jetty.jetty.Server@1c7783ea

Zauważ, że w komunikatach startowych podany jest URL, z którego powinniśmy korzystać przy komunikacji z serwerem; przyda się to w dalszej części ćwiczenia. Można teraz iść dalej.

PHPUnit i Selenium Co prawda PHPUnit udostępnia interfejsy do współpracy z Selenium, jednak natura tej współpracy jest (przynajmniej w czasie przygotowywania tego wydania) nieco chwiejna. Integracja jest stabilna i solidna w zakresie funkcjonalności definiowanych przez wersję Selenium 1. Tymczasem nasz serwer, co widać w poprzednim punkcie, jest w wersji 2.42. Oznacza to, że (domyślnie) PHPUnit integruje się w pełni jedynie z podzbiorem funkcji nowego wydania Selenium.

398

ROZDZIAŁ 18.  TESTY JEDNOSTKOWE Z PHPUNIT

PHPUnit realizuje częściową obsługę interfejsu WebDriver API (interfejsu do pełnego środowiska testowego Selenium 2) za pośrednictwem klasy PHPUnit_Extensions_Selenium2TestCase. Niestety, kod ten nie działa obecnie w systemie Linux, co stanowi istotną przeszkodę dla przeprowadzania wielu zautomatyzowanych testów (nie mówiąc już o systemach produkcyjnych) działających na tej platformie. Aby skutecznie skorzystać z możliwie wielu mechanizmów Selenium, najlepiej użyć PHPUnit w połączeniu z oprzyrządowaniem zapewniającym potrzebne interfejsy do Selenium.

php-webdriver WebDriver to mechanizm, za pośrednictwem którego Selenium kontroluje przeglądarki, wprowadzony w Selenium 2. Programiści Selenium udostępnili interfejsy tego mechanizmu dla języków Java, Python i C#. Istnieje też kilka realizacji dla języka PHP. Osobiście używałem php-webdriver, autorstwa programistów serwisu Facebook. Implementacja ta jest porządnie utrzymywana i odzwierciedla całość oficjalnych interfejsów Selenium. Projekt php-webdriver jest prowadzony w serwisie GitHub, a żeby go pobrać, należy sklonować repozytorium projektu: $ git clone git://github.com/facebook/php-webdriver.git

W miejscu wywołania polecenia powstanie katalog o nazwie php-webdriver. Wystarczy ten katalog umieścić w systemowej ścieżce przeszukiwania.

Tworzenie szkieletu testu Będziemy pracować z instalacją aplikacji WOO dostępnej w systemie lokalnym pod adresem http://localhost/webwoo. Zaczniemy od szablonu pustej klasy testowej: require_once('PHPUnit/Framework/TestCase.php'); require_once( "php-webdriver/lib/__init__.php" ); class seleniumtest extends PHPUnit_Framework_TestCase { protected function setUp() { } public function testAddVenue() { } }

Jak zwykle do kodu włączamy plik PHPUnitFramework/TestCase.php, a także plik php-webdriver/lib/__init__.php, który z kolei włącza inne pliki potrzebne w API Selenium. Teraz możemy potwierdzić, że test się uruchamia. $ phpunit seleniumtest.php PHPUnit 3.7.24 by Sebastian Bergmann. Time: 7 ms, Memory: 3.50Mb OK (1 test, 0 assertions)

Wygląda dobrze, czas więc na jakiś faktyczny test.

Połączenie z serwerem Selenium Pamiętajmy, że w komunikatach startowych Selenium podany był adres połączeniowy serwera. Aby nawiązać połączenie z serwerem Selenium, powinniśmy przekazać ten adres wraz z tablicą parametrów konfiguracyjnych do konstruktora klasy o nazwie RemoteWebDriver:

399

PHP. OBIEKTY, WZORCE, NARZĘDZIA

require_once('PHPUnit/Framework/TestCase.php'); require_once( "php-webdriver/lib/__init__.php" ); class seleniumtest extends PHPUnit_Framework_TestCase { protected function setUp() { $host = " http://127.0.0.1:4444/wd/hub "; $capabilities = array(WebDriverCapabilityType::BROWSER_NAME => 'firefox'); $this->driver = new RemoteWebDriver($host, $capabilities); } public function testAddVenue() { } }

Pełną listę dostępnych parametrów można sprawdzić w pliku remote/RemoteWebDriverCapablityType.php. Dla potrzeb bieżącego ćwiczenia wystarczy ustawić nazwę przeglądarki. Do konstruktora obiektu RemoteWebDriver przekazujemy adres serwera Selenium i tablicę parametrów, a wynikową referencję do obiektu zapisujemy do składowej $driver. Po uruchomieniu tego testu Selenium powinno uruchomić nowe okno przeglądarki oczekujące na przeprowadzenie właściwych testów.

Pisanie testu Spróbujmy przetestować prosty przebieg wykonania aplikacji: przejdziemy do strony AddVenue, dodamy placówkę, a potem dodamy salę. Cały proces obejmuje interakcje pomiędzy trzema stronami. Oto sam test: public function testAddVenue() { $this->driver->get("http://localhost/webwoo/?cmd=AddVenue"); $venel = $this->driver->findElement(WebDriverBy::name("venue_name")); $venel->sendKeys( "my_test_venue"); $venel->submit(); $tdel = $this->driver->findElement(WebDriverBy::xpath("//td[1]")); $this->assertRegexp("/'my_test_venue' added/", $tdel->getText()); $spacel = $this->driver->findElement(WebDriverBy::name("space_name")); $spacel->sendKeys("my_test_space"); $spacel->submit(); $el = $this->driver->findElement(WebDriverBy::xpath("//td[1]")); $this->assertRegexp("/'my_test_space' added/", $el->getText()); }

A oto co się stanie, kiedy faktycznie uruchomimy test: $ phpunit seleniumtest3.php PHPUnit 3.7.24 by Sebastian Bergmann. Time: 8.75 seconds, Memory: 3.75Mb OK (1 test, 2 assertions)

Oczywiście to nie wszystko. Selenium w tle uruchomiło okno przeglądarki i wykonało w nim wszystkie zadane operacje. Trzeba przyznać, że jest to dość efektowne! Przyjrzyjmy się implementacji testu. Najpierw wywołujemy metodę WebDriver::get(), która pobiera stronę startową aplikacji. Zauważmy, że metoda ta powinna otrzymać w wywołaniu pełny adres URL (nie musi to być adres względny wobec serwera Selenium). Selenium wczyta wskazany dokument do uruchomionej przeglądarki, jak na rysunku 18.1.

400

ROZDZIAŁ 18.  TESTY JEDNOSTKOWE Z PHPUNIT

Rysunek 18.1. Strona AddVenue wczytana przez Selenium Po załadowaniu strony możemy nią manipulować za pośrednictwem interfejsu WebDriver API. Na przykład za pośrednictwem metody RemoteWebDriver::findElement() możemy odszukać referencję do obiektu reprezentującego dowolny element strony, zdefiniowany na bazie obiektu WebDriverBy. Klasa WebDriverBy udostępnia zestaw metod wytwórczych, z których każda zwraca obiekt klasy WebDriverBy skonfigurowany według sposobu lokalizowania elementu. Na naszej stronie element formularza posiada atrybut name z wartością "venue_name", więc do pozyskania referencji do obiektu elementu użyliśmy metody wytwórczej WebDriverBy::name(), informującej metodę findElement() o tym, że ma wyszukać element przez atrybut nazwy. Listę dostępnych metod wytwórczych klasy WebDriverBy wymienia tabela 18.4. Tabela 18.4. Metody wytwórcze klasy WebDriverBy Metoda

Opis

className()

Wyszukuje element po nazwie klasy CSS.

cssSelector()

Wyszukuje element po selektorze CSS.

id()

Wyszukuje element po identyfikatorze.

name()

Wyszukuje element po wartości atrybutu name.

linkText()

Wyszukuje element po zawartości treści odnośnika.

partialLinkText()

Wyszukuje element po fragmencie treści odnośnika.

tagName()

Wyszukuje element po nazwie znacznika.

xpath()

Wyszukuje elementy pasujące do wyrażenia Xpath.

Po pozyskaniu referencji do elementu formularza o nazwie venue_name otrzymujemy do dyspozycji obiekt klasy RemoteWebElement; do ustawienia wartości pola formularza możemy wtedy użyć metody sendKeys(). Należy tu zauważyć, że metoda sendKeys() to nie tylko proste ustawienie wartości, ale również symulacja czynności wpisywania wartości w polu formularza. Pozwala to na testowanie systemów, w których po stronie przeglądarki działa kod JavaScript przechwytujący zdarzenia klawiatury. Po ustawieniu nowej wartości pola formularza można już wysłać formularz do aplikacji. Interfejs Selenium jest tu na tyle sprytny, że wywołanie metody submit() na rzecz pola formularza automatycznie powoduje przesłanie całego formularza do aplikacji.

401

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Przesłanie formularza spowoduje oczywiście załadowanie nowej strony, więc sprawdzamy, czy wszystko na niej zgadza się z oczekiwaniami. Ponownie odbywa się to za pomocą metody WebDriver::findElement(), ale tym razem element do wyłuskania jest określany przez wyrażenie Xpath. Jeśli element uda się odnaleźć, metoda findElement() zwróci nowy obiekt klasy RemoteWebElement. Jeśli wyszukiwanie się nie powiedzie, dojdzie do zgłoszenia wyjątku zatrzymującego wykonywanie testu. Jeśli wszystko pójdzie dobrze, to za pomocą metody RemoteWebElement::getText() będziemy mogli odczytać wartość znalezionego elementu w celu porównania z wartością oczekiwaną. Udało się nam już przesłać formularz i sprawdzić zawartość zwróconej w odpowiedzi strony WWW (widocznej na rysunku 18.2).

Rysunek 18.2. Strona AddSpace Pozostaje już tylko ponownie wypełnić formularz, przesłać go do aplikacji i sprawdzić wynik na kolejnej stronie; znamy już metody sprawdzania zawartości otrzymywanych stron WWW. Rzecz jasna nie wyczerpuje to możliwości Selenium jako narzędzia testowego, udało się co najwyżej przedstawić zarys jego użyteczności w projektach PHP. Czytelników zainteresowanych dodatkowymi informacjami odsyłam do podręcznika Selenium pod adresem http://seleniumhq.org/docs/index.html. Warto też zajrzeć do dokumentacji Selenium w zakresie integracji z PHPUnit, dostępnej pod adresem http://www.phpunit.de/manual/current/en/selenium.html.

Słowo ostrzeżenia Nietrudno wskazać pozytywy automatyzowania testów systemu. Osobiście stosuję sumiennie testowanie jednostkowe, używam też PHPUnit do testów funkcjonalnych. Testuję więc kod na poziomie systemu i na poziomie poszczególnych klas. Nie mam wątpliwości, że daje to wymierne korzyści, nie mogę jednak nie wspomnieć o koszcie. Testy niewątpliwie zwiększają koszt rozwoju projektu. Wbudowanie w projekt bezpieczeństwa wiąże się równocześnie z rozciągnięciem implementacji w czasie, ale nie tylko: wydłuża się również faza kompilacji projektu, co może wpływać na wdrożenia. Trzeba więc liczyć nie tylko czas potrzebny na napisanie testów, ale również czas potrzebny na ich wykonanie. W niektórych systemach trzeba przygotować odrębne zestawy testów funkcjonalnych, na przykład do testowania zachowania systemu względem różnych baz danych i różnych

402

ROZDZIAŁ 18.  TESTY JEDNOSTKOWE Z PHPUNIT

systemów kontroli wersji. Jeśli pojawi się jeszcze kilka tego rodzaju zmiennych środowiskowych, staniemy w obliczu istotnej bariery. A przecież testy, których się nie wykonuje, nie są też nic warte. Odpowiedzią jest automatyzowanie uruchamiania testów, na przykład uruchamianie ich z poziomu jakiegoś mechanizmu harmonogramującego pracę, jak cron. Inne rozwiązanie to utrzymywanie wyróżnionego, okrojonego podzbioru testów do łatwego uruchamiania przed zatwierdzeniem kodu do repozytorium. A komplet testów byłby uruchamiany odpowiednio rzadziej. Trzeba też rozważyć aspekt kruchości infrastruktury testowej. Testy dają programistom poczucie pewności przy wprowadzaniu zmian, ale w miarę rozrostu infrastruktury testowej łatwo pojedynczą zmianą sprowokować błędy licznych testów. Niekiedy zresztą takie zachowanie infrastruktury testowej jest pożądane — chcemy przecież wiedzieć, jak błąd wpływa na oczekiwane zachowanie całego systemu, a nie tylko bezpośredniego otoczenia. Niekiedy jednak infrastruktura testów załamuje się po trywialnej zmianie, na przykład przy zmianie sformułowania komunikatu zwrotnego. Każdy nieadekwatny test trzeba możliwie szybko poprawić, ale niekiedy oznacza to poprawkę w kilkudziesięciu przypadkach testowych, tylko z powodu drobnej zmiany konfiguracji albo architektury. Testy jednostkowe są na to mniej podatne dlatego, że koncentrują się na pojedynczych komponentach, w możliwie ścisłej izolacji. Koszt związany z utrzymywaniem testów w synchronizacji z rozwijanym systemem jest czynnikiem, którego nie sposób pominąć w bilansie korzyści. Ogólnie jednak sądzę, że saldo jest dodatnie. Można też podjąć środki zmniejszające wrażliwość infrastruktury testowej. Warto już przy pisaniu testów uwzględnić ewentualne przyszłe zmiany. Osobiście staram się na przykład zamiast prostych porównań stosować dopasowania do wyrażeń regularnych. Test pod kątem dopasowania kilku słów kluczowych ma mniejszą szansę zawieść po banalnym usunięciu znaku nowego wiersza z końca komunikatu zwrotnego. Z drugiej strony, zbytnia liberalizacja kryteriów weryfikacji testu też jest groźna — trzeba to wyważyć. Kolejne zagadnienie to zakres stosowalności atrap i imitacji w celu fałszowania otoczenia systemowego przy testowaniu komponentów. Niektórzy nalegają, aby izolacja komponentów była możliwie pełna i imitują całe otoczenie klasy. W niektórych projektach to się sprawdza. Ale bywa, że utrzymywanie systemu imitacji i atrap staje się zwyczajnie zbyt czasochłonne. Do kosztu utrzymania testów w spójnym stanie z systemem dochodzi koszt odpowiedniego aktualizowania również całego (niekiedy sporego) rusztowania atrap. Weźmy za przykład zmianę typu wartości zwracanej z metody. Jeśli zapomnimy o analogicznej zmianie typu w atrapie zasilającej testowaną klasę, testy mogą być nieskuteczne. A rosnąca złożoność systemu atrapowego ma swoje własne wady, w tym coraz większe ryzyko błędu w implementacji atrap i imitacji. Diagnostyka poprawności testów to doprawdy niewdzięczne zajęcie, zwłaszcza kiedy sam system jest zupełnie w porządku, a tylko infrastruktura testów kuleje. Zwykłem rozstrzygać tę kwestię na wyczucie: domyślnie stosuję atrapy i imitacje, ale tam, gdzie koszt takiego rozwiązania jest za wysoki, nie waham się wprowadzić do testu prawdziwe komponenty otoczenia. Traci się wtedy nieco na skupieniu na podmiocie testu, ale za to ma się ten komfort, że ewentualne błędy prowokowane w otoczeniu komponentu są faktycznie błędami systemu. Można oczywiście stosować kombinacje komponentów prawdziwych i fałszowanych; na przykład rutynowo stosuję w testach prawdziwe bazy danych, tylko że z danymi składowanymi w pamięci komputera, a nie na dysku. Zwłaszcza w PDO jest to proste do osiągnięcia. Oto uproszczona klasa, która komunikuje się z bazą danych za pomocą PDO: class DBFace { private $pdo; function __construct($dsn, $user=null, $pass=null) { $this->pdo = new PDO($dsn, $user, $pass); $this->pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } function query($query) { $stmt = $this->pdo->query($query); return $stmt; } }

403

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Jeśli klasy odwzorowania bazują na obiektach DBFace, wtedy można bardzo łatwo przełączyć cały test na bazę danych SQLite w trybie pamięciowym: public function setUp() { $face = new DBFace("sqlite::memory:"); $face->query("create table user " ."(id INTEGER PRIMARY KEY, name TEXT)"); $face->query("insert into user (name) values('bob')"); $face->query("insert into user (name) values('harry')"); $this->mapper = new ToolMapper($face); }

Łatwo wywnioskować, że nie w zakresie testowania nie jestem szczególnym radykałem. Rutynowo „oszukuję” klasyczne zalecenia, kombinując komponenty prawdziwe i fałszowane, a ponieważ podstawianie danych jest operacją wielokrotną, często centralizuję konfigurację testów w implementacji wzorca, który Martin Fowler ochrzcił jako Object Mothers. Klasy składające się na tę implementację są prostymi wytwórniami generującymi wypełnione obiekty gotowe do testów. Po zasygnalizowaniu potencjalnych problemów sprawianych przez testy wypada jedynie wypunktować zalety, które moim zdaniem przeważają szalę. Otóż testy:  Pomagają zapobiegać błędom (co najmniej regresjom, ale też innym błędom przy rozwijaniu i refaktoryzowaniu systemu).  Pomagają ujawniać istniejące błędy (tym skuteczniej, im lepsze pokrycie kodu testami).  Zachęcają do porządnego projektowania systemu.  Zwiększają jakość systemu i eliminują obawę o to, że zmiany wnoszą więcej problemów niż korzyści.  Zwiększają obawy przy wdrażaniu i wydawaniu kodu. W każdym projekcie, dla którego zdarzyło mi się pisać testy, doczekałem się płynących z nich korzyści — i zwykle długo nie czekałem.

Podsumowanie W rozdziale powróciliśmy do kwestii testów, które jako programiści wszyscy piszemy, ale które często po jednokrotnym użyciu zarzucamy. Czytelnicy zapoznali się z PHPUnit, czyli frameworkiem ułatwiającym pisanie testów w sposób zupełnie podobny do owych testów marnowanych — ale z korzyściami rozciągniętymi również na przyszłe rewizje kodu. Utworzyliśmy ćwiczebną implementację przypadku testowego. Można się było zapoznać z przydatnymi metodami asercji. Przedstawiono obiekty ograniczeń i kompozycje tych obiektów, tworzące dowolnie skomplikowane kryteria testów. Była wreszcie mowa o atrapach i imitacjach. Można się było przekonać, że refaktoryzacja kodu pod kątem testów zazwyczaj oznacza ogólne zwiększenie jakości projektu. Opisano techniki testowania aplikacji WWW — najpierw za pośrednictwem samego PHPUnit, a potem z użyciem Selenium. Na koniec zaryzykowałem prezentację najpoważniejszych kosztów wiążących się z utrzymywaniem solidnej infrastruktury testów.

404

ROZDZIAŁ 19 

Automatyzacja instalacji z Phing

Automatyzacja czynności związanych z instalacją czy wdrożeniem projektu to druga strona tego samego medalu, który na awersie ma automatyzowane zarządzanie wersjami. To ostatnie pozwala wielu programistom na efektywną współpracę w ramach pojedynczego projektu. Kiedy zaś wielu programistów musi wykonywać u siebie testowe wdrożenia projektu, paląca staje się potrzeba automatyzacji tego procesu. Jeden programista może testować swoje dokonania w katalogu /usr/local/apache/htdocs, inny w katalogu /home/bibble/public_html. Obaj mogą korzystać z różnych haseł dostępu do baz danych, różnych katalogów bibliotecznych, różnych mechanizmów rozsyłania poczty. W odpowiednio zaprojektowanym kodzie wszystkie te różnice można łatwo ująć, delegując ich rozpoznanie do konfiguracji, a to z kolei wymaga wykonania tej konfiguracji za każdym razem, kiedy będzie trzeba przetestować bieżący stan aplikacji. Jeśli testowanie efektów wprowadzanych zmian ma się odbywać kilka razy dziennie (albo, co gorsza, kilka razy na godzinę), ręczne kopiowanie plików projektu i konfiguracji pomiędzy katalogami staje się niedopuszczalne. Znamy już instalatory z PEAR. Wydaje się, że produkt dla końcowego użytkownika powinien mieć właśnie postać pakietu PEAR, bo taki mechanizm udostępnienia daje najmniejszą barierę instalacji (użytkownicy zapewne posiadają PEAR w swoich systemach, a PEAR obsługuje instalowanie z sieci). PEAR nieźle obsługuje ostatnie etapy instalacji, ale zostawia nam sporo nadającej się do automatyzacji pracy przed utworzeniem pakietu: trzeba na przykład wyłuskać pliki z repozytorium systemu kontroli wersji, uruchomić testy i skompilować pliki do postaci katalogu dystrybucyjnego. Na koniec warto byłoby zautomatyzować również samo tworzenie pakietu PEAR. W tym rozdziale zapoznasz się z mechanizmem, który to wszystko umożliwi, a będzie to Phing. Niniejszy rozdział będzie poświęcony:  Pobieraniu i instalacji programu Phing — czyli temu, jak zainstalować instalator.  Właściwościom — czyli ustawianiu i odczytywaniu danych.  Typom — czyli opisywaniu skomplikowanych części projektu.  Różnicowaniu zadań kompilacji — czyli podziałowi procesu przygotowania projektu na współzależne zestawy funkcji.  Operacjom.

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Czym jest Phing?

Phing to narzędzie służące do kompilacji1 projektów PHP. Jest wzorowane na bardzo popularnym (i cechującym się bardzo dużymi możliwościami) narzędziu o nazwie Ant, przeznaczonym dla języka Java. Nazwa tego ostatniego nie jest przypadkowa. Ant (ang. ant — mrówka) to aplikacja niewielka, ale pozwalająca na tworzenie iście wielkich rzeczy. Zarówno Ant, jak i Phing opierają swoje działanie na pliku XML (zwykle jest to plik build.xml), który opisuje kolejność i tryb instalacji bądź innego rodzaju operacji na poszczególnych elementach projektu. Świat PHP naprawdę czeka na dobre narzędzia kompilowania pakietów. Programiści poważnie traktujący swoje zajęcie mieli w przeszłości w tym zakresie kilka możliwości. Przede wszystkim mogli wykorzystywać program make, powszechnie stosowany w systemach uniksowych i ich pochodnych do automatyzacji kompilacji i konsolidacji programów w najróżniejszych językach, w tym C i Perlu; make jest jednakowoż wyjątkowo wybredny, jeśli chodzi o składnię, a jego opanowanie wymaga nie lada wiedzy o programowaniu powłoki — dla wielu programistów PHP, którzy nie mieli okazji wychować się na terminalach uniksowych i linuksowych, może to być dużym wyzwaniem. Program make nie daje też wbudowanych możliwości realizacji przekształceń nazw plików i ich zawartości — pełni bowiem jedynie rolę łącznika pomiędzy poleceniami powłoki. Utrudnia to jego stosowanie na różnych platformach. Nie wszystkie bowiem dysponują identycznymi wersjami narzędzia make, jeśli w ogóle je posiadają. A nawet, jeśli w danym systemie zainstalowany jest program make, nie oznacza to automatycznie, że są w nim dostępne wszystkie programy, które są wywoływane z poziomu pliku kompilacji (pliku makefile). W nazwie Phing odzwierciedlona została relacja pomiędzy tym narzędziem a make: Phing należy rozszyfrowywać jako PHing Is Not Gnu make (rekurencyjne rozwinięcie nazwy to stary dowcip programistów — samo GNU oznacza Gnu is Not Unix). Phing to program interpretujący podawany na jego wejście plik XML poprzez odczytanie z niego wskazówek co do sposobu przekształcenia projektu. Operacje zaangażowane w to przekształcenie obejmują zazwyczaj kopiowanie plików z katalogu dystrybucyjnego do katalogów docelowych, ale to nie wszystko, na co stać ten program. Phing może generować dokumentację, uruchamiać testy, wywoływać polecenia, uruchamiać kod PHP, tworzyć pakiety PEAR, zastępować słowa kluczowe w plikach, usuwać z nich komentarze i wreszcie generować gotowe do rozpowszechniania kompresowane archiwa projektu. A jeśli nawet Phing sam z siebie nie zrobi wszystkiego, czego byśmy od niego oczekiwali, został zaprojektowany z myślą o możliwie łatwym rozszerzaniu. Ponieważ Phing to aplikacja PHP, do jego uruchomienia potrzebny jest jedynie interpreter PHP. A jako że Phing ma służyć do instalowania aplikacji PHP, wymóg ten jest spełniony z założenia. Mieliśmy się już okazję przekonać o prostocie instalacji pakietów PEAR. PEAR korzysta z własnego mechanizmu automatyzującego kompilację. Skoro PEAR wchodzi w skład dystrybucji PHP, to może do instalowania własnych projektów należałoby stosować właśnie pakietowanie na wzór PEAR? Odpowiedź brzmi: tak. PEAR ułatwia instalowanie, obsługuje też zależności (można więc zapewnić, że nasze pakiety będą ze sobą zgodne). W czasie przygotowywania pakietu trzeba za to zautomatyzować wiele czynności. Technika użycia PEAR jako pakietów rozprowadzanych wśród użytkowników i Phing jako narzędzia wspomagającego ich przygotowanie jest zresztą wykorzystywana również do rozwijania samego pakietu Phing.

Pobieranie i instalacja pakietu Phing Gdyby instalowanie instalatora było skomplikowane, z pewnością coś poszłoby przy tym nie tak! Jeśli jednak dysponujemy w swoim systemie PHP5 lub nowszym (a takie założenie można chyba spokojnie przyjąć, zważywszy choćby na tytuł tej książki), instalacja Phing to rzecz najprostsza pod słońcem. Pobranie i zainstalowanie Phing sprowadza się do wydania dwóch prostych poleceń: $ pear channel-discover pear.phing.info $ pear install phing/phing

1

Język PHP nie jest oczywiście językiem kompilowanym, tylko skryptowym; pojęcie kompilacji jest tu użyte w znaczeniu ang. słowa build, a więc oznacza składanie projektu w nadającą się do rozpowszechniania czy instalacji całość — przyp. tłum.

406

ROZDZIAŁ 19.  AUTOMATYZACJA INSTALACJI Z PHING

W ten sposób zainstalowany zostanie pakiet Phing. Instalacja wymaga uprawnień zapisu w standardowych katalogach pakietów PEAR, co w większości systemów uniksowych i linuksowych oznacza konieczność posiadania uprawnień użytkownika root. Jeśli przy instalacji wystąpią jakiekolwiek problemy, należy odwiedzić stronę http://phing.info/trac/wiki/ Users/Download. Można tam znaleźć mnóstwo wskazówek co do instalowania Phing.

Montowanie dokumentu kompilacji Możemy wreszcie zacząć dłubać przy Phing. Sprawdźmy najpierw, czy w ogóle działa: $ phing -v Phing version 2.5.0

Opcja -v w powyższym poleceniu to żądanie zwrócenia przez skrypt informacji o numerze wersji. W chwili, gdy czytasz te słowa, aktualny numer wersji może być zupełnie inny, ale sam komunikat bardzo podobny. Spróbujmy teraz wywołać polecenie phing bez żadnych argumentów: $ phing Buildfile: build.xml does not exist!

Jak widać, Phing domaga się wskazówek. Normalnie szuka ich w pliku kompilacji o nazwie build.xml. Spróbujmy więc podać na wejście okrojony plik kompilacji i pozbyć się w ten sposób komunikatu o błędzie:

Skoro tak wygląda XML-owy komentarz, to drugi wiersz naszego pliku będzie ignorowany. W plikach kompilacji można umieszczać dowolną liczbę dowolnych komentarzy i naprawdę warto z tej możliwości korzystać, zwłaszcza w co bardziej rozbudowanych plikach. Bez nich obszerniejsze pliki kompilacji okazują się nieczytelne. Faktycznym początkiem pliku kompilacji jest element project. Element ten może definiować do pięciu atrybutów, z których dwa — name i default — są obowiązkowe. Atrybut name określa nazwę projektu. Atrybut default definiuje zaś zadanie kompilacji (ang. target), które ma zostać wybrane w przypadku braku określenia zadania w wywołaniu polecenia phing. Opcjonalny atrybut description można wykorzystać do opisowego

407

PHP. OBIEKTY, WZORCE, NARZĘDZIA

podsumowania. Czwarty atrybut, o nazwie basedir, pozwala na określenie katalogu kontekstu kompilacji. Jeśli zostanie pominięty, rolę tego katalogu będzie pełnił katalog, z którego wywołane zostanie polecenie phing. Ostatni atrybut, phingVersion, pozwala na wskazanie wersji programu Phing wymaganej do skutecznej kompilacji pakietu. Komplet informacji o atrybutach elementu project wymienia tabela 19.1. Tabela 19.1. Atrybuty elementu project Atrybut

Obowiązkowy

Znaczenie

name

tak

Nazwa projektu.

description

nie

Opisowe (ale najlepiej skrócone) podsumowanie projektu.

default

tak

Domyślne zadanie kompilacji.

phingVersion

nie

Minimalna wymagana wersja programu Phing.

basedir

nie

Kontekst (katalog systemu plików), w którym należy przeprowadzić kompilację.

Wartość domyślna

katalog bieżący (.)

Po zdefiniowaniu elementu project musimy w nim osadzić element target opisujący zadanie kompilacji — przynajmniej jeden, ten określony atrybutem default.

Różnicowanie zadań kompilacji To, co określamy tu mianem „zadań” (ang. target), to zestaw czynności kompilacyjnych zebranych pod jedną nazwą i realizujących pewne wyodrębnione zadanie, na przykład skopiowanie plików z jednego katalogu do innego czy też wygenerowanie dokumentacji. W naszym przykładzie w swoim okrojonym niemiłosiernie pliku kompilacji zdefiniowaliśmy również dość minimalistycznie jedno zadanie:

Jak widać, element target opisujący zadanie musi definiować przynajmniej jeden atrybut — name. Skoro domyślnym zadaniem kompilacji uczyniliśmy wcześniej main, to za każdym razem, kiedy phing zostanie wywołany bez argumentów, przystąpi do realizacji tego właśnie zadania. Mieliśmy już potwierdzenie tego zachowania w przykładzie: megaquiz > main:

Zadania mogą być współzależne. Ustanawiając zależność jednego zadania od innego, instruujemy Phing, że realizacja pierwszego zadania musi być poprzedzona realizacją zadania, od którego to pierwsze zależy. Spróbujmy taką zależność wyrazić w pliku kompilacji:





Obecność opisu w elemencie target nie wpływa na ogół na przebieg kompilacji. Jeśli jednak użytkownik wywoła program Phing z opcją -projecthelp, zobaczy listę zadań z ich opisami: $ phing -projecthelp Buildfile: /home/bob/working/megaquiz/build.xml Warning: target 'runfirst' has no tasks or dependencies Program quizowy Default target: -----------------------------------------------------------main Zadanie główne Main targets: -----------------------------------------------------------main Zadanie główne runfirst Pierwsze zadanie runsecond Drugie zadanie

Zauważmy, że atrybut description został dodany również do elementu project. Jeśli któreś z zadań chcemy wyłączyć z wypisywanej listy, możemy użyć atrybutu hidden; stosuje się go często w zadaniach, które realizują czynności porządkowe, ale które nie powinny być wywoływane wprost z wiersza poleceń.





Baza danych: ${dbname} Hasło: ${dbpass} Adres: ${dbhost}

Wprowadziliśmy tu nowy element — property. Element ten wymaga określenia atrybutów name (nazwa właściwości) i value (wartość właściwości). Nie sposób nie zauważyć również uzupełnienia elementu target zadania głównego. Osadzone w nim elementy echo są przykładami zadań. O zadaniach powiemy sobie więcej w następnym punkcie. Na razie wystarczy powiedzieć, że echo działa dokładnie tak, jak można by oczekiwać

410

ROZDZIAŁ 19.  AUTOMATYZACJA INSTALACJI Z PHING

— wyprowadza zawartość elementu na wyjście. Na uwagę zasługuje składnia odwołania do właściwości w pliku kompilacji — właściwość jest zapowiadana znakiem dolara, a jej nazwa ujęta w nawiasy klamrowe. Phing, widząc taką konstrukcję, zastępuje ją wartością właściwości: ${nazwa-właściwości}

Na razie działanie pliku kompilacji ogranicza się do przypisania wartości trzem właściwościom i wypisania tych wartości na standardowe wyjście. Sprawdźmy: $ phing Buildfile: /home/bob/working/megaquiz/build.xml megaquiz > [echo] [echo] [echo]

main: Baza danych: megaquiz Hasło: default Adres: localhost

BUILD FINISHED Total time: 0.4402 seconds

Skoro znamy już właściwości, możemy dokończyć omówienie elementów target. Otóż element ten może definiować dwa atrybuty dodatkowe: if i unless. Każdy z nich można ustawić z nazwą właściwości. Jeśli zastosujemy w elemencie zadania atrybut if z nazwą właściwości, zadanie zostanie zrealizowane pod warunkiem uprzedniego ustawienia tej właściwości. Jeśli nie zostanie ona ustawiona, zadanie zostanie dyskretnie pominięte. Spróbujmy więc usunąć z pliku definicję właściwości dbpass i uzależnić od niej realizację zadania main:



Hasło: ${dbpass}

412

ROZDZIAŁ 19.  AUTOMATYZACJA INSTALACJI Z PHING

Właściwość dbpass ustawiliśmy pierwotnie z wartością default. W ramach realizacji zadania main ustawiamy ją ponownie, przesłaniając jej poprzednią wartość (override="yes") i przypisując nową: specific. O skuteczności drugiej definicji przekonują komunikaty kompilacji: $ phing Buildfile: /home/bob/working/megaquiz/build.xml megaquiz > main: [echo] Hasło:

specific

BUILD FINISHED Total time: 0.3802 seconds

Gdybyśmy w drugiej deklaracji właściwości o tej samej nazwie nie zastosowali atrybutu override z wartością yes, zadanie echo wyprowadziłoby na wyjście pierwotną wartość właściwości — default. Trzeba przy tym pamiętać, że w aspekcie przesłaniania właściwości zadania nie zachowują się dokładnie jak funkcje — w tym sensie, że nie ma tu pojęcia lokalnego zasięgu właściwości. Przesłonięcie wartości właściwości w obrębie zadania powoduje zamazanie poprzedniej wartości w całej reszcie procesu kompilacji. Można temu oczywiście zapobiec, zachowując pierwotną wartość właściwości w „lokalnej” właściwości tymczasowej i przywracając ją po zakończeniu realizacji właściwych zadań w obrębie zadania. Jak dotąd manipulowaliśmy jedynie takimi właściwościami, które definiowaliśmy samodzielnie. Tymczasem Phing obsługuje zestaw właściwości wbudowanych. Odwołujemy się do nich tak samo jak do własnych. Oto przykład:



database: ${dbname} pass: ${dbpass} host: ${dbhost}

Jak widać, wykonanie pliku kompilacji sprowadza się do wypisania wartości właściwości bez ich uprzedniego zadeklarowania ani sprawdzenia, czy w ogóle istnieją. Dlatego próba uruchomienia kompilacji bez dodatkowych argumentów zakończy się następująco: $ phing ... [echo] database: ${dbname} [echo] pass: ${dbpass} [echo] host: ${dbhost} ...

Wartości używanych właściwości można zdefiniować w osobnym pliku; nazwijmy go megaquiz.properties: dbname=filedb dbpass=filepass dbhost=filehost

Teraz można wskazać ten plik do kompilacji za pomocą opcji propertyfile: $ phing -propertyfile megaquiz.properties

414

ROZDZIAŁ 19.  AUTOMATYZACJA INSTALACJI Z PHING

... [echo] database: filedb [echo] pass: filepass [echo] host: filehost ...

Osobiście uważam ten mechanizm za znacznie wygodniejszy niż utrzymywanie długich list starannie dobranych argumentów wywołania polecenia Phing. Trzeba jednak pamiętać, aby omyłkowo nie wprowadzić pliku właściwości do systemu kontroli wersji — zwłaszcza jeśli zawiera hasło do bazy danych! Do zagwarantowania przypisania wartości właściwościom możemy wykorzystać odpowiednio sformułowane zadania. Załóżmy na przykład, że nasz projekt wymaga ustawienia właściwości dbpass. Chcielibyśmy, aby użytkownik podawał wartość tej właściwości w wywołaniu kompilacji (bo tak ustawiona wartość właściwości ma zawsze pierwszeństwo przed wartościami ustawionymi w pliku kompilacji albo środowisku). Jeśli użytkownik nie spełni oczekiwań, możemy sprawdzić zestaw zmiennych środowiskowych. Jeśli i tam nie znajdziemy właściwości, poddajemy się i kontynuujemy instalację z domyślną wartością właściwości:





415

PHP. OBIEKTY, WZORCE, NARZĘDZIA



pass: ${dbpass}

Zadanie warunkowe wymaga ustawienia atrybutu property; opcjonalnie można do niego dołączyć również atrybut value, z wartością przypisywaną do właściwości, kiedy zagnieżdżona w zadaniu klauzula zostanie spełniona. W przypadku braku jawnej wartości domyślnej atrybutu, kiedy klauzula będzie spełniona, właściwość wskazana atrybutem property zostanie ustawiona na true. Sama klauzula warunkowa to jeden z szeregu elementów, z których część — jak występujący u nas element not — może zawierać własne elementy zagnieżdżone. Ponieważ chcemy przypisać wartość do nieustawionej właściwości dbpass, wartość z elementu warunkowego isset musi zostać zanegowana za pomocą elementu not. Analogiczny warunek w składni PHP zapisalibyśmy następująco: if (!isset($dbname)) { $dbname = "default"; }

 Uwaga Lista wszystkich rozpoznawanych elementów warunkowych jest zamieszczona w dokumentacji Phinga pod adresem http://www.phing.info/docs/guide/stable/chapters/ProjectComponents.html#Conditions.

Typy Zdaje się, że odwołując się do właściwości, manipulujemy danymi. Ale Phing rozpoznaje dodatkowo specjalny zestaw elementów, w ramach których można przetwarzać różnego rodzaju informacje przydatne w procesie kompilacji.

FileSet Załóżmy, że w pliku kompilacji mamy reprezentować katalog (co jest koniecznością wyjątkowo częstą). Katalog ten możemy niewątpliwie reprezentować właściwością, narażając się jednak na problemy wynikające z różnych reprezentacji separatorów katalogów w różnych systemach docelowych. Lepiej wtedy skorzystać z typu danych FileSet. FileSet, czyli zbiór plików, to typ niezależny od platformy systemowej, z umownym separatorem w postaci ukośników; separatory umowne są w tle automatycznie konwertowane na separatory obowiązujące w danym systemie (np. ukośniki lewe w systemie Windows). Element fileset reprezentujący typ FileSet prezentuje się w swej minimalnej postaci następująco:

Katalog wskazujemy w ramach elementu fileset atrybutem dir. Możemy element uzupełnić atrybutem id, za pośrednictwem którego będziemy mogli odwoływać się do elementu w dalszej części pliku kompilacji:

W definicji zbioru plików FileSet możemy również określać kategorie plików, które mają wchodzić w skład zbioru albo mają być z tego zbioru wyłączone. Przy instalowaniu plików możemy na przykład wyłączyć z instalacji te pliki zbioru, których nazwy pasują do pewnego wzorca. Wyłączenie takie realizuje się atrybutem excludes:

416

ROZDZIAŁ 19.  AUTOMATYZACJA INSTALACJI Z PHING

Składnia wartości atrybutu excludes wymaga osobnego komentarza. Otóż para gwiazdek reprezentuje tu dowolny katalog bądź podkatalog w katalogu src/lib. W przytoczonym przykładzie określamy więc, że z elementu fileset o id równym srclib wyłączamy pliki kończące się na _test.php i Test.php umieszczone w podkatalogach katalogu określonego atrybutem dir. Jeśli atrybut excludes ma definiować większą liczbę wzorców wyłączenia, należy je oddzielać znakami odstępów (spacjami, znakami tabulacji, znakami nowego wiersza itd.). Taką samą składnię możemy stosować w definicjach wartości kolejnego atrybutu o nazwie includes. Jeśli na przykład nasze katalogi wewnątrz src/lib zawierają rozmaite pliki, które były wykorzystywane przez programistów aplikacji, ale nie mają być objęte instalacją, możemy te pliki wyłączyć, jak przed chwilą, ale znacznie prościej będzie odpowiednio zdefiniować zbiór plików włączanych — niech będą to po prostu wszystkie pliki kończące się .php; wszystkie inne zostaną przy instalacji pominięte:

Kiedy rozpędzimy się z definiowaniem atrybutów excludes i includes, możemy łatwo tak wydłużyć definicje elementów fileset, że przestaną być czytelne. Można na szczęście wyodrębnić z definicji pojedyncze reguły włączania i wyłączania plików i definiować je osobnymi elementami osadzonymi wewnątrz elementu fileset, jak tutaj:

" />

hasło: ${dbpass}

Zakładamy, że domyślnym zadaniem kompilacji jest ponownie main. Jego realizacja jest uzależniona od zadania setpass, w ramach którego ma nastąpić ustalenie hasła bazy danych. To ostatnie zadanie jest warunkowane atrybutem unless — jeśli operator ustawił już hasło (np. argumentem wywołania), zadanie nie zostanie podjęte. Zadanie setpass definiuje pojedynczy element input. Element ten może otrzymać atrybut message określający monit prezentowany operatorowi. Atrybut propertyName wskazuje właściwość kompilacji, do której należy przypisać wartość odebraną z wejścia — to atrybut wymagany. Jeśli użytkownik naciśnie klawisz Enter bez wprowadzania wartości, właściwość wskazywana przez propertyName zostanie ustawiona wartością domyślną definiowaną atrybutem defaultValue. Wreszcie atrybut promptChar pozwala na zdefiniowanie tzw. znaku zachęty, czyli znaku, który będzie dla użytkownika sygnałem gotowości instalatora do odbierania danych z wejścia. Wypróbujmy działanie całości:

422

ROZDZIAŁ 19.  AUTOMATYZACJA INSTALACJI Z PHING

$ phing Buildfile: /home/bob/working/megaquiz/build.xml megaquiz > setpass: Nie ustawiono hasła bazy danych [default] > test megaquiz > main: [echo] hasło:

test

BUILD FINISHED Total time: 6.0322 seconds

Podsumowanie elementu input zamieszczone jest w tabeli 19.6. Tabela 19.6. Atrybuty elementu input Atrybut

Obowiązkowy

Opis

propertyName

tak

Właściwość, do której przypisane zostaną dane podane na wejście.

message

nie

Komunikat monitu wprowadzenia danych.

defaultValue

nie

Wartość wybierana przy braku danych na wejściu.

validArgs

nie

Lista dopuszczalnych wartości, oddzielonych przecinkami. Jeśli użytkownik wprowadzi wartość spoza listy, Phing ponowi monit.

promptChar

nie

Znak zachęty sygnalizujący gotowość do przyjmowania danych.

Delete Instalacja to tworzenie, kopiowanie i przekształcanie plików. Ale nie można zapomnieć o usuwaniu. Usuwanie ma kolosalne znaczenie zwłaszcza w procesie przygotowania instalacji — czasem trzeba się przecież pozbyć plików poprzedniej instalacji. Wiemy już, że normalnie w kolejnej operacji kopiowania kopiowane są z katalogu kompilacji do katalogu docelowego te pliki, które w międzyczasie uległy zmianom. Jeśli chcemy mieć pewność przeprowadzenia pełnej instalacji, możemy po prostu usunąć efekty poprzedniej instalacji. Usuńmy katalog:



Kiedy teraz uruchomimy polecenie phing z argumentem clean (nazwą zadania), wywołana zostanie operacja delete. Oto co pojawi się na wyjściu programu: $ phing clean Buildfile: /home/bob/working/megaquiz/build.xml megaquiz > clean: [delete] Deleting directory /home/bob/working/megaquiz/build BUILD FINISHED

Działanie operacji usuwania można zawęzić do konkretnego pliku, uzupełniając definicję elementu atrybutem file. Można też wskazać zbiór plików do usunięcia, osadzając wewnątrz delete element fileset.

423

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Podsumowanie W poważniejszych projektach nie można pozwolić sobie na wykonywanie wszystkich czynności w jednym miejscu. Proces rozwoju kodu musi być oddzielony od instalacji, aby bieżące modyfikacje kodu nie wpływały na kod produkcyjny, który przecież powinien działać nieprzerwanie i bez zgrzytów. Postulat ten pomagają spełnić systemy kontroli wersji pozwalające wybrać z repozytorium kod projektu i pracować nad nim w wyizolowanym obszarze (tzw. piaskownicy). Ale łatwość wybierania kodu z repozytorium musi być poparta łatwością konfigurowania rozwijanego projektu w środowisku testowym. A i klient (którym czasem jest sam programista, kiedy po latach zapomni już szczegółów swojego dzieła) powinien mieć komfort instalacji gotowego produktu w swoim systemie — po ewentualnym rzucie oka na treść pliku README. Bohaterem tego rozdziału był program Phing, fantastyczne narzędzie przybliżające programistom PHP znaczną część udogodnień znanych z Apache Ant. Oczywiście treść rozdziału nie wyczerpywała tematu możliwości tego narzędzia. Niemniej jednak udało się omówić zadania, operacje, typy i właściwości; jeśli poświęcisz trochę czasu, szybko wdrożysz się do stosowania pozostałych, w tym zaawansowanych elementów kompilacji — tworzenia gotowych do dystrybucji (skompresowanych) archiwów tar, automatycznego generowania pakietów PEAR czy uruchamiania kodu PHP z poziomu pliku kompilacji. A jeśli wyczerpiesz już wszystkie możliwości Phing i uznasz je za niewystarczające, odkryjesz, że Phing (tak jak Ant) to narzędzie rozszerzalne — możesz samodzielnie oprogramować własne operacje! Zresztą nawet jeśli nie zamierzasz rozszerzać możliwości programu Phing, zachęcam do zajrzenia do jego kodu źródłowego — to kod pisany w języku PHP, całkowicie obiektowy i mogący służyć jako wzorcowy przykład zastosowania koncepcji obiektowych.

424

ROZDZIAŁ 20 

Ciągła integracja kodu

W poprzednich rozdziałach omówiono mnóstwo narzędzi pomocnych przy zarządzaniu projektami. Wiemy już, że testy jednostkowe, automatyczne dokumentowanie kodu, automatyczne kompilacje i kontrola wersji są niezwykle przydatnymi mechanizmami. Ale konsekwentne ich stosowanie — zwłaszcza narzędzi do testów jednostkowych — bywa nużące. Nawet jeśli testy wykonują się jedynie kilka minut, programiści często są zbyt skoncentrowani na kodowaniu i o nich zapominają. Przecież koledzy, jako klienci nowych funkcji, czekają na nie z niecierpliwością. Nie sposób więc oprzeć się pokusie programowania do oporu. Z drugiej strony, błędy znacznie łatwiej się eliminuje, kiedy są jeszcze „świeże” — programista wie, która z ostatnich zmian mogła potencjalnie wprowadzić błąd, więc jest w stanie szybciej go zlokalizować i naprawić. W tym rozdziale poznasz mechanizmy tzw. ciągłej integracji, czyli praktyki polegającej na automatyzacji testów i kompilacji projektów, a także zbierającej w jedno wszystkie narzędzia omawiane w poprzednich rozdziałach. Rozdział będzie poświęcony:  Definicji integracji ciągłej.  Przygotowaniu projektu pod kątem ciągłej integracji.  Przedstawieniu popularnego serwera integracji ciągłej Jenkins.  Dostosowywaniu Jenkinsa dla projektów PHP za pomocą dedykowanych rozszerzeń.

Czym jest ciągła integracja? W dawnych czasach integracja była procesem realizowanym po zakończeniu najprzyjemniejszego etapu prac, czyli programowania. Na tym etapie okazywało się też najczęściej, ile jeszcze tak naprawdę zostało do zrobienia. Integracja to proces pozwalający na zmontowanie projektu do postaci gotowych pakietów, zdatnych do wdrożenia i uruchomienia. Nie jest specjalnie efektowna i bywa naprawdę trudna. Integracja ma ścisłe związki z kontrolą jakości kodu. Nie można wdrożyć produktu, jeśli nie spełnia oczekiwań klientów. Potrzebne są więc testy — mnóstwo testów. Jeśli wcześniej produkt nie był odpowiednio szeroko testowany, czeka nas mnóstwo niemiłych niespodzianek. Z rozdziału 18. wiemy, że testowanie powinno się odbywać możliwie najwcześniej i możliwie najczęściej. W rozdziałach 15. i 19. była mowa o tym, że programista powinien od początku pracować z myślą o ostatecznym wdrożeniu projektu. Większość programistów akceptuje te zasady jako reguły świata idealnego, niekoniecznie osiągalne w rzeczywistości.

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Użytkownicy podejścia zorientowanego na testy będą na tym etapie mieli spore „fory”: napisanie testów będzie teraz dużo prostsze, skoro testy i tak wciąż pisaliśmy w toku projektu. Za każdym razem, kiedy finalizujemy komponent, tworzymy też specjalne fragmenty kodu (na przykład u dołu pliku klasy), które tworzą egzemplarze obiektów i wywołują ich metody. Możemy zebrać te odrzucane wcześniej fragmenty kodu kontrolujące jakość rozwijającego się komponentu i test gotowy — wystarczy ująć kod testujący w klasę i dodać do zestawu testów. Może się to wydawać dziwne, ale bywa, że programiści unikają nie tyle samego pisania testów, co ich uruchamiania. Z czasem bowiem czas wykonania kompletu testów jest coraz dłuższy, a ich przebieg jest zakłócany przez błędy, o których programiście już doskonale wiadomo, co utrudnia wychwycenie nowych błędów. Bywa, że błędy pojawiają się w wyniku omyłek innego programisty, który zatwierdził swój niepoprawny kod do repozytorium, i zwyczajnie nie mamy czasu na wstrzymywanie swoich prac z powodu cudzych problemów. Lepiej więc uruchomić kilka swoich własnych testów, niż brnąć przez cały zestaw. Zaniedbywanie uruchamiania testów, a co za tym idzie opóźnienie poprawiania wykrywanych w ten sposób błędów, powoduje ich nawarstwianie i utrudnia poprawki. Wiadomo, że przy błędach najwięcej czasu zajmuje diagnostyka, sama poprawka to ledwo ułamek pracy. Poprawkę da się sklecić czasem w kilka minut, za to wykrycie przyczyny błędu trwa nieraz i godzinami. Jeśli o błędzie będziemy wiedzieć kilka minut bądź godzin po zatwierdzeniu nowej wersji kodu, prawdopodobieństwo szybkiego znalezienia przyczyny będzie znacznie większe. Podobne problemy pojawiają się na etapie kompilacji projektu. Jeśli nie jest on dostatecznie często instalowany, na etapie integracji może się okazać, że projekt działa wyśmienicie w środowisku programistycznym, ale instalowane instancje projektu trapione są tajemniczymi błędami. Im większe odstępy czasowe między próbnymi kompilacjami i instalacjami, tym mniej jasne przyczyny nieudanej instalacji i niepoprawnego działania projektu w środowisku docelowym. Czasami rzecz jest trywialna: niezadeklarowana zależność od biblioteki, pominięcie jednego pliku klasy przy zatwierdzaniu kodu do repozytorium. Trywialna, póki jesteśmy pod ręką. Ale jeśli błąd pojawi się po naszych godzinach pracy? Nieszczęśnik, któremu dostanie się rola wdrożeniowca, nie zna przecież specyficznej odmienności naszego środowiska programistycznego i nie ma dostępu do brakujących plików. Problemy z integracją potęgują się w miarę wzrostu liczby osób pracujących nad projektem. Znamy, lubimy i cenimy swoich kolegów, ale pewnie i oni zaniedbują wiele testów po swojej stronie. A potem tuż przed odcięciem projektu do wdrożenia w piątek o szesnastej zatwierdzają do repozytorium tygodniową porcję nieprzetestowanego kodu. Mechanizmy ciągłej integracji (CI, od Continuous Integration) pomagają zmniejszyć skalę przedstawionych problemów, wymuszając automatyzację kompilacji i testów. CI to zarówno narzędzia, jak i zasady. Co do zasad, CI wymaga regularnego i częstego zatwierdzania kodu do repozytorium (co najmniej raz dziennie). Każda zatwierdzona porcja kodu wymusza uruchomienie kompletu testów i zbudowanie kompletu docelowych pakietów. Niektóre z narzędzi wymaganych w ciągłej integracji projektów już znamy: to między innymi PHPUnit i Phing. Ale same narzędzia to za mało; do skoordynowania i zautomatyzowania ich użycia potrzebny jest system wyższego poziomu. Bez takiego systemu, który nazwiemy serwerem CI, zasady ciągłej integracji zostaną zapewne zaniedbane tak samo, jak zaniedbywane były testy i kompilacje testowe poza systemem CI. Przecież wszyscy bardziej lubimy programować. Wdrożenie do projektu mechanizmu ciągłej integracji oferuje liczne zalety. Przede wszystkim zapewnia częste testowanie i budowanie pakietów instalacyjnych. To zresztą najważniejszy cel i największe dobro wynikające ze stosowania CI. Automatyzacja procesu daje sama z siebie dwie kolejne zalety. Testy i kompilacje odbywają się poza środowiskiem programistycznym — odbywają się w tle, nie wymagają więc od programistów przerywania pracy i oczekiwania na wyniki. Do tego automatyzacja i oddzielenie testów wymuszają porządne projektowanie komponentów systemu: skoro testy odbywają się poza środowiskiem programistycznym, to aby w ogóle można było je wykonać, programista musi od początku pracy uwzględniać kwestię instalacji kodu. Nie wiem, ile razy widywałem projekty, w których przygotowanie kodu do instalacji było sztuką tajemną, znaną wąskiemu kręgowi wtajemniczonych. „Chcesz powiedzieć, że nie dopisałeś regułek rewrite? — pytają potem z uśmiechem pobłażania nieszczęśnika bezskutecznie próbującego samodzielnej instalacji — przecież regułki są w Wiki, wystarczy je skopiować i wkleić do pliku konfiguracyjnego serwera Apache”. Są pomocni i niezastąpieni.

426

ROZDZIAŁ 20.  CIĄGŁA INTEGRACJA KODU

Rozwijanie projektu pod kątem integracji ciągłej oznacza, że już od początku jednym z priorytetów jest łatwość instalacji. Może to oznaczać trochę dodatkowej pracy na początku, ale z czasem będzie to istotna ulga. Dla wszystkich. Na początek zaczniemy więc właśnie od tego kosztownego przygotowania. Zresztą lektura najbliższych podrozdziałów pokaże, że o większości z koniecznych czynności już wiemy.

Przygotowanie projektu do ciągłej integracji Potrzebujemy jakiegoś projektu. Wiedzeni cnotą lenistwa rozejrzyjmy się za czymś, co już posiada potrzebne testy. Oczywistym kandydatem jest projekt z rozdziału 18., ilustrujący pracę z PHPUnit. Nowy projekt nazwiemy userthing (ma w sobie coś i ma w sobie klasę User). Pierwotny układ katalogów naszego projektu ujawnia rysunek 20.1.

Rysunek 20.1. Przykładowy projekt do ilustracji ciągłej integracji kodu Jak widać, struktura projektu została nieco podzielona i pojawiły się nowe katalogi pakietów. Struktura ta w samym kodzie jest obsługiwana na bazie przestrzeni nazw. Mamy już projekt, potrzebny nam teraz system kontroli wersji.

CI a kontrola wersji Kontrola wersji to nieodłączny element ciągłej integracji. System CI potrzebuje systemu kontroli wersji, aby móc odwoływać się do najnowszej rewizji kodu bez konieczności ludzkiej interwencji. Utworzymy repozytorium na serwerze Git tak jak w rozdziale 17.: $ $ $ $ $

sudo mkdir /var/git/userthing sudo chown git:git /var/git/userthing sudo su git cd /var/git/userthing git --bare init

427

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Utworzyliśmy katalog userthing i zmieniliśmy jego właściciela i grupę. Następnie w imieniu użytkownika git utworzyliśmy repozytorium. Teraz trzeba wypchnąć do niego pierwszą wersję kodu projektu userthing, najlepiej importując do nowego repozytorium kod na własnym koncie. $ $ $ $ $ $

cd /home/mattz/work/userthing git init git add . git commit -m 'first commit' git remote add origin [email protected]:/var/git/userthing git push origin master

Przeszliśmy do własnego katalogu roboczego i zainicjalizowaliśmy w nim repozytorium Git. Następnie skojarzyliśmy repozytorium z repozytorium zdalnym, do którego wypchnęliśmy zaimportowany kod. Żeby sprawdzić, czy wszystko działa jak należy, warto spróbować wykonać klonowanie tak wypełnionego repozytorium zdalnego: $ git clone [email protected]:/var/git/userthing Cloning into 'userthing'... Enter passphrase for key '/home/mattz/.ssh/id_rsa': remote: Counting objects: 61, done. remote: Compressing objects: 100% (56/56), done. remote: Total 61 (delta 14), reused 0 (delta 0) Receiving objects: 100% (61/61), 9.09 KiB, done. Resolving deltas: 100% (14/14), done.

Mamy więc repozytorium projektu userthing i jego lokalną kopię. Pora na zautomatyzowanie budowania i testowania projektu.

Phing Z Phingiem zetknęliśmy się w rozdziale 19. Instalowaliśmy go tak: $ pear channel-discover pear.phing.info $ pear install phing/phing

Do zmontowania systemu ciągłej integracji użyjemy właśnie Phinga. Dzięki niemu łatwo zdefiniujemy zadania kompilacji i testowania projektu, a także uruchamiania różnych metryk jakości kodu, przedstawianych w dalszej części rozdziału.  Uwaga Przy instalowaniu Phinga warto przeanalizować komunikaty wypisywane przez instalator pear pod kątem ewentualnych błędów; mogą to być powiadomienia o konieczności pozyskania dodatkowych zależności. Instalator pear jest tu bardzo pomocny i dokładnie pokazuje czynności potrzebne do zainstalowania wymaganych zależności, trzeba tylko się zorientować, że wykonana instalacja może nie być tak kompletna, jak by się mogło wydawać.

Zaczniemy od zupełnych podstaw:







428

ROZDZIAŁ 20.  CIĄGŁA INTEGRACJA KODU









W pliku konfiguracji kompilacji mamy ustawione trzy właściwości. Pierwsza z nich, build, odnosi się do katalogu, w którym będziemy montować projekt przed wygenerowaniem gotowego pakietu dystrybucyjnego. To właśnie tam będą się uruchamiać testy i inne narzędzia omawiane w rozdziale. Właściwość src to katalog kodu źródłowego. Właściwość version definiuje numer wersji pakietu. Zadanie kompilacji build polega na skopiowaniu katalogów userthing i test do katalogu środowiska kompilacji. W bardziej złożonych projektach zadanie to obejmowałoby również rozmaite transformacje, generowanie plików konfiguracyjnych i przygotowywanie zasobów binarnych. To domyślne zadanie projektu. Zadanie clean obejmuje operacje czyszczenia katalogu kompilacji i całej jego zawartości. Spróbujmy uruchomić kompilację: $ phing Buildfile: /home/mattz/work/userthing/build.xml userthing > build: [mkdir] [copy] [copy] [copy] [copy]

Created Created Copying Created Copying

dir: /home/mattz/work/userthing/build 4 empty directories in /home/mattz/work/userthing/build/userthing 3 files to /home/mattz/work/userthing/build/userthing 1 empty directory in /home/mattz/work/userthing/build/test 2 files to /home/mattz/work/userthing/build/test

BUILD FINISHED Total time: 0.7534 seconds

Testy jednostkowe Testy jednostkowe są kluczowe dla systemu ciągłej integracji. Nie ma wielkiego pożytku z regularnego i częstego przygotowywania do wdrożenia kodu, który zawiera błędy. Testowaniem jednostkowym za pomocą PHPUnit zajmowaliśmy się w rozdziale 18. Dla tych, którzy nie czytają od deski do deski, na początek pomocne narzędzia, które warto zainstalować: $ pear config-set auto_discover 1 $ pear install --alldeps pear.phpunit.de/phpunit

W rozdziale 18. pokazywane były też testy napisane dla tej wersji kodu userthing, której będziemy używać do ciągłej integracji. Uruchomimy je jeszcze raz (z katalogu src), żeby mieć pewność, że po reorganizacji projektu nic się nie popsuło: $ phpunit test/ 3.7.24 by Sebastian Bergmann. ..... Time: 317 ms, Memory: 3.75Mb OK (5 tests, 5 assertions)

Widać, że projekt wciąż działa. Ale chcielibyśmy móc wywoływać testy za pomocą Phinga.

429

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Phing udostępnia zadanie exec do wywoływania dowolnych poleceń. Moglibyśmy z powodzeniem użyć tego zadania, ale lepiej rozejrzeć się za czymś dedykowanym. Mianowicie dostępne jest też wbudowane zadanie test:







Zadanie test jest zależne od wykonania zadania build. Dzięki temu możemy być pewni, że testy wykonują się w gotowym, docelowym środowisku, w którym będzie pracował sam system. Element phpunit opisujący zadanie testowe konfiguruje się za pomocą licznych atrybutów definiujących sposób przeprowadzenia testu. Większość właściwej treści zadania konfiguruje się za pośrednictwem zagnieżdżonych elementów: formatter opisuje sposób generowania zestawienia wyników z testów (tutaj ustawiamy raport czytelny dla człowieka); batchtest pozwala na wskazanie wielu plików testowych do wykonania (na bazie zagnieżdżonego elementu fileset). Zauważmy, że element batchtest posiada atrybut classpath, pozwalający na ustawienie ścieżki przeszukiwania plików włączanych na potrzeby testów (nazwa atrybutu wywodzi się ze środowiska języka Java).  Uwaga Zadanie phpunit jest wysoce konfigurowalne; pełna dokumentacja zakresu konfiguracji znajduje się w podręczniku Phinga pod adresem http://www.phing.info/docs/guide/stable/chapters/appendixes/AppendixC-OptionalTasks.html#PHPUnitTask.

Spróbujmy teraz przeprowadzić testy jednostkowe z poziomu zadania Phinga: $ phing test Buildfile: /home/mattz/work/userthing/build.xml userthing > build: userthing > [phpunit] [phpunit] [phpunit] [phpunit] [phpunit]

test: Testsuite: ValidatorTest Tests run: 2, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.07081 s Testsuite: UserStoreTest Tests run: 3, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.02879 s Total tests run: 5, Failures: 0, Errors: 0, Incomplete: 0, Skipped: 0, Time elapsed: 0.10772 s

BUILD FINISHED Total time: 1.0921 second

Dokumentacja Jedną z zasad CI jest przezroczystość. Nowo zbudowany w zakresie CI projekt powinien posiadać aktualną dokumentację, obejmującą również wszystkie nowe klasy i metody, które pojawiły się w ostatniej kompilacji. W rozdziale 16. poznaliśmy narzędzie zdatne do zapewnienia takiego pokrycia kodu dokumentacją: phpDocumentor. Najlepiej od razu je zainstalujmy: $ pear channel-discover pear.phpdoc.org $ pear install phpdoc/phpdocumentor

430

ROZDZIAŁ 20.  CIĄGŁA INTEGRACJA KODU

I od razu warto próbnie wygenerować dokumentację, tym razem w katalogu build: $ mkdir docs $ phpdoc --directory=userthing --target=docs --title=userthing --template=abstract

W ten sposób dorobimy się najbardziej podstawowego zarysu dokumentacji. Kiedy zostanie ona wciągnięta do CI, ów nagi szkielet będzie tam na widoku publicznym wisieć jak wyrzut sumienia, zachęcając do jak najszybszego uzupełnienia treścią. I tym razem chcemy ten proces zautomatyzować, definiując go w pliku kompilacji build.xml. Co prawda Phing definiuje zadanie o nazwie phpdoc2 przeznaczone do integrowania z PHPDocumentorem, ale w czasie przygotowywania niniejszego wydania obsługa tego zadania była niekompletna; tak czy inaczej poradzimy sobie; wystarczy użyć ogólnego zadania exec i jawnie wywołać w nim program phpdoc:





Ponownie zadanie budowania dokumentacji uzależniamy od wykonania zadania build. W ramach samego zadania tworzymy katalog docelowy dokumentacji build/docs, a następnie uruchamiamy ogólne zadanie exec. Jako plik wykonywalny wskazujemy phpdoc. Katalog, w którym plik ma zostać uruchomiony, podajemy za pośrednictwem atrybutu dir. Element exec może zawierać zagnieżdżony element arg, który definiuje ewentualne dodatkowe parametry wywołania polecenia. Jeśli jest to pojedynczy parametr, można go zdefiniować w atrybucie value elementu arg; w naszym przypadku mamy jednak całą listę argumentów nadającą się do umieszczenia w zbiorczym atrybucie line, w którym poszczególne parametry oddziela się spacjami.  Uwaga Pełna dokumentacja zadania exec jest dostępna w podręczniku publikowanym pod adresem http://www.phing.info/docs/guide/stable/chapters/appendixes/AppendixB-CoreTasks.html#ExecTask.

 Uwaga Możliwe, że kiedy książka pójdzie do druku, zadanie phpdoc2 będzie ponownie poprawnie integrować się z PHPDocumentorem. Składnię definicji tego zadania można znaleźć w podręczniku pod adresem http://www.phing.info/ docs/guide/stable/chapters/appendixes/AppendixCOptionalTasks.html#PhpDocumentor2Task .

Pokrycie kodu testami jednostkowymi Nie sposób polegać na testach, jeśli nie testują one całości kodu napisanego w ramach projektu. PHPUnit ma możliwość raportowania o stopniu pokrycia kodu testami jednostkowymi. Oto fragment komunikatów pomocy wypisywanych przez PHPUnit: --coverage-html --coverage-clover

Generate code coverage report in HTML format. Write code coverage data in Clover XML format.

Aby skorzystać z wykrywania stopnia pokrycia kodu, trzeba mieć zainstalowane rozszerzenie Xdebug. Więcej informacji o tym rozszerzeniu można znaleźć pod adresem http://pecl.php.net/package/Xdebug (a wskazówki co do instalacji znajdziemy pod adresem http://xdebug.org/docs/install). Często rozszerzenie to jest dostępne za pośrednictwem mechanizmu pakietów dystrybucyjnych w Linuksie. Na przykład w dystrybucji Fedora instalacja sprowadza się do polecenia: $ yum install php-pecl-xdebug

431

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Teraz możemy już uruchomić PHPUnit (w katalogu src) z opcją badania pokrycia kodu: $ mkdir /tmp/coverage $ phpunit --coverage-html /tmp/coverage test PHPUnit 3.7.24 by Sebastian Bergmann. .... Time: 3.36 seconds, Memory: 4.75Mb OK (5 tests, 5 assertions) Generating code coverage report in HTML format ... done

Raport z analizy można obejrzeć w przeglądarce, jak na rysunku 20.2.

Rysunek 20.2. Raport z pokrycia kodu testami jednostkowymi Wypada zaznaczyć, że osiągnięcie pełnego pokrycia testami nie jest równoznaczne z porządnym przetestowaniem projektu. Z drugiej strony, warto wiedzieć o lukach w testowaniu; u nas, jak widać na rysunku 20.2, jest jeszcze sporo pracy w tym zakresie. Po potwierdzeniu, że da się wygenerować raport pokrycia kodu z poziomu wiersza poleceń, możemy włączyć tę funkcję do pliku kompilacji:











432

ROZDZIAŁ 20.  CIĄGŁA INTEGRACJA KODU





Mamy tu nowe zadanie o nazwie citest. W większości jest ono reprodukcją zadania testowego zdefiniowanego wcześniej. Na początku zadanie tworzy katalog raportu z testów i podkatalog raportu pokrycia testami. Zadanie coverage-setup służy do zdefiniowania konfiguracji dla mechanizmu coverage. Tutaj za pomocą atrybutu database określamy miejsce przechowywania gołych danych z badania pokrycia testami. Zagnieżdżony element fileset wskazuje pliki, które mają być przedmiotem badania pokrycia. Do zadania phpunit dodaliśmy dwa elementy formatter. Pierwszy z nich, typu xml, generuje plik o nazwie testreport.xml, w którym znajdują się wyniki testu. Drugi, typu clover, definiuje miejsce generowania informacji o pokryciu testami, również w formacie XML. W ramach zadania citest umieściliśmy też podzadanie coverage-report. Zadanie to na podstawie danych badania pokrycia generuje raport o pokryciu kodu testami w formacie HTML.  Uwaga Dokumentacja zadania coverage-report jest dostępna pod adresem http://www.phing.info/docs/guide/stable/ chapters/appendixes/AppendixC-OptionalTasks.html#CoverageReportTask.

Standardy kodowania Można całymi dniami sprzeczać się o to, gdzie najlepiej wstawiać klamry, ile znaków odstępu powinna mieć tabulacja i jak należy dobierać nazwy dla prywatnych zmiennych klasy. Ale czy nie byłoby lepiej, gdyby nasze wyobrażenia o kodzie idealnym wsparło solidne narzędzie? Jest takie: PHP_CodeSniffer. CodeSniffer potrafi przystawić do projektu zestaw standardów kodowania i wygenerować raport podsumowujący odstępstwa od umówionego stylu. Wydaje się, że to mało przydatna wiedza; i faktycznie niekiedy tak jest. Ale są też sensowne aktywne zastosowania narzędzi tego rodzaju; zanim do nich przejdziemy, wypróbujemy nowe narzędzie w najbardziej podstawowym zadaniu. Najpierw instalacja: $ sudo pear install PHP_CodeSniffer

A teraz porównanie naszego kodu z wytycznymi standardu kodowania Zend: $ phpcs --standard=Zend build/userthing/persist/UserStore.php FILE: ...userthing/build/userthing/persist/UserStore.php -------------------------------------------------------------------------------FOUND 9 ERROR(S) AFFECTING 8 LINE(S) -------------------------------------------------------------------------------6 | ERROR | Opening brace of a class must be on the line after the definition 7 | ERROR | Private member variable "users" must contain a leading underscore 9 | ERROR | Opening brace should be on a new line 13 | ERROR | Closing parenthesis of a multi-line function call must be on a | | line by itself ...

 Uwaga Wes Hunt, recenzent techniczny tego wydania, zgłaszał pewne problemy z instalacją CodeSniffera w systemie Windows: „Pear upierał się, że pakiet jest już zainstalowany; musiałem wyczyścić C:\Users\{użytkownik}\AppData\ Local\Temp\pear\cache. Wtedy CodeSniffer zainstalował się bez przeszkód”.

433

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Jak widać, jeśli chcemy oddawać kod do Zenda, musimy się jeszcze postarać. Zespół programistyczny może jednak z powodzeniem ustalać własne standardy kodowania. Przy tym decyzja o tym, który konkretnie miałby to być standard, jest mniej ważna od tego, aby przyjąć i utrzymać jakąkolwiek wspólną konwencję. Jeśli kod jest spójny pod tym względem, staje się czytelniejszy i łatwiej nad nim pracować. Weźmy choćby konwencje nazewnicze, które mogą wydatnie pomóc w rozpoznaniu znaczenia i odpowiedzialności danej zmiennej po samej nazwie. Spójna konwencja kodowania pomaga też uniknąć wpuszczenia do projektu kodu ryzykownego, podatnego na błędy logiczne (np. pozbawionego klamer przy zagnieżdżonych instrukcjach if-else). Trzeba jednak uważać, bo styl kodowania bywa kwestią bardzo subiektywną i wrażliwą — programiści potrafią być bardzo przywiązani do swojego stylu pracy i niechętnie godzą się na nieuzgodnione zmiany. Skoro CodeSniffer pozwala na zdefiniowanie własnej konwencji, dlaczego by nie podjąć wysiłku ustalenia wspólnego stylu w łonie zespołu? Może tak uda się uniknąć unieszczęśliwienia niektórych programistów narzuceniem kompletnie odmiennej konwencji. Zaletą automatycznej kontroli stylu kodowania jest bez wątpienia „maszynowość”, a więc i bezstronność takiego procesu. Jeśli zespół zdecyduje się na wprowadzenie jakiegoś zestawu reguł kodowania, to lepiej, żeby ewentualne naruszenia ustaleń wytykał obiektywny skrypt niż humorzasty współpracownik, któremu przecież zdarza się dokładnie to samo. Łatwo się domyślić, że kontrola stylu kodowania będzie naszym następnym zadaniem kompilacji:







Zadanie phpcodesniffer załatwi sprawę za nas. Do określenia aprobowanego stylu kodowania użyłem standardu Zend. Pliki do kontroli stylu są zdefiniowane za pomocą zagnieżdżonego elementu fileset. Całość uzupełniają dwa elementy formatujące: pierwszy, typu checkstyle, generuje plik XML raportu kontroli w katalogu raportów; drugi, typu default, wypisuje podsumowanie kontroli na wyjście standardowe.  Uwaga CodeSniffer pozwala na definiowanie własnych reguł stylu kodowania; na stronie PEAR znajduje się stosowny przewodnik: http://pear.php.net/manual/en/package.php.php-codesniffer.coding-standard-tutorial.php.

Budowanie pakietu Skoro kod mamy wytestowany i opatrzony rozmaitymi raportami, warto jeszcze sprawdzić, czy da się z tego wszystkiego zbudować gotowy pakiet. Pakiet będzie miał format PEAR, a wygenerujemy go za pomocą zadania pearpkg2:







434

ROZDZIAŁ 20.  CIĄGŁA INTEGRACJA KODU













Rola poszczególnych elementów zadania powinna być oczywista, zwłaszcza jeśli odwołamy się do struktury pliku pakietu prezentowanej w rozdziale 15. Zauważmy, że elementowi releaseversion przypisujemy wartość właściwości version zdefiniowanej na początku pliku kompilacji. Zestaw plików do włączenia do pakietu jest definiowany elementem fileset.  Uwaga Dokumentacja zadania pearpkg2 znajduje się pod adresem http://www.phing.info/docs/guide/stable/chapters/ appendixes/AppendixC-OptionalTasks.html#PearPackage2Task.

Dzięki opcji packagefile zadanie makepackagefile będzie generowało plik o nazwie userthing_package.xml. Aby dokonać przekształcenia definicji otrzymanego pliku pakietu na faktyczny pakiet nadający się do dystrybucji, uciekniemy się znów do ogólnego zadania exec:





Powyższa definicja zadania exec jest równoważna poleceniu: pear package userthing_package.xml

wykonanemu w katalogu kompilacji. Wynikiem wykonania polecenia jest archiwum pakietu o nazwie userthing-1.1.1.tgz.  Uwaga Archiwum pakietu można utworzyć również za pomocą dedykowanego zadania tartask, udokumentowanego na stronie http://www.phing.info/docs/guide/stable/chapters/appendixes/AppendixC-OptionalTasks.html#TarTask.

Zauważmy, że wewnątrz elementu exec pojawia się definicja atrybutu checkreturn. Dyrektywa ta nakazuje Phingowi wypisać ostrzeżenie o nieudanej próbie uruchomienia polecenia (kiedy polecenie zwróci kod błędu inny niż zero). Jeśli pominiemy ten atrybut, polecenie uruchamiane zadaniem exec może nie wykonać się poprawnie (na przykład z powodu braku pliku wykonywalnego pear w podanej ścieżce). Spróbujmy uruchomić nowe zadanie tworzenia archiwum pakietu:

435

PHP. OBIEKTY, WZORCE, NARZĘDZIA

$ phing buildpackage Buildfile: /home/mattz/work/userthing/build.xml userthing > build: userthing > makepackagefile: [pearpkg2] Creating [default] package.xml file in base directory. Analyzing userthing/domain/User.php Analyzing userthing/persist/UserStore.php Analyzing userthing/util/Validator.php userthing > buildpackage: BUILD FINISHED Total time: 1.7685 second

Ostateczny test poprawności pakietu to próba jego instalacji: $ pear install --force build/userthing-1.1.1.tgz install ok: channel://pear.php.net/userthing-1.1.1

Dorobiliśmy się niniejszym pokaźnego zestawu narzędzi zdatnych do skutecznego monitorowania projektu. Ale oczywiście gdyby trzeba je było uruchamiać ręcznie, nawet za pomocą wygodnego pliku kompilacji Phinga, niebawem wszyscy programiści by to zaniedbali — w najlepszym przypadku narzędzia zostałyby użyte dopiero na krótko przed fazą wdrożenia, ale wtedy ich efektywność można uznać za znikomą — w końcu mają przede wszystkim służyć jako system wczesnego ostrzegania. Dlatego potrzebujemy serwera CI, który zrobi lepszy użytek z kompletu narzędzi.

Jenkins Jenkins (dawniej znany jako Hudson) to otwarty (w sensie open-source) serwer ciągłej integracji. Napisano go co prawda w języku Java, ale bardzo łatwo można go zintegrować z oprzyrządowaniem języka PHP. Jest to możliwe dzięki temu, że serwer ciągłej integracji sam w sobie nie próbuje samodzielnie kompilować projektów, ale deleguje to do rozmaitych poleceń i jedynie monitoruje i reaguje na ich wyniki. Jenkins integruje się dobrze z językiem PHP również dlatego, że został zaprojektowany z uwzględnieniem mechanizmu rozszerzeń (wtyczek) i cieszy się liczną społecznością aktywnych programistów stale rozwijających możliwości serwera.  Uwaga Dlaczego Jenkins? Otóż Jenkins jest bardzo prosty w użyciu i łatwy do rozszerzania. Jest to system uznany, sprawdzony i posiadający wsparcie silnej społeczności. Jest darmowy i otwartoźródłowy. Dostępne są rozszerzenia integrujące Jenkinsa z projektami pisanymi w języku PHP (i z większością oprzyrządowania wykorzystywanego do zarządzania takimi projektami). Nie znaczy to jednak, że jest to jedyne tego rodzaju rozwiązanie. W poprzednim wydaniu tej książki omawiałem system CruiseControl (http://cruisecontrol.sourceforge.net/), który wcale nie stracił na jakości. Z kolei osoby zainteresowane serwerem ciągłej integracji napisanym również w PHP powinny zainteresować się projektem Xinc (http://code.google.com/p/xinc/).

Instalowanie Jenkinsa Jenkinsa napisano w Javie, więc wymaga zainstalowanego środowiska wykonawczego Java. Instalacja tego środowiska wygląda różnie w różnych systemach. W dystrybucji Fedora instaluje się je tak: $ yum install java

436

ROZDZIAŁ 20.  CIĄGŁA INTEGRACJA KODU

Środowisko wykonawcze Java w wersjach dla różnych systemów można też pobrać z witryny www.java.com. Aby sprawdzić, czy w systemie znajduje się już poprawnie zainstalowane środowisko wykonawcze języka Java, wystarczy wydać polecenie: $ java -version java version "1.7.0_60" OpenJDK Runtime Environment (fedora-2.4.2.0.fc19-x86_64) OpenJDK 64-Bit Server VM (build 24.0-b56, mixed mode)

Samego Jenkinsa można pobrać ze strony WWW projektu pod adresem http://jenkins-ci.org/. Jest tam dostępny w postaci archiwum WAR (Java Web Archiwe) oraz w postaci pakietów dedykowanych dla różnych systemów i dystrybucji. Osobiście skorzystałem z pakietu dedykowanego dla dystrybucji Fedora: $ wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo $ rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key $ yum install jenkins

Na stronie Jenkinsa znajdują się oczywiście instrukcje instalacji pakietów dla większości dystrybucji. Po zainstalowaniu oprogramowania Jenkinsa można uruchomić wprost, za pomocą polecenia java: $ sudo java -jar /usr/lib/jenkins/jenkins.war

Jednak taki tryb uruchamiania będzie w przyszłości problematyczny. Dlatego znacznie lepiej będzie użyć skryptu startowego, definiującego i uruchamiającego Jenkinsa jako usługę systemową, uruchamianą w imieniu użytkownika jenkins. W przypadku dystrybucji Fedora uruchamianie Jenkinsa jako usługi systemowej odbywa się tak: $ service jenkins start

Uniwersalne skrypty startowe można znaleźć pod adresem https://wiki.jenkins-ci.org/display/JENKINS/ JenkinsLinuxStartupScript. Domyślnie Jenkins uruchamia nasłuch na porcie 8080. Żeby sprawdzić, czy Jenkins faktycznie jest gotowy do pracy, wystarczy uruchomić przeglądarkę i skierować ją pod adres http://adres-url-serwera:8080/. W przeglądarce powinna pojawić się mniej więcej taka strona jak na rysunku 20.3.

Rysunek 20.3. Strona panelu głównego Jenkinsa 437

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Instalowanie rozszerzeń Jenkinsa Jenkins jest systemem wysoce konfigurowalnym, a my będziemy potrzebować sporej liczby wtyczek do zintegrowania go z zapowiadanymi mechanizmami ciągłej integracji naszych projektów. Do panelu zarządzania wtyczkami dostajemy się za pośrednictwem opcji Manage Jenkins, a potem Manage Plugins. W zakładce Available znajduje się długa lista dostępnych rozszerzeń. Zaznaczamy pola w kolumnie Install przy wszystkich wtyczkach, które chcemy dodać do Jenkinsa. Listę potrzebnych wtyczek zawiera tabela 20.1. Tabela 20.1. Wybrane wtyczki rozszerzeń Jenkinsa Wtyczka

Opis

Git Plugin

Umożliwia integrację z repozytoriami Git.

xUnit Plugin

Umożliwia integrację z rodziną narzędzi xUnit, w tym z PHPUnit.

Phing Plugin

Umożliwia uruchamianie zadań systemu Phing.

Clover PHP Plugin

Udostępnia pliki analizy pokrycia (w formatach XML i HTML) generowane przez PHPUnit i pozwala na generowanie raportów.

HTML Publisher Plugin

Integruje raporty w formacie HTML; wykorzystywana do włączenia dokumentacji generowanej przez PHPDocumentora.

Checkstyle Plugin

Generuje raporty naruszeń stylu kodowania z plików XML wytwarzanych przez PHPCodeSniffera.

Stronę zarządzania wtyczkami Jenkinsa widać na rysunku 20.4.

Rysunek 20.4. Strona zarządzania wtyczkami Po zainstalowaniu wskazanych wtyczek będziemy już prawie gotowi do utworzenia i skonfigurowania ciągłej integracji dla naszego projektu. 438

ROZDZIAŁ 20.  CIĄGŁA INTEGRACJA KODU

Konfigurowanie klucza publicznego serwera Git Żeby używać wtyczki integracji z repozytorium Git, należy zadbać o to, aby Jenkins miał dostęp do repozytorium. Z rozdziału 17. wiemy, jak generować klucz publiczny potrzebny do uwierzytelnienia połączenia z serwerem repozytorium. Procedurę tę trzeba powtórzyć także tym razem. Ale czy Jenkins posiada konto systemowe? Położenie katalogu domowego da się konfigurować, ale naturalnie Jenkins sporo podpowiada; po kliknięciu Configure Jenkins, a potem Configure System na wyświetlonej stronie można znaleźć katalog domowy Jenkinsa. Warto skonfrontować tę informację z wpisem konta jenkins w pliku /etc/passwd. W moim systemie katalog domowy Jenkinsa był ustawiony na /var/lib/jenkins. Powinniśmy utworzyć tam katalog konfiguracji SSH: $ $ $ $ $

sudo su jenkins -s /bin/bash cd ~ mkdir .ssh chmod 0700 .ssh ssh-keygen

Powyższe polecenia sprowadzają się do przełączenia się na konto jenkins z uruchomieniem wskazanej powłoki (tu bash), ponieważ konto jenkins może mieć domyślnie wyłączoną powłokę logowania. Po przejściu do katalogu domowego tego konta polecenie mkdir tworzy katalog konfiguracji SSH, polecenie chmod ustawia na tym katalogu wymagane restrykcje dostępu, a polecenie ssh-keygen generuje tam parę kluczy SSH. W odpowiedzi na pytanie o hasło do klucza można po prostu wcisnąć Enter, co umożliwi uwierzytelnianie po kluczu bez podawania hasła zabezpieczającego sam klucz. Potem wystarczy już tylko zadbać o to, aby plik kluczy był dostępny do odczytu i zapisu wyłącznie dla użytkownika konta jenkins: $ chmod 0600 . ssh/id_rsa

Możemy już wydobyć klucz publiczny konta jenkins z pliku .ssh/id_rsa.pub i dodać go do kluczy dopuszczonych na koncie zdalnego serwera Git (ta część procedury również była opisywana w rozdziale 17.). To jeszcze nie wszystko; po stronie serwera jenkins trzeba jeszcze zadbać o to, aby serwer Gita był kwalifikowany jako „znany” dla SSH. Ustawienie to można połączyć z testem dostępności repozytorium Git dla Jenkinsa. Wciąż z konta jenkins wydajemy polecenia: $ cd /tmp $ git clone [email protected] :/var/git/userthing

Przy operacji klonowania repozytorium zostaniemy zapytani o potwierdzenie autentyczności serwera Git, co zostanie utrwalone w pliku .ssh/known_hosts użytkownika jenkins. Dzięki temu połączenia z Jenkinsa do Gita będą się już mogły odbywać bez żadnych przeszkód.

Instalowanie projektu Na stronie panelu głównego Jenkinsa klikamy odnośnik create new jobs. Na wyświetlonej stronie możemy nareszcie utworzyć projekt ciągłej integracji dla projektu userthing. Stronę tworzenia projektu widać na rysunku 20.5. Następna strona to strona konfiguracji. Przede wszystkim chcemy tam dać Jenkinsowi namiary na zdalne repozytorium Git: w polu Source Code Manager zaznaczamy przycisk Git i podajemy adres naszego repozytorium, jak na rysunku 20.6. Jeśli wszystko udało się skonfigurować, powinniśmy już w Jenkinsie mieć dostęp do kodu projektu. Aby to sprawdzić, warto posłużyć się Phingiem — za pośrednictwem funkcji Invoke Phing targets z panelu Add build step. Zadania Phing do wywołania podajemy w polu tekstowym (patrz rysunek 20.7).

439

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Rysunek 20.5. Strona konfiguracji projektu w Jenkinsie

Rysunek 20.6. Konfigurowanie repozytorium kontroli wersji 440

ROZDZIAŁ 20.  CIĄGŁA INTEGRACJA KODU

Rysunek 20.7. Konfigurowanie zadań Phing

Pierwsza kompilacja Po zapisaniu ustawień konfiguracyjnych i kliknięciu Build Now Jenkins uruchomi proces kompilacji i testu projektu. To istny moment prawdy! W polu Build History powinien pojawić się odnośnik do wyników kompilacji. Kliknięcie go prowadzi do strony monitorującej wyjście procesu kompilacji (Console Output), gdzie można sprawdzić przebieg wykonania kompilacji (jak na rysunku 20.8). Pełny sukces! Jenkins wciągnął kod projektu z serwera Git i uruchomił na nim wszystkie zadania zdefiniowane w pliku kompilacji.

Konfigurowanie raportów Dzięki starannej konstrukcji pliku kompilacji naszego projektu wszelkie raporty z wykonywanych zadań są utrwalane w katalogu build/reports, a dokumentacja jest umieszczana w katalogu build/docs. Odpowiednie wtyczki można skonfigurować za pośrednictwem funkcji Add post-build action na stronie konfiguracji projektu. Rysunek 20.9 ilustruje niektóre opcje konfiguracji raportów. Zamiast zamieszczać kilkanaście zrzutów ekranowych, lepiej podsumować konfigurację w tabelce. Tabela 20.2 pokazuje niektóre z przydatnych czynności pokompilacyjnych, z ich polami konfiguracyjnymi i wartościami odpowiednimi dla naszego projektu. Wartości wszystkich pól konfiguracyjnych z tabeli 20.2 zdefiniowaliśmy właściwie już przy konstruowaniu pliku kompilacji; poza ostatnim, w którym można podać listę adresów e-mail — wskazane tam osoby będą w ten sposób informowane o nieudanych kompilacjach. Po dokończeniu konfiguracji możemy wrócić do strony głównej projektu i ponownie zainicjować kompilację. Teraz podsumowanie kompilacji będzie znacznie bogatsze — jak na rysunku 20.10.

441

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Rysunek 20.8. Console Output

Rysunek 20.9. Konfiguracja wtyczki raportów 442

ROZDZIAŁ 20.  CIĄGŁA INTEGRACJA KODU

Tabela 20.2. Konfiguracja raportów Pozycja konfiguracji

Zadanie Phing

Pole

Wartość

Publish Checkstyle analysis results

phpcodesniffer

Checkstyle results

build/reports/checkstyle.xml

Publish Clover PHP Coverage Report

phpunit

Clover XML Location

build/reports/cloverreport.xml

Clover HTML report directory

build/reports/coverage/

HTML directory to archive

build/docs

Index page[s]

index.html

Test report XMLs

build/reports/testreport.xml

Recipients

ktoś@gdzieś.com

Publish HTML reports

Publish Junit test result report E-mail Notification

exec (lub phpdoc2) phpunit

Rysunek 20.10. Strona projektu z informacjami o trendach Z czasem, w miarę wykonywania kolejnych kompilacji, wykresy trendów będą pozwalały śledzić poprawę i osłabienie pokrycia testami, poprawność wykonania testów i stopień zgodności z przyjętym stylem kodowania. Na tej samej stronie są też publikowane odnośniki do dokumentacji, szczegółowych wyników testów i szczegółowych raportów pokrycia kodu testami.

443

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Automatyzacja kompilacji Wszystkie te jakże bogate informacje będą praktycznie bezużyteczne, jeśli trzeba będzie pamiętać o ręcznym uruchamianiu kompilacji w celu przegenerowania statystyk. Jenkins również na to ma sposób — dysponuje mechanizmami pozwalającymi konfigurować automatyczne uruchamianie kompilacji. Projekt w Jenkinsie może być kompilowany w ustalonych odstępach czasowych; Jenkins może też odpytywać repozytorium i uruchamiać kompilację po wykryciu pojawienia się nowej wersji; odpytywanie również odbywa się w konfigurowalnych odstępach czasu. Odstępy mogą być definiowane w formacie charakterystycznym dla mechanizmu cron, pozwalającym na precyzyjne, choć nieco nieczytelne oznaczenie planu uruchamiania. Na szczęście Jenkins proponuje porządną pomoc w postaci dokumentacji online, a jeśli nie potrzebujemy bardzo precyzyjnego harmonogramowania, możemy skorzystać z wygodnych predefiniowanych aliasów: @hourly (co godzinę), @midnight (każdego dnia o północy), @daily (raz dziennie), @weekly (raz na tydzień) oraz @monthly (raz na miesiąc). Na rysunku 20.11 widać konfigurację automatycznego uruchamiania kompilacji raz dziennie oraz po każdej wykrytej zmianie wersji w systemie kontroli wersji (sprawdzanie odbywa się co godzinę).

Rysunek 20.11. Planowanie automatycznego uruchamiania kompilacji

Błędy testów Wszystko świetnie się udało, więc jesteśmy dobrej myśli, mimo że projekt userthing bodaj tylko ze względów estetycznych nie kwalifikuje się jeszcze do włączenia do bazy kodu Zend. Ale skuteczny test to taki, który „wyłoży” kod, więc spróbujmy coś popsuć, aby sprawdzić, czy Jenkins rzeczywiście to wychwyci. Oto fragment klasy o nazwie Validate w przestrzeni nazw userthing\util: public function validateUser($mail, $pass) { // popsujmy! return false; $user = $this->store->getUser($mail); if (is_null($user)) { return null;

444

ROZDZIAŁ 20.  CIĄGŁA INTEGRACJA KODU

} if ($user->getPass() == $pass) { return true; } $this->store->notifyPasswordFailure($mail); return false; }

Oto skuteczny sabotaż metody: teraz validateUser() zawsze zwróci false. A oto test, który powinien wykryć dziwne zachowanie metody validateUser() — z pliku test/ValidatorTest.php: public function testValidate_CorrectPass() { $this->assertTrue( $this->validator->validateUser("[email protected]", "12345"), "Oczekiwano udanej weryfikacji użytkownika" ); }

Po wprowadzeniu zmiany wystarczy zatwierdzić ją do repozytorium i chwilę odczekać. Niebawem status projektu będzie pokazywał kompilację oznaczoną alarmującą, czerwoną ikoną błędów. Po kliknięciu odnośnika do statusu kompilacji zobaczymy więcej danych o przyczynach niepowodzenia kompilacji — mniej więcej jak na rysunku 20.12.

Rysunek 20.12. Nieudana kompilacja projektu Momentalnie otrzymamy także wiadomość e-mail zatytułowaną Build failed in Jenkins: userthing #9. Wiadomość będzie zawierała dane o ostatniej zmianie zatwierdzonej do repozytorium, przebiegu procesu kompilacji i przebiegu nieudanych testów jednostkowych.

445

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Podsumowanie W niniejszym rozdziale udało się zebrać większość narzędzi omawianych z osobna w poprzednich rozdziałach i zmontować z nich i z serwera Jenkins. W ramach ćwiczeń udało się zmontować prosty projekt przykładowy dla ciągłej integracji i poddać go działaniu kompletu narzędzi ciągłej integracji: PHPUnit (w zakresie testowania i analizy pokrycia kodu testami), PHP_CodeSniffer, PHP_CodeBrowser, phpDocumentor i Git. Następnie uruchomiliśmy serwer Jenkins i przećwiczyliśmy definiowanie projektu w ramach systemu ciągłej integracji. Na koniec udało się zmusić Jenkinsa do aktywnego powiadamiania o błędach kompilacji, a także zdefiniować zadania pozwalające kontrolować również proces budowania i instalowania pakietów.

446

ROZDZIAŁ 21 

Obiekty, wzorce, narzędzia

Od podstaw programowania obiektowego w PHP, przez zasady i wzorce projektowe, po narzędzia i techniki, wszystko omawialiśmy pod kątem jednego i tego samego celu: skutecznego projektowania w PHP. W niniejszym rozdziale pozwolę sobie podsumować i przypomnieć omawiane już zagadnienia:  PHP i obiekty — o tym, jak PHP polepsza mechanizmy programowania obiektowego i jak z tego skorzystać.  Obiekty i projektowanie — mowa będzie o podstawowych zasadach projektowania obiektowego.  Wzorce — i to, co nas w nich urzeka.  Zasady stosowania wzorców — czyli najważniejsze zasady projektowe leżące u podstaw wielu wzorców.  Narzędzia wspomagające programistę — wrócimy do narzędzi już omówionych, poznamy też kilka nowych.

Obiekty Z rozdziału 2. wiemy, że obiekty długi czas były w PHP jedynie niepełnowartościowym dodatkiem. Obsługa obiektów w PHP3 była raczej szczątkowa — tam obiekty były w zasadzie tablicami asocjacyjnymi w przebraniu. Entuzjaści programowania obiektowego doczekali się pewnych usprawnień obiektowych w PHP4, ale wciąż obiektowość była tam mocno kaleka — choćby z powodu domyślnego przekazywania i przypisywania obiektów przez referencje. Wraz z pojawieniem się PHP5 obiekty wreszcie stały się osią programowania w PHP. W PHP5 można co prawda wciąż z powodzeniem programować bez deklarowania jednej choćby klasy, ale bez wątpienia sam język został już zoptymalizowany pod kątem projektowania obiektowego. Mechanizmami obiektowości w PHP zajmowaliśmy się przede wszystkim w rozdziałach 3., 4. i 5., gdzie omawiane były szczegóły owych mechanizmów. Nie zabrakło też omówienia nowości obiektowych wprowadzonych w piątej wersji PHP: introspekcji, wyjątków, metod i składowych prywatnych i zabezpieczonych, metod i składowych finalnych, metody __toString(), modyfikatora static, klas i metod abstrakcyjnych, interfejsów, iteratorów, metod przechwytujących, sygnalizowania typów argumentów, modyfikatora const, przekazywania przez referencję, wywołania __clone(), metody __construct(), późnego wiązania składowych statycznych i przestrzeni nazw. Sama długość tej (niekompletnej przecież) listy pokazuje, że PHP pewnie zmierza ku możliwie pełnej obiektowości. Przydałoby się jeszcze kilka elementów (często proponowanych i omawianych): proponowałbym sygnalizowanie typów argumentów również dla typów elementarnych. Nie od rzeczy byłoby też wprowadzenie sygnalizacji typu zwracanego — tak, aby w deklaracji metody można było deklarować również typ wartości zwracanej. Silnik PHP mógłby wtedy wymuszać zgodność typu zwracanego zarówno dla metod bieżących, jak i ewentualnych metod przesłaniających daną metodę w hierarchii klas.

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Ale to wszystko jedynie braki pomniejsze. Ważne, że Zend Engine 2, czyli PHP5, zmienił ukierunkowanie języka i projektów realizowanych w tym języku, odkrywając go przed kolejnymi rzeszami programistów i dając nowe możliwości jego obecnym zwolennikom. W rozdziale 6. poznaliśmy korzyści, z jakimi wiąże się obiektowe zorientowanie projektowania. Ponieważ zaś obiekty i projektowanie były głównymi treściami tej książki, warto przypomnieć niektóre z wniosków, do których doszliśmy.

Wybór Nie istnieje żaden nakaz zmuszający programistów do wyłącznego korzystania z klas i obiektów. Właściwie zaprojektowany kod obiektowy daje przejrzysty i czytelny interfejs, który można wykorzystywać w dowolnym kodzie, również strukturalnym. I nawet jeśli nie interesuje Cię tworzenie obiektów (co dopuszczam tylko pro forma, bo inaczej nie dotarłbyś w lekturze aż tu), z pewnością będziesz je wykorzystywał, choćby tylko z pozycji użytkownika pakietów repozytorium PEAR.

Hermetyzacja i delegowanie Obiekty zajmują się własnymi sprawami i wykonują swoje zadania za zamkniętymi dla gapiów drzwiami. Udostępniają jedynie interfejs umożliwiający inicjowanie żądań i odbieranie wyników. Wszelkie dane obiektów, które nie muszą być eksponowane, jak i szczegóły implementacji obiektów, powinny pozostać za owymi drzwiami. Zasada hermetyzacji wprowadza istotną różnicę pomiędzy projektami obiektowymi i proceduralnymi. Część sterująca jest zazwyczaj w projektach obiektowych zaskakująco oszczędna i składa się w sumie jedynie z zestawu wywołań (statycznych) tworzących egzemplarze obiektów i wywołań ich metod przekazujących dane pomiędzy sobą. Dla porównania, ta część w projektach proceduralnych jest zwykle mocno rozbudowana — logika sterowania jest w znacznym stopniu i dość silnie połączona z implementacją, odwołując się co i raz do zmiennych, szacując wartości zwracane, wybierając różne ścieżki wykonania na podstawie zbieranych zewsząd informacji.

Osłabianie sprzężenia Rozprzężanie polega na wyeliminowaniu współzależności komponentów projektu, tak aby zmiany w obrębie jednego komponentu miały minimalny wpływ na kształt pozostałych. Dobrze zaprojektowane obiekty są jednostkami mocno izolowanymi — nie muszą np. odwoływać się do otoczenia celem przypomnienia sobie wartości przetworzonych w poprzednim wywołaniu. Obiekty, zarządzając wewnętrzną reprezentacją stanu, zmniejszają potrzebę stosowania zmiennych globalnych — głównych winowajców silnego sprzężenia komponentów. Korzystając ze zmiennej globalnej, wiążemy jedną część systemu z inną. Jeśli komponent (obojętnie, czy będzie to klasa, funkcja czy blok kodu) odwołuje się do zmiennej globalnej, pojawia się ryzyko, że inny komponent przypadkowo użyje tej samej nazwy dla własnej zmiennej i podmieni jej wartość widoczną również u nas. Co gorsza, zawsze może też pojawić się komponent trzeci (i kolejne), polegający w swych zadaniach na wartościach ustawianych przez pierwszy. Zmieniając sposób działania pierwszego komponentu, możemy zakłócić działanie trzeciego. Celem projektowania obiektowego jest redukcja takich zależności i maksymalne usamodzielnienie komponentów systemu. Innym aspektem silnego sprzężenia jest problem powielania kodu. Tam, gdzie zachodzi potrzeba powtarzania tego samego algorytmu w różnych częściach projektu, mamy najwyraźniej do czynienia z silnym sprzężeniem. A co się dzieje, kiedy algorytm trzeba zmienić? Trzeba odnaleźć i zaktualizować wszystkie jego wystąpienia, a przy tym łatwo o przeoczenie skutkujące niestabilnością systemu. Powielanie kodu wynika z kolei często z wymuszania przepływu sterowania instrukcjami warunkowymi. Jeśli na przykład system ma działać w określony sposób w pewnych okolicznościach (na przykład podczas działania w systemie Linux) i w inny sposób w innych okolicznościach (na przykład w systemie Windows), nietrudno popaść w pułapkę szpikowania kodu wciąż tymi samymi instrukcjami warunkowymi. Jeśli w takim układzie zwiększymy zakres owych okoliczności i podejmowanych decyzji (np. definiując trzeci sposób działania

448

ROZDZIAŁ 21.  OBIEKTY, WZORCE, NARZĘDZIA

w systemie Mac OS), będziemy musieli odpowiednio uzupełnić wszystkie instrukcje warunkowe, przy czym również łatwo o przeoczenie. Programowanie obiektowe proponuje w takich przypadkach specjalną technikę — testy warunków można zastąpić polimorfizmem. Polimorfizm, znany też pod pojęciem przełączania klas, to transparentne stosowanie różnych klas pochodnych w zależności od okoliczności. Ponieważ każda z klas pochodnych udostępnia i obsługuje identyczny interfejs klasy bazowej, kod użytkujący taką hierarchię ani nie musi wiedzieć, ani troszczyć się o to, która ze specjalizowanych implementacji jest w danych okolicznościach wybierana. Oczywiście nie da się w programowaniu obiektowym wyeliminować kodu warunkowego całkowicie, ale można jego stosowanie ograniczyć. Kod warunkowy pozostaje w użyciu tam, gdzie trzeba wybrać podtyp mający zrealizować żądania użytkowników. Sęk w tym, że wybór ten jest dokonywany jednokrotnie i w jednym miejscu, znacznie redukując tak niepożądane sprzężenie.

Zdatność do wielokrotnego stosowania kodu Hermetyzacja promuje osłabianie sprzężenia, a to z kolei daje komponentom znaczny stopień zdatności do ponownego wykorzystania. Komponenty samowystarczalne i komunikujące się z otoczeniem wyłącznie za pośrednictwem swoich interfejsów publicznych mogą zostać osadzone w dowolnym kontekście, bez jakichkolwiek zmian w ich hermetycznym wobec otoczenia kodzie. Tyle teoria, w praktyce taki stopień usamodzielnienia i hermetyzacji jest osiągany bardzo rzadko. Nawet elegancki ortogonalny kod może być zakorzeniony w projekcie. Tworząc zestaw klas przeznaczonych do zarządzania treściami publikowanymi w konkretnej witrynie WWW, warto jednak poświęcić podczas fazy projektowania chwilę na zastanowienie się, które z elementów projektu będą specyficzne dla tego projektu i wdrożenia, a które będzie można wykorzystać jako podstawę przyszłych projektów polegających na zarządzaniu treścią. Porada co do ponownego stosowania kodu: centralizuj klasy, które mogłyby zostać wykorzystane w wielu projektach. Innymi słowy, nie kopiuj „zdatnej do ponownego użycia” klasy do nowego projektu. Spowoduje to sprzężenie na poziomie makro, kiedy to zmiana klasy w obrębie jednego projektu będzie musiała zostać odzwierciedlona w pozostałych. Lepiej kodem takiej klasy zarządzać za pośrednictwem centralnego i wspólnego dla owych projektów repozytorium.

Estetyka Ten argument nie przekona co prawda niechętnych, ale moim zdaniem kod obiektowy jest po prostu estetyczny. Wszystkie zawiłości implementacji są ukryte za eleganckimi i spójnymi interfejsami, przez co obiekt jawi się jego użytkownikowi jako narzędzie poręczne i lekkie. Osobiście uwielbiam zwłaszcza elegancję wynikającą z polimorfizmu, kiedy to jeden interfejs pozwala na manipulowanie nieraz zupełnie odmiennymi obiektami, przy czym odmienności tej zupełnie nie widać ze strony manipulującego; można wtedy stosować obiekty jak dobre klocki — wszystkie do siebie pasują. Oczywiście są też zdania odrębne. Faktycznie, kod obiektowy często zmusza do stosowania rozwlekłych nazw klas, które same jeszcze byłyby do strawienia, ale w wywołaniu statycznym z nazwą metody (z tak samo rozwlekłymi typami parametrów) staje ością w gardle. Dotyczy to zwłaszcza pakietów repozytorium PEAR z przyjętą w nich konwencją nazywania klas mającą emulować nieistniejące w PHP przestrzenie nazw. Widać światełko w tunelu, skoro mamy już te upragnione przestrzenie nazw — programiści PEAR na pewno nie zapomną o zgodności wstecz i nie rzucą się natychmiast modyfikować nomenklaturę kodu, ale z czasem klasyczne uciążliwe konwencje nazewnicze odejdą w zapomnienie. Warto przy tym wspomnieć, że najbardziej nawet eleganckie rozwiązanie nie zawsze jest najlepsze czy najbardziej efektywne. Czasami będzie nas kusić implementacja pełnego systemu obiektowego tam, gdzie swoje zadanie zupełnie dobrze spełni prosty skrypt z kilkoma wywołaniami systemowymi. Innym niekiedy uzasadnionym zarzutem wobec kodu obiektowego jest jakże częste powstawanie istnego kotła klas i obiektów, których powiązań nie sposób z pozoru rozwikłać. Zdarza się i tak, ale tutaj wielce pomocna jest odpowiednia dokumentacja opatrzona przykładami użycia.

449

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Wzorce Ostatnio w firmie, w której działalność jestem zaangażowany, o pracę starał się programista języka Java. W swoim życiorysie wyrażał skruchę, ubolewając, że ze wzorców projektowych korzysta od kilku dopiero lat. Owo charakterystyczne przeświadczenie, że wzorce to świeże odkrycie, wynika zapewne z rozgłosu, jaki ostatnio zyskały. Tymczasem jest wielce prawdopodobne, że ów doświadczony programista stosował (nienazwane) wzorce projektowe przez większość swojej praktyki zawodowej. Wzorce opisują powszechnie spotykane problemy i przetestowane rozwiązania tych problemów. Wzorce identyfikują, kodyfikują i organizują najlepsze doświadczenia społeczności programistów. Nie są wynalazkami ani klauzulami doktryny. Wzorzec byłby nieważny, gdyby nie opisywał praktyk stosowanych już powszechnie — inaczej nie mogłyby być sprawdzone. Pamiętajmy, że koncepcja wzorców projektowych wykluła się w dziedzinie architektury. Ludzie budowali dziedzińce i sklepienia od tysięcy lat, bez stosowania wzorców jako środków opisywania rozwiązań problemu zagospodarowania przestrzeni i funkcji tej przestrzeni. To powiedziawszy, trzeba dodać, że wzorce projektowe często wywołują emocje charakterystyczne dla dysput politycznych i religijnych. Dewocyjni zwolennicy wzorców przemierzają korytarze w iście ewangelicznym uniesieniu z egzemplarzem książki Bandy Czworga pod pachą. Zaczepiają tych, którzy nie znają jeszcze dobrej nowiny, i wymawiają nazwy wzorców z nabożną czcią. Nic dziwnego, że niektórzy z krytyków wzorców sądzą, że to lipa. W językach takich jak Perl i PHP wzorce są o tyle kontrowersyjne, że ściśle kojarzą się z językami obiektowymi. Kiedy obiektowość jest decyzją projektową, a nie rzeczą daną, przywiązanie do wzorców może być co najwyżej deklaracją preferencji.

Co dają nam wzorce? Wzorce wprowadziłem w rozdziale 7. Spróbujmy przypomnieć sobie niektóre z korzyści, jakie wtedy przedstawiałem.

Pewne i sprawdzone Po pierwsze, wzorce to sprawdzone „w boju” rozwiązania konkretnych problemów. Nakreślanie analogii pomiędzy wzorcami a receptami jest jednak nietrafione — recepty realizuje się ściśle, wzorce stanowią zaś produkt jedynie połowicznie przetworzony („niedopieczony” wedle Martina Fowlera) i wymagają znacznie większej staranności. Niemniej jednak i wzorce, i recepty mają ważną wspólną cechę: zostały wypróbowane i sprawdzone jeszcze przed przepisaniem.

Wzorce sugerują stosowanie innych wzorców Tak się składa, że jedne wzorce „pasują” do drugich. Niektóre dają się łączyć niemal od ręki. Rozwiązanie problemu wedle takiego czy innego wzorca będzie miało w każdym przypadku dalece idące konsekwencje. Owe konsekwencje często tworzą warunki do zastosowania kolejnego wzorca. Przy podejmowaniu decyzji o wdrożeniu tak powiązanych wzorców należy naturalnie zachować wstrzemięźliwość i rozważać ich konieczność, szacując rzeczywiste potrzeby, aby nie popaść w pułapkę konstruowania eleganckiego, ale bezużytecznego kodu. Łatwo wtedy o uzyskanie efektu, który w dziedzinie zagospodarowania przestrzeni określilibyśmy mianem architektonicznego bohomazu.

Wspólne słownictwo Wzorce to środki jednolitego opisu powszechnie obserwowanych problemów i ich rozwiązań. Wielkie znaczenie mają nazwy wzorców — mają nam wystarczyć za rozwlekłe opisy, pozwalając na efektywne komunikowanie pomysłów. A niewtajemniczonym powinny sugerować właściwe znaczenie.

450

ROZDZIAŁ 21.  OBIEKTY, WZORCE, NARZĘDZIA

Wzorce promują projektowanie Kwesta ta będzie dyskutowana w następnym punkcie; poprawnie stosowane wzorce zachęcają do prawidłowego projektowania. Ale też trzeba pamiętać, że wzorce nie są zaczarowanym ołówkiem programistów ani jedynym panaceum na ogromną złożoność dużych projektów.

Wzorce a zasady projektowe We wzorcach projektowych kładzie się spory nacisk na prawidłowe projektowanie rozwiązań. Właściwie stosowane mogą okazać się pomocne w budowaniu odpowiednio słabo sprzężonego i mocno elastycznego kodu. Krytycy mają rację, wskazując na nadużywanie wzorców przez nowo przekonanych. Implementacje wzorców projektowych dają eleganckie i miłe dla oka struktury, łatwo więc zapomnieć, że prawidłowe podejście projektowe zawsze zakłada mierzenie środków na cele. Wzorce mają rozwiązywać rzeczywiście obserwowane problemy. Kiedy sam zacząłem korzystać z wzorców, złapałem się na wdrażaniu wszędzie wzorca Abstract Factory. Musiałem przecież generować obiekty, a niewątpliwie ten wzorzec był w tym zadaniu pomocny. Okazało się jednak, że z lenistwa przysparzałem sobie jedynie dodatkowej pracy. Zestawy obiektów, które miałem generować, były w istocie powiązane, ale nie posiadały alternatywnych implementacji. Wzorzec Abstract Pattern nadaje się zaś w swym klasycznym wydaniu do stosowania tam, gdzie trzeba generować zbiory obiektów o alternatywnej implementacji w zależności od okoliczności. Uruchomienie wzorca wymaga utworzenia klas wytwórni dla każdego typu obiektu oraz klasy serwującej wytwórnie — już sam opis jest obszerny, a co dopiero implementacja. Mój kod byłby znacznie bardziej elegancki, gdybym utworzył klasę zwykłej wytwórni i poddawał ją refaktoryzacji jedynie tam, gdzie ujawniłaby się konieczność generowania równoległych zestawów obiektów. Samo korzystanie z wzorców nie zapewnia poprawności projektu. Dlatego podczas projektowania rozwiązań warto trzymać się dwóch zasad: „Prostota przede wszystkim”1 oraz „Wybierz najprostsze działające rozwiązanie”. Zwolennicy doktryny eXtreme Programming dołożyliby jeszcze jedną: „Nie będziesz tego potrzebował”, zachęcającą do rezygnacji z implementacji funkcji, które nie są w systemie absolutnie niezbędne. Po tym ostrzeżeniu możemy wrócić do tonu hurraoptymistycznego. Jak wyłożyłem w rozdziale 9., wzorce ucieleśniają pewien zestaw reguł projektowych, które można by śmiało uogólnić na projektowanie oprogramowania w ogóle.

Kompozycja lepsza od dziedziczenia Relacje dziedziczenia są bardzo efektywnym środkiem wyrazu. Dziedziczenie pozwala na dynamiczne przełączanie klas obiektów (polimorfizm), stanowiące rdzeń wielu wzorców i technik prezentowanych w książce. Ale polegając wyłącznie na dziedziczeniu, ryzykuje się zmniejszeniem elastyczności systemu i zwiększeniem stopnia powielania kodu.

Unikanie silnego sprzężenia O tym już mówiliśmy w niniejszym rozdziale, warto jednak dodać jeszcze słowo komentarza. Otóż całkowite i zupełne odizolowanie poszczególnych komponentów systemu jest mrzonką — zmiana w komponencie najczęściej bowiem wymaga jakichś zmian w jego otoczeniu. Można jednak oddźwięk takiej zmiany stłumić, unikając powielania kodu (analizowaliśmy stosowne przykłady z rozgałęzieniami kodu w instrukcjach warunkowych), nadużywania zmiennych globalnych (ale również nadużywania wzorca Singleton), a także redukowania stosowania konkretnych klas pochodnych tam, gdzie można by zastosować typ abstrakcyjny poparty polimorfizmem. To ostatnie prowadzi wprost do następnej zasady:

1

A w oryginale KISS, czyli Keep it simple, stupid — przyp. tłum.

451

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Programuj pod kątem interfejsu, nie implementacji Komponenty programowe należy projektować z jasno zdefiniowanymi publicznymi interfejsami i składać na nie ściśle ograniczony zakres odpowiedzialności, której ograniczenie jest jasne dla użytkowników. Jeśli zdefiniujemy interfejs w abstrakcyjnej klasie bazowej, a klasy użytkownika będą inicjowały żądania za pośrednictwem tegoż interfejsu, osiągniemy pożądane rozdzielenie użytkowników od implementacji. Pamiętajmy też o zasadzie minimalizmu funkcjonalnego („Nie będziesz tego potrzebował”). Jeśli początkowo widać potrzebę jednej tylko implementacji dla danego typu, nie warto od razu tworzyć abstrakcyjnej klasy bazowej. Jasny i czytelny interfejs może mieć przecież równie dobrze cała hierarchia, jak i pojedyncza klasa. Kiedy zaś okaże się, że zadania owej pojedynczej implementacji zaczynają się rozwidlać, dotychczasową klasę można uczynić abstrakcyjną klasą bazową dwóch klas konkretnych. Użytkownik nie zauważy nawet zmiany — wszak wciąż będzie operował na tym samym typie. Klasycznym sygnałem konieczności rozwidlenia implementacji i ukrycia klas poszczególnych gałęzi implementacji za interfejsem abstrakcyjnej klasy bazowej jest występowanie w tej implementacji nadmiernej liczby warunkowych rozgałęzień kodu.

Izoluj to, co zmienne Kiedy zorientujesz się, że pogrążasz się w klasach pochodnych, powinieneś spróbować wyodrębnić przyczynę ich mnożenia się do osobnego typu. Dotyczy to zwłaszcza tych sytuacji, kiedy tą przyczyną jest zadanie klasy poboczne ze względu na jej odpowiedzialność podstawową. Jeśli na przykład przyłapiemy się na tworzeniu dla typu UpdatableThing pochodnych FtpUpdatableThing, HttpUpdatableThing i FileSystemUpdatableThing, to powinniśmy pamiętać, że podstawowym zadaniem pierwotnego typu było reprezentowanie czegoś, co da się aktualizować — mechanizmy pobierania informacji niezbędnych do aktualizacji (FTP, HTTP, lokalny system plików) są wobec tego zadania poboczne. Elementami zmiennymi są tu sposoby aktualizacji (Ftp, Http i FileSystem) i wypadałoby dla nich przewidzieć odrębny typ, np. UpdateMechanism. Typ ten można specjalizować dla różnych mechanizmów pobierania aktualizacji bez zakłócania samej aktualizacji, czyli głównego (a teraz już jedynego) zadania klasy UpdatableThing. Zauważmy, że przy okazji zastąpiliśmy strukturę statyczną powoływaną do życia „w czasie kompilacji” (w cudzysłowie, bo kompilacji tu nie uświadczymy) ułożeniem dynamicznym, zataczając tym samym krąg i wracając do zasady pierwszej, mówiącej o wyższości kompozycji nad dziedziczeniem.

Narzędzia Kwestie omawiane w tej części książki (a zapowiadane w rozdziale 14.) są często traktowane po macoszemu zarówno przez publicystów, jak i samych programistów. W trakcie mojej kariery programistycznej odkryłem zaś, że owe narzędzia są dla powodzenia projektu czynnikami o wadze równej starannemu i właściwemu projektowaniu. Mam więc wątpliwości, czy gromadzenie dokumentacji i automatyzacja kompilacji i instalacji są mniej ważne niż takie cuda jak wzorzec Composite.  Uwaga Z całym szacunkiem dla wzorca Composite, którego elegancji nie da się nie docenić: wszak daje on proste drzewo dziedziczenia, którego obiekty mogą być dynamicznie układane w struktury drzewiaste, ale o całe rzędy wielkości elastyczniejsze i bardziej złożone niż sama hierarchia dziedziczenia; mamy tu wiele obiektów dzielących interfejs prezentowany jednomyślnie otoczeniu. Owo przeciąganie liny pomiędzy złożonym a prostym, pojedynczym i wielokrotnym, to chyba coś więcej niż projektowanie — to już niemal poezja.

Choć kwestie dokumentowania kodu, automatyzacji kompilacji, instalacji, testowania oraz kontroli wersji są bardziej prozaiczne niż wzorce, nie są wcale mniej ważne. W brutalnym świecie praktyki nawet najpiękniejszy projekt nie przetrwa, kiedy implementujący go programiści nie będą mieli swobody dostępu do kodu albo nie będą go rozumieli. Bez narzędzi automatyzujących testowanie trudno zapewnić jakość systemu. Bez wygodnych narzędzi kompilacji i instalacji nikt nie będzie chciał ślęczeć nad wdrożeniem naszego dzieła. A w miarę

452

ROZDZIAŁ 21.  OBIEKTY, WZORCE, NARZĘDZIA

zwiększania się liczby użytkowników aplikacji PHP rośnie odpowiedzialność programistów za dostarczanie aplikacji wysokiej jakości i łatwych w instalacji i rozruchu. Każdy projekt istnieje na dwóch płaszczyznach. Po pierwsze, jest zlepkiem struktur kodu realizującego funkcje, ale równocześnie zbiorem plików i katalogów, niwą współpracy, zbiorem źródeł i celów i przedmiotem przekształceń. Owo zewnętrzne ujęcie projektu warunkuje jego istnienie jako systemu w takim samym stopniu jak kod. Mechanizmy kompilacji, testowania, dokumentowania i kontroli wersji, czyli ów całościowy metasystem, powinny więc cieszyć się taką samą uwagą jak obsługiwany przez nie kod.

Testowanie Testowanie to ta część systemu, która jest zewnętrzna wobec kodu, ale nierozerwalnie z nim związana. Ponieważ nie możemy i nie chcemy tych dwóch rzeczy rozdzielić, powinniśmy w obrębie kodu systemu przewidzieć również kod testujący jako sposób monitorowania zasięgu oddźwięku wprowadzanych do właściwego kodu zmian. W rozdziale poświęconym częściowo testowaniu przekonaliśmy się bowiem, że z pozoru lokalna zmiana, np. zmiana typu wartości zwracanej z metody, może zakłócić poprawność kodu w dość odległym miejscu systemu, a co gorsza, tak powstałe błędy mogą ujawniać się po całych dniach, tygodniach czy nawet miesiącach od wprowadzenia zmiany. Odpowiednia infrastruktura testowa daje szansę ich natychmiastowego wychwytywania (im lepsze testy, tym większa szansa). Testowanie popłaca również jako środek poprawiania obiektowego projektu systemu. Testowanie interfejsu klasy przed albo choćby współbieżnie z jej implementacją wymusza na programiście dokładność w projektowaniu tego interfejsu i staranne określanie zadań i odpowiedzialności poszczególnych jego metod. W książce testowaniem zajmowaliśmy się w rozdziale 18., omawiając PHPUnit.

Dokumentacja Kod wcale nie jest tak przejrzysty i czytelny, jak jawi się jego autorowi. Osoba niewtajemniczona, po raz pierwszy mająca okazję analizować nieznany sobie kod, zawsze staje w obliczu poważnego wyzwania. Zresztą nawet autor kodu w końcu zapomina, jak to wszystko działa. W rozdziale 16. wypróbowywaliśmy więc program phpDocumentor, dzięki któremu dokumentację mogliśmy osadzać wprost w kodzie, przy okazji jego pisania, a potem automatycznie ją stamtąd wyodrębniać. Dokumentacja generowana przez phpDocumentor jest szczególnie przydatna w kontekście obiektowym, ponieważ program ten potrafi na podstawie samego kodu dokumentować podstawowe cechy klas i wygenerować ich hipertekstowy wykaz. Znakomicie ułatwia to analizę projektu, zwłaszcza kiedy klasy są rozsiane pomiędzy różnymi plikami kodu źródłowego.

Zarządzanie wersjami Praca zespołowa to zawsze wyzwanie. Powiedzmy to sobie wprost: ludzie są dziwaczni. Programiści są zaś jeszcze gorsi. Po przydzieleniu ról w zespole i zakresów zadań poszczególnych jego członków ostatnią pożądaną rzeczą są tarcia występujące w kodzie źródłowym. Jak przekonaliśmy się w rozdziale 17., Git (i podobne narzędzia, choćby CVS i Subversion) pozwala na skuteczne scalanie efektów pracy wielu programistów w jednym, centralnym repozytorium kodu. Tam, gdzie kolizje są nieuniknione, Git musi odwołać się do czynnika ludzkiego, ułatwia jednak uzgodnienie kolidujących wersji, wskazując źródło kolizji. Nawet programista pracujący sam doceni zalety narzędzi kontroli wersji. Może bowiem dzięki nim rozgałęziać swoje projekty i izolować prace rozwijające następne wersje projektu od prac typowo konserwatorskich, polegających na łataniu dziur, nie pozbywając się możliwości włączenia wszystkich powstałych tak poprawek do głównej gałęzi rozwoju projektu, kiedy ten osiągnie już kolejny stabilny punkt. Git utrzymuje poza tym rejestr wszystkich zmian, jakie wprowadzono kiedykolwiek do projektu. Oznacza to, że zawsze można cofnąć się do wersji z konkretnej daty bądź oznaczonej konkretną etykietą. Uwierz mi, możliwość ta pewnego dnia uratuje Twój projekt przed zapaścią.

453

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Automatyczna kompilacja (instalacja) Efektywne zarządzanie wersjami na niewiele się zdaje, kiedy nie dysponujemy efektywnym sposobem kompilacji (instalacji). Wdrożenie projektu o pewnej złożoności zawsze jest pracochłonne. Trzeba umieścić różne pliki w rozmaitych katalogach, wypełnić pliki konfiguracji poprawnymi dla danego kontekstu wdrożenia wartościami, utworzyć brakujące albo przekształcić istniejące tabele baz danych i tak dalej. W książce udało mi się przedstawić dwa narzędzia automatyzujące instalację. Pierwsze z nich, instalator repozytorium PEAR (rozdział 15.), sprawdza się w instalacji niewielkich, spójnych aplikacji i samodzielnych pakietów. Drugie tego typu narzędzie, Phing, czytelnicy poznali w rozdziale 19., przekonując się, że jego możliwości i elastyczność pozwalają na zastosowanie go w najbardziej nawet rozbudowanych i zawiłych instalacjach. Automatyzacja sprowadza koszmar instalacji do jednego bądź dwóch poleceń. Przy minimalnym wysiłku można wywoływać z poziomu narzędzia kompilacji elementy infrastruktury testowej i generować dokumentację. Jeśli nie przekonują Cię do tego utyskiwania programistów, pamiętaj chociaż o użytkownikach końcowych, którzy z wielką ulgą przyjmą do wiadomości fakt, że nie muszą już całe popołudnie ślęczeć nad kopiowaniem plików i zmienianiem parametrów konfiguracji przy każdej aktualizacji ich nabytku (a Twojego dzieła) do nowej wersji.

System integracji ciągłej Nic to, że trzeba testować i kompilować projekt — najgorsze, że trzeba to robić niemal stale. A im większy projekt, tym rola tych etapów jest większa — zwłaszcza kiedy rozwój projektu odbywa się już w wielu gałęziach. Testowanie powinno dotyczyć przede wszystkim gałęzi stabilnej, w której dopuszcza się jedynie pomniejsze poprawki znalezionych błędów, ale również gałęzi rozwojowych (niekiedy wielu) oraz głównego korpusu kodu. Gdybyśmy chcieli przeprowadzić to ręcznie, nawet za pomocą narzędzi testujących, nie znaleźlibyśmy ani chwili czasu na programowanie. Programistom to nie w smak, więc kończy się to zazwyczaj… zaniedbaniem testów. W rozdziale 20. przyjrzeliśmy się mechanizmom tzw. integracji ciągłej, czyli praktyce oraz narzędziom automatyzującym proces kompilowania projektu i jego testowania w możliwie szerokim zakresie.

Co pominęliśmy? Ze względu na brak czasu i miejsca zmuszony byłem pominąć prezentację kilku narzędzi użytecznych w projektach programistycznych. Pierwszym i najważniejszym spośród nich jest Bugzilla. Nazwa ta powinna sugerować dwie rzeczy. Po pierwsze, to narzędzie związane jakoś z wykrywaniem błędów (ang. bug). Po drugie, stanowi część projektu Mozilla. Bugzilla, podobnie jak Git, to jedno z tych narzędzi, które raz wypróbowane zdają się nieodzowne. Bugzillę można pobrać z witryny http://www.bugzilla.org/. Narzędzie to ma służyć użytkownikom naszych aplikacji do zgłaszania zauważonych problemów, ale z mojego doświadczenia wynika, że równie dobrze można je stosować do opisywania prac czekających na wykonanie i rozdysponowywania ich pomiędzy członkami zespołu programistycznego. W każdej chwili można uzyskać wykaz błędów, zawężając go ewentualnie wedle konkretnego produktu, „właściciela” błędów, numeru wersji i priorytetu. Każdy błąd dysponuje swoją stroną, w ramach której można dyskutować pojawiające się przy jego rozwiązywaniu kwestie. Głosy w dyskusji i zmiany statusu błędu można rozpowszechniać za pośrednictwem poczty elektronicznej wśród członków zespołu, co pozwala im na śledzenie na bieżąco postępu prac. Każdy poważny projekt potrzebuje przynajmniej jednej listy dystrybucyjnej poczty elektronicznej, za pośrednictwem której użytkownicy mogą otrzymywać informacje o dotyczących ich zmianach, a programiści mogą dyskutować o architekturze i przydziale zasobów. Moim ulubionym oprogramowaniem zarządzającym taką listą jest Mailman (http://www.gnu.org/software/mailman/) — program darmowy, prosty w instalacji i dający się wszechstronnie konfigurować. Jeśli zaś nie chcesz instalować specjalnego oprogramowania dla listy dystrybucyjnej swojego projektu, możesz skorzystać z usług jednej z licznych witryn, w ramach której można takie listy łatwo (i za darmo) uruchomić.

454

ROZDZIAŁ 21.  OBIEKTY, WZORCE, NARZĘDZIA

Nie sposób przecenić znaczenia dokumentacji osadzanej w kodzie, ale poważny projekt obrasta w sterty materiałów pisanych o nieco innym charakterze. Trzeba do nich zaliczyć instrukcje obsługi, konsultacje dotyczące dalszego rozwoju, terminarze spotkań i wreszcie zaproszenia na imprezy. W czasie życia projektu materiały tego rodzaju ujawniają swój płynny charakter i trzeba znaleźć sposób na umożliwienie zespołowi współpracy przy ich ustalaniu. Do tworzenia sieci hipertekstowych dokumentów świetnie nadaje się Wiki (co po hawajsku ma oznaczać „bardzo szybki”). Dodawanie i edycja stron to kwestia kliknięcia przycisku, a dla umieszczanych w dokumencie słów pasujących do nazw innych stron Wiki automatycznie generuje stosowne odnośniki. To kolejne z tych poręcznych, prostych i niezwykle przydatnych narzędzi, które samemu wymyśliło się już dawno, ale nie miało okazji wprowadzić pomysłu w czyn. Osobiście miałem świetne doświadczenia z PhpWiki (do pobrania spod adresu http://phpwiki.sourceforge.net) oraz DokuWiki (http://wiki.splitbrain.org/wiki:dokuwiki).

Podsumowanie Cały ten rozdział miał charakter podsumowania i przypominał najważniejsze zagadnienia omawiane w poszczególnych częściach książki. Próżno by w nim szukać konkretów w postaci opisów pojedynczych wzorców czy funkcji obiektowych, ale jest chyba reprezentatywny dla całości książki. Każdy autor staje w obliczu ograniczeń czasu i miejsca uniemożliwiających omówienie wszystkiego, co w danej dziedzinie jest znane. Mam jednak nadzieję, że Czytelnik po lekturze tej książki jedno wie na pewno — że PHP dojrzewa. Już jest najpopularniejszym na świecie językiem programowania. Mam nadzieję, że wciąż pozostanie ulubionym językiem hobbistów i wciąż będzie zadziwiał kolejnych początkujących efektami, jakie można osiągnąć śladowymi wprost ilościami kodu. Równocześnie jednak język ten staje się podstawą coraz większej liczby rozległych systemów, tworzonych przez profesjonalne zespoły programistów. W takich projektach typowe dla dotychczasowych aplikacji PHP podejście ukierunkowane na natychmiastowy efekt nie może się obronić. PHP od zawsze był językiem wszechstronnym, udostępniającym (na bazie mechanizmu rozszerzeń) setki bibliotek i aplikacji. Teraz to także język obiektowy, a więc dający programiście kolejny zestaw poręcznych narzędzi semantycznych. A kiedy już zacznie się myśleć obiektowo, można garściami czerpać z dorobku innych programistów: można przebierać we wzorcach projektowych opracowanych nie tylko dla PHP, ale choćby dla takich języków jak Smalltalk, C++, C# czy Java. Tylko od nas zależy, czy zrobimy to dobrze — w oparciu o dobry projekt i sprawdzone techniki. Przyszłość, tak czy owak, należy do kodu wielokrotnego użytku.

455

PHP. OBIEKTY, WZORCE, NARZĘDZIA

456

DODATEK A 

Bibliografia

Książki Alexander Christopher, Ishikawa Sara, Silverstein Murray, Jacobson Max, Fiksdahl-King Ingrid i Angel Shlomo, A Pattern Language: Towns, Buildings, Construction, Oxford University Press, 1977. Alur Deepak, Crupi John i Malks Dan, Core J2EE Patterns: Best Practices and Design Strategies1, Prentice Hall, 2001. Beck Kent, Extreme Programming Explained: Embrace Change, Addison-Wesley, 1999. Chacon Scott, Pro Git, New York, Apress, 2009. Fogel Karl i Bar Moshe, Open Source Development with CVS, wydanie trzecie, Paraglyph Press, 2003. Fowler Martin i Scott Kendall, UML Distilled: A Brief Guide to the Standard Object Modelling Language, Addison-Wesley, 1999. Fowler Martin, Beck Kent, Brant John, Opdyke William i Roberts Don, Refactoring: Improving the Design of Existing Code, Addison-Wesley, 1999. Fowler Martin, Patterns of Enterprise Application Architecture2, Addison-Wesley, 2003. Gamma Erich, Helm Richard, Johnson Ralph, Vlissides John, Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1995. Hunt Andrew i Thomas David, The Pragmatic Programmer: From Journeyman to Master3, Addison-Wesley, 2000. Kerievsky Joshua, Refactoring to Patterns, Addison-Wesley, 2004. Metsker Steven John, Building Parsers with Java, Addison-Wesley 2001. Nock Clifton, Data Access Patterns: Database Interactions in Object-Oriented Applications, Addison-Wesley, 2004. Shalloway Alan i Trott James R., Design Patterns Explained: A New Perspective on Object Oriented Design4, Addison-Wesley, 2002. Stelling Stephen i Maasen Olav, Applied Java Patterns, Sun Microsystems Press, 2002. 1 2 3 4

Wydania polskie: J2EE. Wzorce projektowe, Helion, 2003 oraz J2EE. Wzorce projektowe. Wydanie 2, Helion, 2004 — przyp. tłum. Wydanie polskie: Architektura systemów zarządzania przedsiębiorstwem. Wzorce projektowe, Helion, 2005 — przyp. tłum. Wydanie polskie: Pragmatyczny programista. Od czeladnika do mistrza, WNT, 2002 — przyp. tłum. Wydania polskie: Projektowanie zorientowane obiektowo. Wzorce projektowe, Helion, 2002 oraz Projektowanie zorientowane obiektowo. Wzorce projektowe. Wydanie 2, Helion, 2005 — przyp. tłum.

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Publikacje Beck Kent, Gamma Erich, Test Infected: Programmers Love Writing Tests (http://junit.sourceforge.net/doc/testinfected/testing.htm). Collins-Sussman Ben, Fitzpatrick Brian W., Pilato C. Michael, Version Control with Subversion (http://svnbook.red-bean.com/). Lerdorf Rasmus, PHP/FI Brief History (http://www.php.net//manual/phpfi2.php#history). Gutmans Andi, Zend’s Andi Gutmans on PHP 6 & how Apple is the „biggest barrier” to mobile’s future (http://venturebeat.com/2012/10/24/zends-andi-gutmans-on-php-6-being-a-developer-ceo-and-how-appleis-the-biggest-barrier-to-the-future-of-mobile/). Suraski Zeev, The Object Oriented Evolution of PHP (http://www.devx.com/webdev/Article/10007/0/page/1).

Witryny WWW Bugzilla: http://www.bugzilla.org CruiseControl: http://cruisecontrol.sourceforge.net/ CVS: http://www.cvshome.org/ CvsGui: http://www.wincvs.org/ CVSNT: http://www.cvsnt.org/wiki DokuWiki: http://wiki.splitbrain.org/wiki:dokuwiki Foswiki: http://foswiki.org/ Eclipse: http://www.eclipse.org/ Java: http://www.java.com Jenkins: http://jenkins-ci.org/ GNU: http://www.gnu.org/ Git: http://git-scm.com/ Git book: http://git-scm.com/book Google Code: http://code.google.com Mailman: http://www.gnu.org/software/mailman/ Martin Fowler: http://www.martinfowler.com/ Memcached: http://danga.com/memcached/ Mercurial: http://mercurial.selenic.com OpenPear: http://openpear.org/ Phing: http://phing.info/trac/ PHPUnit: http://www.phpunit.de PhpWiki: http://phpwiki.sourceforge.net

458

DODATEK A  BIBLIOGRAFIA

PEAR: http://pear.php.net PECL: http://pecl.php.net/ Phing: http://phing.info/ PHP: http://www.php.net PhpWiki: http://phpwiki.sourceforge.net PHPDocumentor: http://www.phpdoc.org/ Pirum: http://pirum.sensiolabs.org Portland Pattern Repository’s Wiki (Ward Cunningham): http://www.c2.com/cgi/wiki Pyrus: http://pear2.php.net RapidSVN: http://rapidsvn.tigris.org/ QDB: http://www.bash.org Selenium: http://seleniumhq.org/ SPL: http://www.php.net/spl Subversion: http://subversion.apache.org/ Ximbiot — CVS Wiki: http://ximbiot.com/cvs/wiki/ Xdebug: http://xdebug.org/ Xinc: http://code.google.com/p/xinc/ Zend: http://www.zend.com

459

PHP. OBIEKTY, WZORCE, NARZĘDZIA

460

DODATEK B 

Prosty analizator leksykalny

Omawiany w rozdziale 11. wzorzec Interpreter nie obejmował zadania samego tzw. parsowania, czyli analizy leksykalnej, a Interpreter bez takiego analizatora jest dość bezużyteczny, chyba że zmusimy użytkowników do samodzielnego pisania kodu PHP wywołującego interpreter! Do współdziałania z interpreterem można by co prawda wdrożyć gotowe analizatory leksykalne autorstwa osób trzecich; w prawdziwym projekcie byłoby to zapewne najlepsze wyjście. W tym dodatku pokuszę się jednak o prezentację i omówienie uproszczonego analizatora leksykalnego przystosowanego do współpracy z interpreterem MarkLogic z rozdziału 11. Nie należy zapominać, że będzie to raczej zarys koncepcyjny takiego analizatora — przykład ten, choć działający, niekoniecznie musi nadawać się do bezpośredniego stosowania w praktyce.  Uwaga Interfejs i ogólną strukturę niniejszego analizatora zaczerpnąłem po części z książki Stevena Metskera Building Parsers with Java (Addison-Wesley, 2001). Uproszczoną brutalnie implementację i zasiane w niej ewentualne błędy biorę jednak na siebie. Mam też pozwolenie Stevena na wykorzystanie jego koncepcji.

Skaner Żeby móc przeanalizować wyrażenie, trzeba najpierw rozbić je na zestaw słów i znaków, czyli elementów leksykalnych (ang. tokens). Elementy te zostaną zdefiniowane za pośrednictwem wyrażeń regularnych w poniższej klasie. Klasa ta będzie też pełnić rolę stosu pomocnego w następnych punktach. Oto ona: namespace gi\parse; class Scanner { // rodzaje elementów leksykalnych const WORD = 1; const QUOTE = 2; const APOS = 3; const WHITESPACE = 6; const EOL = 8; const CHAR = 9; const EOF = 0; const SOF = -1; protected $line_no = 1; protected $char_no = 0; protected $token = null;

PHP. OBIEKTY, WZORCE, NARZĘDZIA

protected $token_type = -1; // Reader daje dostęp do "surowych" danych, Context // przechowuje dane wynikowe function __construct( Reader $r, Context $context ) { $this->r = $r; $this->context = $context; } function getContext() { return $this->context; } // pominięcie znaków odstępu function eatWhiteSpace( ) { $ret = 0; if ( $this->token_type != self::WHITESPACE && $this->token_type != self::EOL ) { return $ret; } while ( $this->nextToken() == self::WHITESPACE || $this->token_type == self::EOL ) { $ret++; } return $ret; } // pobranie symbolicznej reprezentacji elementu leksykalnego // (bieżącego albo wskazanego argumentem $int) function getTypeString( $int=-1 ) { if ( $inttokenType(); } if ( $int 'WORD', self::QUOTE => 'QUOTE', self::APOS => 'APOS', self::WHITESPACE => 'WHITESPACE', self::EOL => 'EOL', self::CHAR => 'CHAR', self::EOF => 'EOF' ); return $resolve[$int]; } // bieżący rodzaj elementu (reprezentowany liczbowo) function tokenType() { return $this->token_type; } // zawartość bieżącego elementu function token() { return $this->token; } // TRUE, jeśli bieżący element to słowo function isWord( ) { return ( $this->token_type == self::WORD ); } // TRUE, jeśli bieżący element to znak cudzysłowu

462

DODATEK B  PROSTY ANALIZATOR LEKSYKALNY

function isQuote( ) { return ( $this->token_type == self::APOS || $this->token_type == self::QUOTE ); } // numer bieżącego wiersza w strumieniu źródłowym function line_no() { return $this->line_no; } // numer bieżącego znaku w strumieniu źródłowym function char_no() { return $this->char_no; } // klonowanie obiektu function __clone() { $this->r = clone($this->r); } // Przejście do następnego elementu w strumieniu. Ustawienie // bieżącego elementu i zapamiętanie numeru wiersza i znaku. function nextToken() { $this->token = null; $type; while ( ! is_bool($char=$this->getChar()) ) { if ( $this->isEolChar( $char ) ) { $this->token = $this->manageEolChars( $char ); $this->line_no++; $this->char_no = 0; $type = self::EOL; return ( $this->token_type = self::EOL ); } else if ( $this->isWordChar( $char ) ) { $this->token = $this->eatWordChars( $char ); $type = self::WORD; } else if ( $this->isSpaceChar( $char ) ) { $this->token = $char; $type = self::WHITESPACE; } else if ( $char == "'" ) { $this->token = $char; $type = self::APOS; } else if ( $char == '"' ) { $this->token = $char; $type = self::QUOTE; } else { $type = self::CHAR; $this->token = $char; } $this->char_no += strlen( $this->token() ); return ( $this->token_type = $type ); } return ( $this->token_type = self::EOF );

463

PHP. OBIEKTY, WZORCE, NARZĘDZIA

} // zwraca tablicę (typ elementu i zawartość elementu) // dla NASTĘPNEGO elementu w strumieniu function peekToken() { $state = $this->getState(); $type = $this->nextToken(); $token = $this->token(); $this->setState( $state ); return array( $type, $token ); } // pobranie obiektu ScannerState reprezentującego bieżącą // pozycję skanera w strumieniu i dane o bieżącym elemencie function getState() { $state = new ScannerState(); $state->line_no = $this->line_no; $state->char_no = $this->char_no; $state->token = $this->token; $state->token_type = $this->token_type; $state->r = clone($this->r); $state->context = clone($this->context); return $state; } // użycie obiektu ScannerState do przywrócenia stanu skanera function setState( ScannerState $state ) { $this->line_no = $state->line_no; $this->char_no = $state->char_no; $this->token = $state->token; $this->token_type = $state->token_type; $this->r = $state->r; $this->context = $state->context; } // pobranie następnego znaku ze strumienia źródłowego private function getChar() { return $this->r->getChar(); } // pobieranie kolejnych znaków, dopóki są znakami słowa private function eatWordChars( $char ) { $val = $char; while ($this->isWordChar($char=$this->getChar())) { $val .= $char; } if ( $char ) { $this->pushBackChar( ); } return $val; } // pobranie kolejnych znaków, dopóki są znakami odstępu private function eatSpaceChars( $char ) { $val = $char; while ( $this->isSpaceChar( $char=$this->getChar() )) { $val .= $char; }

464

DODATEK B  PROSTY ANALIZATOR LEKSYKALNY

$this->pushBackChar( ); return $val; } // cofnięcie się o jeden znak w strumieniu źródłowym private function pushBackChar( ) { $this->r->pushBackChar(); } // argument jest znakiem słowa private function isWordChar( $char ) { return preg_match( "/[A-Za-z0-9_\-]/", $char ); } // argument jest znakiem odstępu private function isSpaceChar( $char ) { return preg_match( "/\t| /", $char ); } // argument jest znakiem końca wiersza private function isEolChar( $char ) { return preg_match( "/\n|\r/", $char ); } // połknięcie znaków \n, \r bądź \r\n private function manageEolChars( $char ) { if ( $char == "\r" ) { $next_char=$this->getChar(); if ( $next_char == "\n" ) { return "{$char}{$next_char}"; } else { $this->pushBackChar(); } } return $char; } function getPos() { return $this->r->getPos(); } } class ScannerState { public $line_no; public $char_no; public $token; public $token_type; public $r; }

W klasie mamy ustawione stałe reprezentujące różne rodzaje elementów leksykalnych rozpoznawanych przez parser. Będziemy dopasowywać znaki, słowa, znaki odstępów i znaki cudzysłowu. Obecność tych dopasowań jest sprawdzana w metodach wyodrębnionych dla poszczególnych elementów: isWordChar(), isSpaceChar() i tak dalej. Sercem klasy jest metoda nextToken(). Próbuje ona dopasować następny element leksykalny ze strumienia wejściowego (ciągu). Skaner przechowuje obiekt kontekstu Context. Obiekty parserów używają tego obiektu do dzielenia się wynikami pracy na strumieniu.

465

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Warto zwrócić uwagę również na drugą klasę: ScannerState. Skaner jest zaprojektowany tak, żeby obiekty Parser mogły zachowywać stan, próbować różnych dopasowań i ewentualnie przywrócić zapamiętany stan, jeśli dana droga parsowania okazuje się ślepą uliczką. Obiekt stanu ScannerState jest wypełniany przez metodę getState() skanera. Z kolei metoda setState() pozwala na przywrócenie stanu skanera na bazie przekazanego obiektu stanu ScannerState. Oto klasa obiektów kontekstu Context: namespace gi\parse; //... class Context { public $resultstack = array(); function pushResult( $mixed ) { array_push( $this->resultstack, $mixed ); } function popResult( ) { return array_pop( $this->resultstack ); } function resultCount() { return count( $this->resultstack ); } function peekResult( ) { if ( empty( $this->resultstack ) ) { throw new Exception( "empty resultstack" ); } return $this->resultstack[count( $this->resultstack ) -1 ]; } }

Jak widać, to prosta klasa stosu, rodzaj podręcznego „brudnopisu” dla obiektów parserów. Wykonuje zadanie zbliżone do klasy kontekstu we wzorcu Interpreter, ale nie jest dokładnie tą samą klasą. Zauważmy, że obiekt klasy Scanner (skaner) nie operuje na pliku ani ciągu znaków bezpośrednio: używa do tego obiektu klasy Reader. Pozwala to na łatwe przełączanie skanera pomiędzy różnymi źródłami danych. Oto interfejs Reader i jego implementacja dla ciągu znaków — StringReader: namespace gi\parse; interface Reader { function getChar(); function getPos(); function pushBackChar(); } class StringReader implements Reader { private $in; private $pos; function __construct( $in ) { $this->in = $in; $this->pos = 0; } function getChar() { if ( $this->pos >= strlen( $this->in ) ) { return false; }

466

DODATEK B  PROSTY ANALIZATOR LEKSYKALNY

$char = substr( $this->in, $this->pos, 1 ); $this->pos++; return $char; } function getPos() { return $this->pos; } function pushBackChar() { $this->pos--; } function string() { return $this->in; } }

Klasa ta realizuje prostą operację wczytywania kolejnych znaków z ciągu znaków. Równie łatwo można zaimplementować wersję czytającą z pliku. Chyba najlepszą prezentacją użyteczności skanera jest… jego użycie. Oto przykładowy kod realizujący operację podziału prostego wyrażenia: $context = new \gi\parse\Context(); $user_in = "\$input equals '4' or \$input equals 'four'"; $reader = new \gi\parse\StringReader($user_in); $scanner = new \gi\parse\Scanner($reader, $context); while ($scanner->nextToken() != \gi\parse\Scanner::EOF) { print $scanner->token(); print "\t{$scanner->char_no()}"; print "\t{$scanner->getTypeString()}\n"; }

Zainicjalizowaliśmy obiekt Scanner, a następnie „przepuściliśmy” przez niego elementy leksykalne ze strumienia wejściowego, poprzez cykliczne wywoływanie metody nextToken(). Wywoływana w pętli metoda token() zwraca bieżący element leksykalny. Metoda char_no() informuje o pozycji w skanowanym ciągu, a getTypeString() zwraca napisową wersję stałej reprezentującej kategorię bieżącego elementu leksykalnego. Wynik wykonania powyższego kodu powinien wyglądać następująco: $ input equals ' 4 ' or $ input equals ' four '

1 6 7 13 14 15 16 17 18 20 21 22 27 28 34 35 36 40 41

CHAR WORD WHITESPACE WORD WHITESPACE APOS WORD APOS WHITESPACE WORD WHITESPACE CHAR WORD WHITESPACE WORD WHITESPACE APOS WORD APOS

467

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Moglibyśmy oczywiście wyróżnić znacznie bardziej szczegółowe kategorie, ale dla naszych celów te będą zupełnie wystarczające, tyle że podział ciągu na elementy leksykalne to łatwiejsza część czekającej nas pracy. Pytanie, jak sprawdzić poprawność gramatyczną, czyli składniową ciągu?

Analizator leksykalny Jednym z rozwiązań problemu kontroli gramatyki (składni) jest konstruowanie drzewa obiektów klasy Parser. Oto abstrakcyjna klasa Parser, na której będziemy bazować: namespace gi\parse; abstract class Parser { const GIP_RESPECTSPACE = 1; protected $respectSpace = false; protected static $debug = false; protected $discard = false; protected $name; private static $count=0; function __construct($name = null, $options=null) { if (is_null($name)) { self::$count++; $this->name = get_class( $this )." (".self::$count.")"; } else { $this->name = $name; } if (is_array($options)) { if (isset($options[self::GIP_RESPECTSPACE])) { $this->respectSpace=true; } } } protected function next(Scanner $scanner) { $scanner->nextToken(); if (! $this->respectSpace) { $scanner->eatWhiteSpace(); } } function spaceSignificant($bool) { $this->respectSpace = $bool; } static function setDebug($bool) { self::$debug = $bool; } function setHandler(Handler $handler) { $this->handler = $handler; } final function scan(Scanner $scanner) { if ($scanner->tokenType() == Scanner::SOF) { $scanner->nextToken(); } $ret = $this->doScan($scanner);

468

DODATEK B  PROSTY ANALIZATOR LEKSYKALNY

if ($ret && ! $this->discard && $this->term()) { $this->push($scanner); } if ($ret) { $this->invokeHandler($scanner); } if ($this->term() && $ret) { $this->next($scanner); } $this->report("::scan returning $ret"); return $ret; } function discard() { $this->discard = true; } abstract function trigger(Scanner $scanner); function term() { return true; } // prywatne/chronione protected function invokeHandler(Scanner $scanner) { if (! empty( $this->handler)) { $this->report("calling handler: ".get_class( $this->handler)); $this->handler->handleMatch($this, $scanner); } } protected function report($msg) { if (self::$debug) { print "name}> ".get_class($this).": $msg\n"; } } protected function push(Scanner $scanner) { $context = $scanner->getContext(); $context->pushResult($scanner->token()); } abstract protected function doScan(Scanner $scan); }

Cała zabawa rozpoczyna się w tej klasie w obrębie metody scan(). Tutaj tkwi większość logiki analizatora. Metoda scan() otrzymuje w wywołaniu obiekt skanera, z którym ma współpracować. Pierwszą czynnością w metodzie jest przekazanie obiektu Parser do implementacji abstrakcyjnej metody doScan() w klasie pochodnej. Metoda ta zwraca true bądź false. Przykład jej implementacji będziemy analizować później. Jeśli doScan() ogłosi sukces (true), to (o ile spełnionych jest kilka innych warunków) wyniki analizy są odkładane na stos wyników obiektu Context. Obiekt Scanner utrzymuje obiekt Context, za pośrednictwem którego odbywa się komunikacja pomiędzy obiektami klasy Parser. Odkładanie na stos wyników pomyślnej analizy odbywa się w ramach metody Parser::push(): protected function push(Scanner $scanner) { $context = $scanner->getContext(); $context->pushResult($scanner->token()); }

469

PHP. OBIEKTY, WZORCE, NARZĘDZIA

Odłożenie wyniku na stos może zostać zablokowane błędem analizy (wartością false metody doScan()), ale i dwoma innymi warunkami. Po pierwsze, kod użytkujący klasę może zażądać od analizatora odrzucenia udanego dopasowania wywołaniem metody discard(). Wywołanie to przełącza składową $discard na true. Po drugie, wyniki mogą odkładać na stos jedynie analizatory końcowe (to znaczy te, które nie są kompozytami innych analizatorów). Analizatory kompozytowe (egzemplarze klasy CollectionParse zwane w dalszej części dodatku również analizatorami agregatowymi) będą zamiast tego zlecać odłożenie wyników na stos odpowiednim komponentom. Aby sprawdzić, czy bieżący analizator jest końcowym, trzeba wywołać na jego rzecz metodę term(), która w analizatorach agregatowych będzie przesłaniana, tak aby zwracała false. Jeśli analizator wykonał udane dopasowanie, wywoływana jest następna metoda — invokeHandler(). W wywołaniu przekazywany jest obiekt skanera. Jeśli z analizatorem skojarzony został handler (obiekt klasy implementującej interfejs Handler; skojarzenie odbywa się przez wywołanie metody setHandler() na rzecz obiektu klasy Parser), nastąpi teraz wywołanie metody handleMatch() tego ostatniego. Obiekty implementujące interfejs Handler mają wcielać poprawne składniowo instrukcje w czyn; zobaczymy to niedługo. Po powrocie do metody scan() następują na rzecz obiektu skanera (za pośrednictwem metody next()) wywołania metody przesuwającej jego pozycję (nextToken()) i metody pomijającej znaki odstępów (eatWhiteSpaces()) w ciągu wejściowym. Na koniec zwracamy wartość wziętą z doScan(). Poza metodą doScan() mamy w tej klasie abstrakcyjną metodę trigger(). Jej wywołanie ma ustalać, czy analizator powinien próbować wykonać dopasowanie. Jeśli trigger() zwróci false, należy przyjąć, że nie ma odpowiednich warunków do realizacji analizy. Przyjrzyjmy się więc teraz okrojonej do niezbędnego minimum implementacji konkretnej klasy analizatora końcowego; parser CharacterParse służy do dopasowywania konkretnego znaku: namespace gi\parse; class CharacterParse extends Parser { private $char; function __construct($char, $name = null, $options=null) { parent::__construct($name, $options); $this->char = $char; } function trigger(Scanner $scanner) { return ($scanner->token() == $this->char); } protected function doScan(Scanner $scanner) { return ($this->trigger($scanner)); } }

Konstruktor klasy przyjmuje w wywołaniu znak do dopasowania oraz, opcjonalnie, nazwę analizatora (wykorzystywaną do celów diagnostycznych). Metoda trigger() sprawdza tutaj, czy skaner jest ustawiony na element znakowy pasujący do przekazanego do konstruktora znaku. Ponieważ nie ma potrzeby dalszego skanowania ciągu, implementacja metody doScan() ogranicza się do wywołania metody trigger(). Dopasowywanie w analizatorze końcowym jest, jak widać, dość prostą sprawą. Przejdźmy więc do analizatorów-kompozytów. Na początek trzeba dla nich zdefiniować wspólną klasę bazową; od razu też poznamy przykład konkretnej implementacji tej klasy: namespace gi\parse; // klasa abstrakcyjna analizatorów-kompozytów abstract class CollectionParse extends Parser { protected $parsers = array(); function add(Parser $p) { if (is_null($p)) {

470

DODATEK B  PROSTY ANALIZATOR LEKSYKALNY

throw new Exception( "pusty argument" ); } $this->parsers[]= $p; return $p; } function term() { return false; } } class SequenceParse extends CollectionParse { function trigger(Scanner $scanner) { if ( empty($this->parsers)) { return false; } return $this->parsers[0]->trigger($scanner); } protected function doScan(Scanner $scanner) { $start_state = $scanner->getState(); foreach($this->parsers as $parser) { if (!($parser->trigger($scanner) && $scan = $parser->scan($scanner))) { $scanner->setState($start_state); return false; } } return true; } }

Klasa abstrakcyjna CollectionParse implementuje jedynie metodę add() służącą do dodawania analizatorów do agregatu i przesłania metodę term(), tak aby zwracała false. Metoda SequenceParser::trigger() testuje jedynie pierwszy analizator agregatu, wywołując jego metodę trigger(). Wywołujący analizator będzie wywoływał CollectionParse::trigger(), aby sprawdzić, czy warto wywoływać metodę CollectionParse::scan(). Jeśli dojdzie do wywołania tej ostatniej, wywołanie zostanie oddelegowane do doScan() i zacznie się wywoływanie metod trigger() i scan() wszystkich komponentów agregatu. Jeśli którykolwiek z nich zwróci false, CollectionParse::doScan() również zwróci false. Jednym z problemów parsowania jest możliwość wycofania się z prób dopasowań. Obiekt SequenceParse może w każdym z agregowanych parserów zawierać całe poddrzewa parserów. Będą one kolejno wywoływać metodę push() skanera i rejestrować wynik w obiekcie kontekstu — ale jeśli okaże się, że liść w tym drzewie parserów zwróci „fałsz”, to co powinna zrobić SequenceParse z dotychczas uzyskanym kontekstem? Sekwencja to wszystko albo nic, więc nie ma wyjścia — trzeba cofnąć się na sam początek skanera i kontekstu. Zrealizujemy to, zapisując stan początkowy doScan() i wywołując metodę setState() tuż przed zwróceniem false w przypadku niepowodzenia parsowania. Uzupełnijmy przykład pozostałymi konkretnymi klasami analizatorów agregatowych: namespace gi\parse; // ogłasza dopasowanie, jeśli ogłosi je choćby jeden z komponentów class RepetitionParse extends CollectionParse { private $min; private $max; function __construct($min=0, $max=0, $name=null, $options=null) { parent::__construct($name, $options);

471

PHP. OBIEKTY, WZORCE, NARZĘDZIA

if ($max < $min && $max > 0) { throw new Exception( "maksimum ($max) mniejsze od minimum ($min)"); } $this->min = $min; $this->max = $max; } function trigger(Scanner $scanner) { return true; } protected function doScan(Scanner $scanner) { $start_state = $scanner->getState(); if (empty($this->parsers)) { return true; } $parser = $this->parsers[0]; $count = 0; while (true) { if ($this->max > 0 && $count >= $this->max) { return true; } if (! $parser->trigger($scanner)) { if ($this->min == 0 || $count >= $this->min) { return true; } else { $scanner->setState($start_state); return false; } } if (! $parser->scan($scanner)) { if ($this->min == 0 || $count >= $this->min) { return true; } else { $scanner->setState($start_state); return false; } } $count++; } return true; } } // ogłasza dopasowanie, jeśli ogłosi je tylko jeden z dwóch komponentów class AlternationParse extends CollectionParse { function trigger(Scanner $scanner) { foreach ($this->parsers as $parser) { if ($parser->trigger($scanner)) { return true; } } return false; } protected function doScan( Scanner $scanner ) {

472

DODATEK B  PROSTY ANALIZATOR LEKSYKALNY

$type = $scanner->tokenType(); foreach ($this->parsers as $parser) { start_state = $scanner->getState(); if ($type == $parser->trigger($scanner) && $parser->scan(($scanner)) { return true; } } $scanner->setState($start_state); return false; } } // analizator końcowy dopasowujący literały ciągów znaków class StringLiteralParse extends Parser { function trigger(Scanner $scanner) { return ($scanner->tokenType() == Scanner::APOS || $scanner->tokenType() == Scanner::QUOTE); } protected function push(Scanner $scanner) { return; } protected function doScan(Scanner $scanner) { $quotechar = $scanner->tokenType(); $ret = false; $string = ""; while ($token = $scanner->nextToken()) { if ($token == $quotechar) { $ret = true; break; } $string .= $scanner->token(); } if ($string && ! $this->discard) { $scanner->getContext()->pushResult($string); } return $ret; } } // analizator końcowy dopasowujący słowa class WordParse extends Parser { function __construct($word=null, $name=null, $options=null) { parent::__construct($name, $options); $this->word = $word; } function trigger(Scanner $scanner) { if ($scanner->tokenType() != Scanner::WORD) { return false; } if (is_null($this->word)) { return true; } return ($this->word == $scanner->token());

473

PHP. OBIEKTY, WZORCE, NARZĘDZIA

} protected function doScan(Scanner $scanner) { $ret = ($this->trigger($scanner)); return $ret; } }

Łącząc ze sobą analizatory końcowe i agregatowe, możemy skonstruować analizator nawet dość skomplikowanej składni. Komplet klas analizatorów występujących w naszym przykładzie widoczny jest na rysunku B.1.

Rysunek B.1. Klasy analizatorów Mamy tu zastosowanie wzorca Composite w taki sposób, że użytkownik klas może skonstruować w kodzie analizator składni na podobieństwo notacji EBNF. Odwzorowanie hierarchii Parser na notację EBNF prezentuje tabela B.1. Tabela B.1. Analizator-kompozyt a EBNF Klasa

EBNF

Znaczenie

AlternationParse

orExpr|andExpr

Jeden albo drugi element.

SequenceParse

'and' operand

Lista (komplet elementów w zadanej kolejności).

RepetitionParse

(eqExpr)*

Zero lub więcej elementów.

474

DODATEK B  PROSTY ANALIZATOR LEKSYKALNY

Spróbujmy więc zaimplementować nasz minijęzyk z rozdziału 11. przy użyciu hierarchii Parser. Oto fragment notacji EBNF prezentowanej w tamtym rozdziale: wyr operand orExpr andExpr eqExpr zmienna

::= ::= ::= ::= ::= ::=

operand (orExpr | andExpr)* ( '(' wyr ')' | | zmienna ) (eqExpr)* 'or' operand 'and' operand 'equals' operand '$'

Tak opisaną gramatykę implementuje i analizuje poniższa prosta klasa: class MarkParse { private $expression; private $operand; private $interpreter; private $context; function __construct($statement) { $this->compile($statement); } function evaluate($input) { $icontext = new InterpreterContext(); $prefab = new VariableExpression('input', $input); // dodaj zmienną input do kontekstu $prefab–>interpret($icontext); $this->interpreter->interpret($icontext); $result = $icontext->lookup($this->interpreter); return $result; } function compile($statement_str) { // skonstruuj drzewo analizy $context = new \gi\parse\Context(); $scanner = new \gi\parse\Scanner( new \gi\parse\StringReader($statement_str), $context); $statement = $this->expression(); $scanresult = $statement->scan($scanner); if (! $scanresult || $scanner->tokenType() != \gi\parse\Scanner::EOF) { $msg = ""; $msg .= " wiersz: {$scanner->line_no()} "; $msg .= " znak: {$scanner->char_no()}"; $msg .= " element: {$scanner->token()}\n"; throw new Exception($msg); } $this->interpreter = $scanner->getContext()->popResult(); } function expression() { if (!isset($this->expression)) { $this->expression = new \gi\parse\SequenceParse(); $this->expression->add($this->operand()); $bools = new \gi\parse\RepetitionParse(); $whichbool = new \gi\parse\AlternationParse(); $whichbool->add($this->orExpr());

475

PHP. OBIEKTY, WZORCE, NARZĘDZIA

$whichbool->add($this->andExpr()); $bools->add($whichbool); $this->expression->add($bools); } return $this->expression; } function orExpr() { $or = new \gi\parse\SequenceParse(); $or->add(new \gi\parse\WordParse('or'))->discard(); $or->add($this->operand()); $or->setHandler(new BooleanOrHandler()); return $or; } function andExpr() { $and = new \gi\parse\SequenceParse(); $and->add(new \gi\parse\WordParse('and'))->discard(); $and->add($this->operand()); $and->setHandler(newBooleanAndHandler()); return $and; } function operand() { if (!isset($this->operand)) { $this->operand = new \gi\parse\SequenceParse(); $comp = new \gi\parse\AlternationParse(); $exp = new \gi\parse\SequenceParse(); $exp->add(new \gi\parse\CharacterParse('(' ))->discard(); $exp->add($this->expression()); $exp->add(new \gi\parse\CharacterParse(')' ))->discard(); $comp->add($exp); $comp->add(new \gi\parse\StringLiteralParse()) ->setHandler(new StringLiteralHandler()); $comp->add($this->variable()); $this->operand->add($comp); $this->operand->add(new \gi\parse\RepetitionParse()) ->add($this->eqExpr()); } return $this->operand; } function eqExpr() { $equals = new \gi\parse\SequenceParse(); $equals->add(new \gi\parse\WordParse('equals'))->discard(); $equals->add($this->operand()); $equals->setHandler(new EqualsHandler()); return $equals; } function variable() { $variable = new \gi\parse\SequenceParse(); $variable->add(new \gi\parse\CharacterParse('$'))->discard(); $variable->add(new \gi\parse\WordParse()); $variable->setHandler(new VariableHandler()); return $variable; } }

476

DODATEK B  PROSTY ANALIZATOR LEKSYKALNY

Klasa jest skomplikowana jedynie z pozoru, ponieważ jej zadanie sprowadza się do konstrukcji drzewa analizy opisanej przed chwilą gramatyki. Większość metod odpowiada tu nazwom produkcji (nazwom reguł produkcji występujących w poszczególnych wierszach notacji EBNF), jak eqExpr czy andExpr. Jeśli przyjrzeć się metodzie expression(), widać, że faktycznie wciela ona w życie regułę produkcyjną z EBNF: // expr ::= operand (orExpr | andExpr)* function expression() { if (!isset($this->expression)) { $this->expression = new \gi\parse\SequenceParse(); $this->expression->add($this->operand()); $bools = new \gi\parse\RepetitionParse(); $whichbool = new \gi\parse\AlternationParse(); $whichbool->add($this->orExpr()); $whichbool->add($this->andExpr()); $bools->add($whichbool); $this->expression->add($bools); } return $this->expression; }

Tak w kodzie, jak i w regule EBNF zdefiniowaliśmy sekwencję składającą się z referencji do operandu uzupełnionej dowolną liczbą orExpr bądź andExpr. Zauważ, że analizator zwracany przez tę metodę zachowujemy w zmiennej składowej. Ma to zapobiec nieskończonym pętlom, które powstałyby w wyniku odwoływania się metod wywoływanych z expression() do expression(). Jedyne metody, których zadanie nie ogranicza się do odwzorowania reguły produkcyjnej w drzewie analizatorów, to metody compile() i evaluate(). Metoda compile() może być wywoływana bezpośrednio bądź automatycznie za pośrednictwem konstruktora klasy MarkParse. Metoda ta przyjmuje w wywołaniu ciąg instrukcji i tworzy dla niego obiekt skanera. Następnie wywołuje metodę expression(), która zwraca drzewo analizatorów dla ustalonej gramatyki. Potem następuje wywołanie metody Parser::scan() z obiektem skanera. Jeśli nie uda się dopasować przekazanego kodu do gramatyki, metoda compile() zgłosi wyjątek. W przeciwnym razie pobierze wynik kompilacji składowany na stosie wyników skanera. Jak się wkrótce przekonamy, powinien być nim obiekt klasy Expression. Wynik taki jest zachowywany w składowej o nazwie $interpreter. Metoda evaluate() oblicza dla drzewa Expression jego wartość. Obliczenie realizowane jest przez predefiniowanie obiektu VariableExpression o nazwie input i zarejestrowanie go w obiekcie kontekstu, przekazywanym potem do głównego obiektu Expression. Dzięki temu zmienna $input jest dla programistów MarkLogic dostępna zawsze, podobnie jak zmienne PHP (np. $_REQUEST).  Uwaga W rozdziale 11. omawialiśmy klasę VariableException jako element przykładu wzorca projektowego Interpreter.

W celu uzyskania ostatecznego wyniku evaluate() wywołuje Expression::interpret(). Wynik można potem pobrać z obiektu kontekstu. Wiemy już, jak analizować ciągi wejściowe i jak konstruować gramatykę. Wiemy też, jak połączyć obiekty Expression do przetworzenia zapytania na bazie wzorca Interpreter. Nie wiemy jeszcze jednak, jak powiązać te dwa procesy. Jak od drzewa analizy przejść do interpretera? Odpowiedź tkwi w obiektach klasy Handler kojarzonych z obiektami analizatorów za pośrednictwem metody Parser::setHandler(). Spójrzmy na przykład na sposób zarządzania zmiennymi. Obiekt VariableHandler przypisujemy do analizatora w ramach metody variable(): $variable->setHandler(new VariableHandler());

Oto interfejs Handler: namespace gi\parse; interface Handler { function handleMatch(Parser $parser, Scanner $scanner); }

477

PHP. OBIEKTY, WZORCE, NARZĘDZIA

A oto jego implementacja w klasie VariableHandler: class VariableHandler implements \gi\parse\Handler { function handleMatch(\gi\parse\Parser $parser, \gi\parse\Scanner $scanner) { $varname = $scanner->getContex()->popResult(); $scanner->getContext()->pushResult( new VariableExpression($varname)); } }

Jeśli analizator skojarzony z obiektem klasy VariableHandler dopasuje zmienną podczas skanowania, wywoła metodę handleMatch(). Ostatnim elementem na stosie będzie z definicji nazwa dopasowanej zmiennej. Zdejmujemy ją ze stosu, odkładając na jej miejsce nowy obiekt klasy VariableExpression o odpowiedniej nazwie. Podobnie działamy w ramach obiektów klas EqualsExpression, LiteralExpression i tak dalej. Oto pozostałe handlery: class StringLiteralHandler implements \gi\parse\Handler { function handleMatch(\gi\parse\Parser $parser, \gi\parse\Scanner $scanner) { $value = $scanner->getContext()->popResult(); $scanner->getContext()->pushResult( new LiteralExpression($value)); } } class EqualsHandler implements \gi\parse\Handler { function handleMatch(\gi\parse\Parser $parser, \gi\parse\Scanner $scanner) { $comp1 = $scanner->getContext()->popResult(); $comp2 = $scanner->getContext()->popResult(); $scanner->getContext()->pushResult(new EqualsExpression($comp1, $comp2)); } } class BooleanOrHandler implements \gi\parse\Handler { function handleMatch(\gi\parse\Parser $parser, \gi\parse\Scanner $scanner) { $comp1 = $scanner->getContext()->popResult(); $comp2 = $scanner->getContext()->popResult(); $scanner->getContext()->pushResult(new BooleanOrExpression($comp1, $comp2)); } } class BooleanAndHandler implements \gi\parse\Handler { function handleMatch(\gi\parse\Parser $parser, \gi\parse\Scanner $scanner) { $comp1 = $scanner->getContext()->popResult(); $comp2 = $scanner->getContext()->popResult(); $scanner->getContext()->pushResult(new BooleanAndExpression($comp1, $comp2)); } }

478

DODATEK B  PROSTY ANALIZATOR LEKSYKALNY

Jeśli uzupełnimy całość o implementację wzorca Interpreter z rozdziału 11., będziemy mogli korzystać z klasy MarkParse jak poniżej: $input = 'five'; $statement = "( \$input equals 'five')"; $engine = new MarkParse($statement); $result = $engine->evaluate($input); print "wejście: $input test: $statement\n"; if ($result) { print "prawda!\n"; } else { print "fałsz!\n"; }

Na wyjściu powinniśmy otrzymać: wejście: five test: ( $input equals 'five') prawda!

479

PHP. OBIEKTY, WZORCE, NARZĘDZIA

480

Skorowidz

A abstrakcja Expression, 204 abstrakcyjna klasa bazowa, 160 klasa dekoratora, 196 abstrakcyjny produkt, 175 typ, 164 wytwórca, 175 agregacja, 137 akcesor, 52, 118–120 alias, 98 aliasy metod cech typowych, 67 analizator leksykalny, 461, 468 aplikacje, 232 archiwum JAR, 398 argumenty metod, 117 asemblery obiektów dziedziny, 316 asercje, 388 atrapy, 389, 390 atrybuty, 135 elementu copy, 422 elementu input, 423 elementu patternset, 418 elementu project, 408 elementu target, 412 automatyczna kompilacja, 454 automatyczne scalanie wersji, 364 wczytywanie kodu, 103 automatyzacja instalacji, 405 kompilacji, 444 konfiguracji obiektów, 31

B badanie argumentów metod, 117 klas, 107, 114 metod, 108, 116 obiektów, 107 relacji dziedziczenia, 110 składowych, 110 wywołań metod, 110 baza danych MySQL, 157 biblioteka PHP SPL, 110 bieżąca przestrzeń nazw, 98 blok finally, 80 try-catch, 79 błąd, 73, 90 krytyczny, 81 wykonania, 66, 70, 81 błędy testów, 444 budowanie pakietu, 434 buforowanie, 247

C cecha typowa IdentityTrait, 64 PriceUtilities, 64 cechy typowe, traits, 62 z metodami statycznymi, 68 CI, Continuous Integration, 325, 426, 454 deklaracji przestrzeni nazw, 99 metody, 35 ciąg znaków, 90 ciągła integracja, CI, 325, 426, 454 budowanie pakietu, 434 dokumentacja, 430 kontrola wersji, 427

Phing, 428 przygotowanie projektu, 427 serwer Jenkins, 436 standardy kodowania, 433 testy jednostkowe, 429

D definiowanie cechy typowej, 63 destruktorów, 86 kanału, 345 metody abstrakcyjnej, 69 składowych, 33 deklaracja use, 66 dekorator, 192 delegowanie, 448 destruktor, 86 diagram klas, 134, 152, 186, 193, 208 sekwencji, 139, 252 dobre praktyki, 319 dodawanie katalogu, 374 pakietu do kanału, 346 pliku, 373 dokumentacja, 323, 349, 430, 453 dokumentacja domyślna, 352 dokumentowanie klas, 354 metod, 357, 358 plików, 354 przestrzeni nazw, 358 składowych, 355 dołączanie funkcji, 224 domknięcia, 91 dopełnienia, closures, 93

SKOROWIDZ

dostęp do bazy MySQL, 157 klasy, 50 metody, 63 powłoki zdalnej, 366 składowej, 33, 43, 68 dostępność metod cech typowych, 70 obiektu PDO, 58 drzewo dziedziczenia, 193, 210 duplikacja kodu, 222 dynamiczne ładowanie kodu, 106 dynamicznie konstruowany ciąg znaków, 110 dyrektywa include_path, 102 dystrybucja pakietu, 344 dziedziczenie, 42, 48, 110, 152, 194, 451

E EBNF, 203 elastyczne zapytania, 279 elastyczność obiektów, 183 systemu, 164 element channel, 343 contains, 343 contents, 339 copy, 421, 422 delete, 423 dir, 339 echo, 410, 420 exec, 435 file, 339 fileset, 416 filterchain, 418 input, 423 install, 344 installconditions, 344 lead, 339 patternset, 417 phprelease, 343 project, 408, 410 property, 410 QuickAddVenue, 256 required, 342 stability, 339 status, 257 target, 409, 412 token, 419 uri, 338 user, 339 view, 256 482

elementy leksykalne, tokens, 461 estetyka kodu, 449 etykietowanie wersji, 375

F fałszywe obiekty, 390 fasada, 197 finalizacja obsługi wyjątków, 79 format BloggsCal, 175, 178 MegaCal, 173, 177 PEAR, 434 pliku, 127 portlandzki, 146 wzorca, 146 framework, 19 PHPUnit, 384 SUnit, 384 funkcja call_user_func_array(), 111 class_exists(), 106 fopen(), 102 get_class(), 107 get_class_methods(), 108, 109 get_include_path(), 102 get_parent_class(), 110 getProduct(), 107 include_once(), 100 is_callable(), 109 is_int(), 42 is_subclass_of(), 110 method_exists(), 109 print_r(), 283 require(), 100 require_once(), 100 spl_autoload(), 103 spl_autoload_register(), 103 var_dump(), 113 funkcje anonimowe, 91 diagnostyczne, 329 kontroli typów, 38 ładujące, 103 pomocnicze, 105

G generator, 288 generowanie dokumentacji, 349, 350 obiektów, 163 Abstract Factory, 174 Factory Method, 170, 172 przez klonowanie, 178 Singleton, 167

Git, 363 globalna przestrzeń nazw, 100 gra Civilisation, 184 gramatyka języka MarkLogic, 202

H hermetyzacja, encapsulation, 123, 131, 157, 448 hermetyzacja algorytmów, 211 hierarchia Command, 228, 245, 255, 261 dziedziczenia, 154 Expression, 203 klas, 129, 153 klas kontrolera strony, 267 klas poleceń, 245 Marker, 212 polimorficzna, 164 przestrzeni nazw, 357 Question, 211 Registry, 239 Unit, 189 Hunt Wes, 15

I identyfikator URI, 338 identyfikowanie algorytmów, 201 imitacje, 389 implementacja, 160 Abstract Factory, 177 interfejsu, 62 komunikacji, 173 metody abstrakcyjnej, 60 wzorca Observer, 219 informacje o klasie, 113 o trendach, 443 inspekcja kodu, 325 instalacja pakietu Phing, 406 instalator PEAR, 342, 348, 405 Pyrus, 328 instalowanie automatyczne, 405 Jenkinsa, 436 pakietu, 329 Phinga, 428 programu phpDocumentor, 350 projektu, 439 rozszerzeń Jenkinsa, 438 instrukcja require(), 100 require_once(), 100

SKOROWIDZ

interfejs, 61, 160 Chargeable, 61 do kodu proceduralnego, 199 Iterator, 284, 288 kaskadow, fluent interface, 308 Observable, 216, 217, 220 Reflection, 293, 385 retrospekcji, 112 warstwy danych, 279 WWW kanału, 347 interpretatory plików kompilacji, 25 iterator, 288 izolacja od warstwy danych, 275 izolowanie implementacji, 158

J jednostka pracy, 297, 318 język MarkLogic, 202, 203 UML, 133–141, 152

K kanał PEAR, 331, 345 katalog commands, 230 główny projektu, 341 klasa, 31 AddressManager, 39 AddVenue, 257, 262 AddVenueController, 266 AppConfig, 182 AppController, 255, 261 ApplicationHelper, 235, 246, 257 ApplicationRegistry, 243, 253 Archer, 187 ArmyVisitor, 223 BloggsCommsManager, 176 Collection, 287, 305 Command, 226, 255, 262 CommandFactory, 228 CommsManager, 170–177 CompositeUnit, 190 Conf, 74 ConfException, 77 Controller, 245 ControllerMap, 257 CostStrategy, 156 Debug, 98 DeferredEventCollection, 302 DomainObject, 71, 72, 73, 289 EqualsExpression, 207 Exception, 75

Expression, 204 FileException, 77 FrontController, 255 IdentityObject, 309 kontrolera aplikacji, 258 Lesson, 153, 155 Lister, 99 LiteralExpression, 205 Login, 214 LoginObserver, 217 Mapper, 280, 292, 296, 300 Marker, 212 MDB2, 158 ModuleRuner, 119 NastyBoss, 164 Notifier, 159 ObjectWatcher, 295, 297 Observable, 217 OperatorExpression, 206 PageController, 265, 267 ParamHandler, 125 Parser, 468 PDO, 57 PEAR_Error, 334 PEAR_Exception, 336 PersistenceFactory, 306 PHPUnit_Framework_TestCase, 385 Plains, 192 Preferences, 167 ProcessRequest, 193, 195 Question, 209 ReflectionClass, 112–116 RegistrationMgr, 159 Registry, 236 Request, 250 SelectionFactory, 314 ShopProduct, 36, 40–44, 57, 166 ShopProductWriter, 52, 59 Space, 277, 300 SpaceMapper, 301 TaxCollectorVisitor, 225 TestCase, 389 Tile, 192 Transaction Script, 272 Unit, 184, 189, 221 UserStore, 392 Validate, 444 Venue, 263, 277, 291 VenueCollection, 286 VenueManager, 273 VenueUpdateFactory, 314 XML_Feed_Parser, 336 XML_RPC_Server, 101

klasy abstrakcyjne, 59, 60, 152, 239 analizatorów, 474 bazowe, 152 dekoracji, 195 finalne, 80 hierarchii, 52 interfejsu Reflection API, 112 modelu dziedziny, 275 odwzorowania, 281 pochodne, 152 pomocnicze, 105 specjalizowane, 126 symboli końcowych, 205 warstwy trwałości, 317 włączające, 68 wytwórców i produktów, 172 wzorca Domain Object Factory, 304 wzorca Interpreter, 203 wzorca Observer, 218 klauzula catch, 76, 79 extends, 62 finally, 26, 80 implements, 62 try, 76 use, 93 warunkowa, 416 WHERE, 308 klient Git, 364 klonowanie obiektów, 55, 181 repozytorium, 369 klucz publiczny, 439 kod ortogonalny, 128 proceduralny, 197, 199 produkcyjny, 241 zewnętrzny, 106 kolekcja Collection, 305 SpaceCollection, 290, 292 kolekcje obiektów, 279 kolizja nazw, 68 komentarze DocBlock, 352, 359 kompilacja, 406–408 operacje, 420 typy, 416 właściwości, 410 kompilacje automatyczne, 444 komponent Selenium Server, 398 kompozycja, 137, 151, 157, 451

483

SKOROWIDZ

konfigurowanie kanału PEAR, 345 klucza publicznego, 439 raportów, 441–443 repozytorium kontroli wersji, 440 serwera Git, 365 zadań Phing, 441 konflikt procesów, 243 konsola Output, 442 konstruktor, 36, 48 konstruowanie interpreterów minijęzyków, 201 struktur, 220 kontrola tożsamości, 295 typu, 42 wersji, 363, 373, 427 kontroler aplikacji, 254, 258 fasady, 245, 248, 252 strony, 264, 265 konwerter formatów, 175 kopia powierzchowna, 89 kopie obiektów, 87

Ł ładowanie klas, 103 łączenie cech z interfejsami, 64

M mapa tożsamości, 294, 318 mechanizm MarkLogic, 213 menu dokumentacji, 351 metoda, 35 __call(), 84–87 __clone(), 88, 181 __construct(), 37, 74, 82 __destruct(), 86, 87 __get(), 82–86 __isset(), 83 __set(), 83, 85 __sleep(), 244 __toString(), 90 __unset(), 84 __wakeup(), 244 accept(), 221, 225 addChargeableItem, 62 addDirty(), 298 addParam(), 126 addUnit(), 185, 187 attach(), 216 bombardStrength(), 185 buildStatement(), 313 484

calculateTax(), 62, 63 create(), 71 detach(), 216 die(), 80 doCreateObject(), 302 doExecute(), 262, 263 doInterpret(), 206 execute(), 118, 227, 252 exit(), 80 find(), 282, 284 findByVenue(), 292 finder(), 300 generateId(), 64 get(), 74, 242 getApptEncoder(), 173 getCommand(), 261 getComposite(), 189 getContactEncoder(), 177 getErrorData(), 336 getFromMap(), 296 getHeaderText(), 171 getInstance(), 126, 166 getNotifier(), 159 getOptions(), 247 getPrice(), 51 getProducer(), 45 getProperty(), 168, 269 getResource(), 260 getSummaryLine(), 45, 48 handleLogin(), 214 handleMethod(), 120 init(), 119, 120, 246 insert(), 282 instance(), 238 interpret(), 206 make(), 178 Mapper::findAll(), 291 mark(), 213 notify(), 216 outputAddresses(), 38 prepareStatement(), 273 process(), 196, 229 read()., 127 recruit(), 166 Reflection::export(), 113 removeUnit(), 187 sale(), 91 scan(), 469 set(), 242 setDiscount(), 51 setProperty(), 168 statuses(), 262 targetClass(), 296 visit(), 222 write(), 41, 45, 77, 127

metody abstrakcyjne, 59, 126 abstrakcyjne cechy typowej, 69 asercji, 385 cech typowych, 70 chronione, 50 destrukcji obiektów, 55 finalne, 80 klasy WebDriverBy, 401 konstrukcji, 31 konstrukcji obiektu, 36 prywatne, 50 przechwytujące, 55, 81, 85 przesłonięte, 50 publiczne, 50 publiczne klasy wyjątku, 75 statyczne, 55, 68, 71, 239 wytwórcze, 170, 178, 401 wytwórcze obiektów dopasowań, 391 minijęzyki, 201 model dziedziny, 277 montowanie dokumentu kompilacji, 407

N narzędzia, 25, 452–455 narzędzia obiektowe, 95 narzędzie Ant, 25, 406 Phing, 25, 406 phpDocumentor, 349 PHPUnit, 381 Pirum, 345 nawiasy klamrowe, 35, 99 nazwa akcesora, 84 klasy, 108 konstruktora, 50 nazwy metod, 65 wzorców, 145 nieznana klasa, 105 notacja EBNF, 474 notki, 139

O obiekt, 24, 27, 32, 123, 447 PDO, 272, 282 Request, 237, 239 obiekt-kompozyt, 186

SKOROWIDZ

obiekty brudne, 298 danych, 57 poleceń, 226 rejestru, 237 tożsamości, 306 weryfikujące, 383 obserwator, 215 obserwowanie interfejsu, 215 obiektów, 279 obsługa błędów, 73, 78, 255 błędów w PEAR, 334 obiektów, 55 PEAR, 328 sesji, 238 wierszy, 284 wyjątków, 79 żądania HTTP, 248 odczyt danych, 318 w formacie XML, 125 z pliku, 124 odnośniki w dokumentacji, 359 odpowiedzialność, 127, 129 odwzorowanie danych, 280, 318 ograniczenia, 134 we wzorcu Composite, 191 opcje instalacji, 330 operacja, 135 copy, 420 delete, 423 echo, 420 input, 422 operator ::, 48 ==, 88 ===, 88 as, 67, 68 dostępu do składowej, 33, 35 instanceof, 107, 191 insteadof, 66, 68 new, 37 opis klasy, 355 opóźnione ładowanie, 301 oprogramowanie kontroli wersji, 373 ortogonalność, 128 ortogonalność projektu, 214 osłabianie sprzężenia, 158, 448

P pakiet, 95 Auth, 321 Benchmark, 320 Cache_Lite, 320 Config, 321, 334 File_HtAccess, 320 Log, 330 Mail_Mime, 321 MDB2, 157, 320 Phing, 406 reflect, 112 RPC, 101 util, 97 XML_RSS, 320 pakiety JAR, 329 PEAR, 74, 101, 320, 334, 406 własne, 337 para klucz – wartość, 125 parametr cmd, 256 include_path, 102 parsowanie, 125 PDO, PHP Data Object, 157 PEAR, 25, 101, 327 pętla foreach, 288 PHP/FI, 27 PHP3, 27 PHP4, 28 PHP5, 29, 32 PHPUnit, 381, 384, 432 pierwsza kompilacja, 441 pisanie testu, 400 plik build.xml, 407 Config.php, 334 Dialekt.php, 342 FeedbackCommand.php, 230 httpd.conf, 102 konfiguracji kompilacji, 429 konfiguracyjny, 38, 255 main.php, 252 makefile, 406 package.xml, 337, 344 php.ini, 102 README, 424 Server.php, 101 User.php, 370, 371 venues.php, 267 pliki .htaccess, 102 .inc, 103 .php, 103

biblioteczne, 102 projektu, 368 podłączanie obserwatorów, 218 pola filtrowania, 311 polecenia, 251 polecenie channel-discover, 347 channel-info, 331, 332 commit, 368 config-show, 328, 329 get, 329 git add, 371 git branch, 368 git checkout, 377 git clone, 369 git merge, 378 git status, 370 phing, 409, 411 set, 340 polimorfizm, 123, 129, 160 połączenie z serwerem Selenium, 399 pomocnik widoku, 268, 269 pouczenie, hint, 41 powiadomienia, 215 powiązania, 136 powiązanie procedur, 127 powielanie kodu, 188 powłoka git-shell, 366 pozyskiwanie kolekcji, 290, 292 obiektu polecenia, 261 obiektu widoku, 261 odwzorowań, 290 późne wiązanie statyczne, 71 produkt, 170, 176 program Phing, 405 phpDocumentor, 349 programowanie obiektowe, 21, 124, 183 proceduralne, 124 projekt php-webdriver, 399 projektowanie, 21, 123 projektowanie obiektowe, 123 prototyp, 178 przechwytywanie chybionych wywołań, 81 przegląd architektury, 231 przekazywanie obiektu, 61 przemądrzałe klasy, 133 przesłanianie mechanizmu ładowania, 104 metod, 221 przestrzenie nazw, 95–97 485

SKOROWIDZ

przetwarzanie pliku konfiguracji, 257 żądania, 245 pseudozmienna $this, 36 Pyrus, 328

R raporty, 442 realizacja zadań, 201 refaktoryzacja, 23 Reflection API, 112 reguły projektowe, 129 rejestr, 236, 238 rejestracja, 215 relacje dziedziczenia, 110, 136 implementacji, 136 użycia, 138 repozytorium dla użytkownika lokalnego, 365 Git, 369 kontroli wersji, 440 PEAR, 25, 101, 320–327, 333 zdalne, 365 reprezentacja obiektu, 90 reprezentowanie klas, 134 retrospekcja, 112, 118 rezygnacja, 215 rodziny produktów, 175 role plików pakietu, 340 rozgałęzianie projektu, 375 rozprzęganie, 157 rozszerzalność języka UML, 135 rozszerzanie klasy ProcessRequest, 193 klasy Unit, 190 rozszerzenia Jenkinsa, 438 rozszerzenie .tgz, 338 apc, 242 PDO, 157 SimpleXml, 74 Xdebug, 431 rozwój projektu, 21 równoległe rodziny produktów, 175

S schemat bazy danych, 271 segmenty pamięci współdzielonej, 242 Selenium, 398 separacja modelu dziedziny, 277 serializacja, 242, 244 486

serwer Apache, 102 ciągłej integracji, 436 Git, 365, 439 Jenkins, 436 Selenium, 398 skaner, 461 składnia klamrowa, 100 wierszowa, 100 składniki pakietu, 338 składowanie danych, 318 składowe chronione, 50 klasy, 33 prywatne, 50 publiczne, 50 stałe, 58 statyczne, 55 skrypt transakcji, 270, 274, 276 słowo kluczowe abstract, 59 as, 98 catch, 76 class, 31, 108 clone, 88, 178 const, 58 extends, 47, 62 final, 80, 81 finally, 79 function, 35, 92 implements, 62 insteadof, 65, 66 interface, 61 namespace, 97 parent, 48, 72 private, 33, 35, 50, 70 protected, 33, 35, 50 public, 33, 35, 50 self, 56, 71 static, 55, 71 throw, 75 trait, 64 try, 76 use, 98 yield, 288 specjalizowanie klasy wyjątku, 76 SPL, 219 spójność, 127 sprzęganie, 127, 157 sprzężenie, 158 stała __NAMESPACE__, 99 PATH_SEPARATOR, 102 standardy kodowania, 433

stosowanie cech typowych, 63 dziedziczenia, 46 wzorców, 146 strategia kreacji obiektów, 165 strona WWW kanału, 346, 347 struktura dziedziczenia, 153 plików pakietu, 340 wzorca, 146 wzorca Composite, 188 strukturalizacja klas, 183 symbole widoczności atrybutów, 135 symulowanie systemu pakietów, 100 system integracji ciągłej, 454 pakietów, 100 plików, 100 szablon widoku, 268, 269 szukanie klasy, 106

Ś ścieżka dostępu do plików, 248 ścieżki przeszukiwania, 101

T tabela, 57 tablica getBacktrace(), 335 testowanie, 324, 453 aplikacji WWW, 394 ręczne, 382 systemu WOO, 396 wyjątków, 386 testy funkcjonalne, 381 jednostkowe, 381, 429, 431 tryb BLOGGS, 171 MEGA, 171 tworzenie kanału, 345 kopii obiektu, 88 obiektów, 163 obserwatorów, 218 odnośników, 359 pakietów PEAR, 337, 405 przypadku testowego, 384 repozytorium zdalnego, 365 szkieletu testu, 399 typ ParamHandler, 126

SKOROWIDZ

typy argumentów metod, 37 elementarne, 37, 38 obiektowe, 40 zależności, 343

U udostępnianie repozytorium, 366 ukrywanie szczegółów implementacji, 158 UML, Unified Modeling Language, 133–141 unikanie kolizji nazw, 65 silnego sprzężenia, 451 usuwanie katalogu, 374 obiektów, 219 pakietów, 329 pliku, 374 utrwalanie danych konfiguracyjnych, 257 uzupełnianie ścieżek, 102 użycie klas dekoracji, 195

W warstwa danych, 233, 277, 279 kontroli żądań, 226 logiczna aplikacji, 226 logiki biznesowej, 233, 270 poleceń i kontroli, 233 prezentacji, 244, 279 widoku, 233 warstwy systemu korporacyjnego, 232 warunkowe zadanie, 416 wczytywanie kodu, 103 wdrażanie Abstract Factory, 179 widok, 244 wielokrotne stosowanie kodu, 449 wizytator, 224, 225 właściwości kompilacji, 410 warunkowe, 415 właściwość $xml, 74 dbpass, 413 włączanie pakietu, 333 współużytkowanie składowych, 90 wyjątek, 75, 78, 336, 386 FileException, 77 MyPearException, 336

wykonywanie zadań, 201 wykrywanie błędów, 454 wymuszanie typu, 41 wyodrębnianie algorytmów, 155, 211 przepływu sterowania, 256 wyprowadzanie pochodnych klasy, 161 wyrażenia regularne, 202, 213 wyróżnienie parametru żądania, 228 wytwórca, 170, 176 wytwórnia abstrakcji, 174 aktualizacji, 312 obiektów dziedziny, 304 selekcji, 312, 314 TerrainFactory, 180, 181 wywołania chybione, 81 wywołania metod przesłoniętych, 50 wywołanie metody, 111 zwrotne, 55, 91, 94 wzorce, 24, 450 wzorce projektowe, 24, 143–151, 161, 450 Abstract Factory, 174, 177, 181, 306 Application Controller, 232, 253, 254 baz danych, 162, 279 Command, 103, 226, 230 Composite, 183, 188, 191 Data Mapper, 280, 316, 318 Decorator, 192, 195, 197 Domain Model, 232, 274 Domain Object Assembler, 318 Domain Object Factory, 303, 304, 318 Facade, 197, 199 Factory Method, 170, 172 Front Controller, 232, 244 generowania obiektów, 162 Identity Map, 293, 296, 318 Identity Object, 306, 318 Intercepting Filter, 196 Interpreter, 201, 209 korporacyjne, 162, 231 Lazy Load, 301, 318 Observer, 214, 215 organizacji obiektów i klas, 162 Page Controller, 232, 264 Prototype, 178, 180 Registry, 232, 235 Selection Factory, 312, 318

Singleton, 167, 169, 181 Strategy, 209, 211 Template View, 232, 268 Transaction Script, 232, 270, 274, 275, 276 Unit of Work, 297, 318 Update Factory, 312, 318 View Helper, 268 Visitor, 220, 225 zadaniowe, 162

X XP, eXtreme Programming, 23

Z zaczepy, 201 zadania Phing, 441 zadanie, target, 408 exec, 435 warunkowe condition, 415 zależności, 342, 343 Zandstra Matt, 13 zapis do pliku, 124 zapytania SQL, 273 zarządzanie, 21 grupami obiektów, 184 kolekcjami wielowierszowymi, 287 komponentami bazodanowymi, 279 kryteriami zapytań, 307 serializacją, 244 wersjami, 363, 453 żądaniami i widokami, 264 zasady projektowe, 451 zasięg aplikacji, 238, 241 klasy, 123, 128 zmiennej, 238 zastosowanie kompozycji, 155 zatwierdzanie zmian, 370 zbieranie nieużytków, garbage collection, 86 zbiór plików, 416 zestaw testów, 387 złota rączka, 133 zmiana aliasu, 98 dostępności metod, 70 zmienna globalna, 169, 241 środowiskowa DBPASS, 414 znacznik, 182 487

SKOROWIDZ

@link, 360, 361 @package, 353 @see, 360 @uses, 361 contents, 341 dependencies, 342

488

znak ukośnika, 98 zrównoleglanie funkcjonalności, 175 zrzucanie wyjątku, 75, 80 zwielokrotnianie kodu, 133

Ż żądania użytkowników, 226 żądanie, 250 Request, 250 śladu polecenia, 261