Autor: Tomasz Jędrzejewski
Data publikacji: 02.01.2005, 16:55 | Ostatnia modyfikacja: 19.11.2006, 17:36
Druga część artykuÅ‚u poÅ›wiÄ™cona programowaniu obiektowemu w PHP 5. Autor opisuje dziedziczenie, statyczne elementy klasy oraz oraz klonowanie obiektów.
Witam w drugiej części artykuÅ‚u poÅ›wiÄ™conego programowaniu obiektowemu (OOP) w PHP 5. Zajmiemy siÄ™ w niej klonowaniem obiektów, statycznymi elementami klasy oraz rozszerzaniem możliwoÅ›ci już istniejÄ…cych klas, czyli dziedziczeniem.
Aby używać możliwości danej klasy, wcale nie musimy tworzyć jej obiektu. Jeśli zadeklarujemy dane pole bądź metodę jako statyczną, otrzymamy w praktyce zwyczajną zmienną lub funkcję, tyle że powiązaną z jakąś klasą. Oto przykład:
<?php class zestaw{ static $format_daty; static function inicjuj_zestaw($format_daty){ self::$format_daty = $format_daty; } // end inicjuj_zestaw(); static function data($czas = 0){ if($czas == 0){ return date(self::$format_daty); } return date(self::$format_daty, $czas); } // end data(); static function data_strefowa($strefa, $czas = 0){ if($czas == 0){ return gmdate(self::$format_daty, time() + (3600 * $strefa)); } return date(self::$format_daty, $czas + (3600 * $strefa)); } // end data_strefowa(); } zestaw::inicjuj_zestaw('d.m.Y H:i'); echo 'Data w twojej strefie czasowej: '.zestaw::data().'<br/>'; echo 'Data na Grenlandii: '.zestaw::data_strefowa(-3).'<br/>'; ?>
W przykładzie chcieliśmy mieć parę funkcji do obsługi daty i czasu. Zapakowaliśmy je w klasę, ale jako że obiektu nam do tego raczej nie potrzeba, wszystkie jej elementy zadeklarowaliśmy jako statyczne słowem kluczowym static. Dzięki temu możemy do nich odwoływać się, nie tworząc obiektu. Oczywiście robimy to trochę inaczej, gdyż najpierw podajemy nazwę klasy, a następnie operatorem '::' oddzielamy ją od nazwy statycznej metody/pola. Dodam, iż w tym przypadku nazwa pola musi być rozpoczęta znakiem dolara (klasa::$pole).
Z wnÄ™trza metod statycznych możemy odwoÅ‚ywać siÄ™ do innych statycznych pól i metod poprzez specjalny "wskaźnik", self, wskazujÄ…cy zawsze na aktualnÄ… klasÄ™. Widać to z resztÄ… na przykÅ‚adzie.
Co jeszcze powinieneś wiedzieć o elementach statycznych? Ano że można je mieszać z elementami niestatycznymi.
Jako ciekawostkÄ™ dodam, iż używanie elementów statycznych jest odrobinÄ™ wolniejsze od tworzenia i korzystania z normalnych obiektów, dlatego najlepiej stosować takÄ… taktykÄ™ w niewielkich skryptach. W wiÄ™kszych różnica w prÄ™dkoÅ›ci jest coraz wyraźniej odczuwalna.
W poprzedniej części wspomniaÅ‚em, iż normalny operator przypisania nie tworzy kopii istniejÄ…cego obiektu, a jedynie nowy odnoÅ›nik do niego. Jednak co bÄ™dzie, gdy naprawdÄ™ bÄ™dziemy musieli utworzyć kopiÄ™ jakiegoÅ› obiektu? Jak to wykonać? Jest na to sposób - operator clone. PosÅ‚użymy siÄ™ tutaj jednym z przykÅ‚adów z poprzedniej części artykuÅ‚u.
<?php class osoba{ public $imie; public $nazwisko; public $ksywa; } $ja = new osoba; $ja -> imie = 'Tomasz'; $ja -> nazwisko = 'Jedrzejewski'; $ja -> ksywa = 'Zyx'; echo 'JA: '.$ja -> imie.' '.$ja -> nazwisko.' aka. '.$ja -> ksywa.'<br/>'; $on = clone $ja; // 1 $on -> imie = 'Bartosz'; $on -> nazwisko = 'Maciaszek'; $on -> ksywa = 'Sickboy'; echo 'ON: '.$on -> imie.' '.$on -> nazwisko.' aka. '.$on -> ksywa.'<br/>'; echo 'JA: '.$ja -> imie.' '.$ja -> nazwisko.' aka. '.$ja -> ksywa.'<br/>'; ?>
Tym razem zmienna $on wskazywała już na kopię obiektu, a nie na obiekt oryginalny. Stąd też po zmianie danych poprzez $on, obiekt $ja zachował swoje pierwotne wartości. W końcu jest oddzielnym obiektem. Przyjrzyj się dobrze operatorowi klonowania (1). Musisz o nim wiedzieć to, iż on sam nie robi przypisania, lecz generuje nowy obiekt na podstawie już istniejącego. Stąd też, aby zapisać gdzieś kopię, koniecznie należy połączyć go z operatorem =.
No dobrze, a co bÄ™dzie, kiedy każdy obiekt danego typu bÄ™dzie w jednym z pól trzymaÅ‚ dane specyficzne dla siebie? GdybyÅ›my zrobili wszystko tak, jak w powyższym przykÅ‚adzie, wartość tego pola zostaÅ‚aby skopiowana również i dwa obiekty miaÅ‚yby w nim te same dane, co powodowaÅ‚oby problemy. Z pomocÄ… przychodzi nam tu metoda specjalna - __clone(). Nie pobiera ona żadnych parametrów, nie zwraca wartoÅ›ci.
<?php class plik{ private $nazwa; private $tryb; private $f; public function __construct($nazwa, $tryb){ $this -> nazwa = $nazwa; $this -> tryb = $tryb; $this -> f = fopen($nazwa, $tryb); } // end __construct(); public function czytaj($bajtow = 0){ if($bajtow == 0){ return fread($this->f, filesize($this->nazwa)); } return fread($this -> f, $bajtow); } // end czytaj(); public function __destruct(){ fclose($this -> f); } // end __destruct(); public function __clone(){ $this -> f = fopen($this -> nazwa, $this -> tryb); } // end __clone(); } echo '<pre>'; $p1 = new plik('test.txt', 'r'); echo 'P1: '.$p1 -> czytaj(35); $p2 = clone $p1; echo 'P2: '.$p2 -> czytaj(35); ?>
Mamy tu klasÄ™ do obsÅ‚ugi plików. Każdy jej obiekt powinien mieć wÅ‚asne połączenie z danym plikiem. Dlatego też w metodzie clone() rÄ™cznie tworzymy nowe połączenie. Nie uÅ›wiadczymy w niej jednak kodu kopiujÄ…cego pozostaÅ‚e wÅ‚aÅ›ciwoÅ›ci z obiektu $p1 do $p2. Dlaczego? Otóż zostaÅ‚y one skopiowane przed wywoÅ‚aniem tej metody, gdyż ta ma za zadanie już tylko pozmieniać wartoÅ›ci tych pól, które zmiany wymagajÄ…. StÄ…d brak kopiowania pozostaÅ‚ych pól.
Po czym poznać, iż metoda clone() zostaÅ‚a wywoÅ‚ana? To proste. Na ekranie dwa razy pokaże nam siÄ™ poczÄ…tek pliku, nie zaÅ› poczÄ…tek i dalsza część (którÄ… otrzymalibyÅ›my, gdyby dwa obiekty korzystaÅ‚y z tego samego połączenia do niego).
Teraz, kiedy wyjaÅ›niliÅ›my sobie już sprawÄ™ kopiowania istniejÄ…cych obiektów, przejdźmy do dziedziczenia, czyli rozszerzania już istniejÄ…cych klas.
NaturalnÄ… dla czÅ‚owieka rzeczÄ… jest klasyfikowanie rzeczy wedÅ‚ug różnych kategorii. W Å›wiecie zwierzÄ…t od dÅ‚uższego czasu używa siÄ™ wielostopniowego podziaÅ‚u na królestwa, gatunki, rasy i inne takie duperszmity. W innych dziedzinach kategorie wykorzystywane sÄ… do klasyfikacji treÅ›ci, do porzÄ…dkowania, wreszcie do przerzucania części zadaÅ„ na wspólne dwóm urzÄ…dzeniom podzespoÅ‚y. Naturalnie wszystko to zawarte zostaÅ‚o w programowaniu obiektowym. Kiedy stworzymy klasÄ™, nie bÄ™dziemy wcale ograniczeni do jej "wbudowanej" funkcjonalnoÅ›ci. JeÅ›li potrzebne nam bÄ™dzie 10 podobnych klas, tyle że operujÄ…cych na innych zbiorach danych, wcale nie bÄ™dziemy musieli wszystkiego pisać od nowa dla każdej z nich. DziÄ™ki mechanizmowi dziedziczenia wspólne metody, pola możemy przerzucić do klasy-matki, a w klasach potomnych nadpisać tylko to, co potrzebne. Co to nam da? OszczÄ™dzimy czas, zmniejszymy i przyspieszymy kod, Å‚atwiej bÄ™dzie nam poprawić znalezione w nim błędy - jeżeli jakiÅ› zagnieździ siÄ™ we wspólnym kawaÅ‚ku kodu, naprawimy go w jednej klasie-matce i zmiany bÄ™dÄ… uwzglÄ™dnione we wszystkich klasach potomnych. To jest dziedziczenie - rozszerzanie już istniejÄ…cych klas i tworzenie hierarchii oraz zależnoÅ›ci pomiÄ™dzy nimi.
Jak dziaÅ‚a dziedziczenie? Otóż gdy mamy dwie klasy i w jednej z nich dopiszemy po jej nazwie extends klasa_pierwsza, wtedy do jej zestawu metod i pól dodawany jest podobny zestaw z klasy rozszerzanej. Możesz to obejrzeć na poniższym schemacie:

W praktyce wyglÄ…da to tak:
<?php class osoba{ public $imie; public $nazwisko; public function nazwa(){ return $this -> imie.' '.$this -> nazwisko; } // end nazwa(); } class pracownik extends osoba{ public $zarobki; public function podwyzka($kwota){ $this -> zarobki += $kwota; } // end podwyzka(); } class student extends osoba{ private $ocen; private $suma_ocen; public function srednia(){ return round($this -> suma_ocen / $this -> ocen, 2); } // end srednia(); public function dodaj_ocene($ocena){ $this -> ocen++; $this -> suma_ocen += $ocena; } // end dodaj_ocene(); } $osoba = new osoba; $pracownik = new pracownik; $student = new student; $osoba -> imie = 'Jan'; $osoba -> nazwisko = 'Kowalski'; $pracownik -> imie = 'Piotr'; $pracownik -> nazwisko = 'Nowak'; $pracownik -> zarobki = 2000; $student -> imie = 'Adam'; $student -> nazwisko = 'Adamowski'; $student -> dodaj_ocene(5); $student -> dodaj_ocene(4); $student -> dodaj_ocene(5); echo 'OSOBA: '.$osoba -> nazwa().'<br/>'; echo 'Student '.$student -> nazwa().' (średnia '.$student->srednia().')<br/>'; echo 'Pracownik '.$pracownik -> nazwa().' (zarobki - '.$pracownik->zarobki.' zl)<br/>'; $student -> dodaj_ocene(3); $pracownik -> podwyzka(115); echo 'Student '.$student -> nazwa().' (średnia '.$student->srednia().')<br/>'; echo 'Pracownik '.$pracownik -> nazwa().' (zarobki - '.$pracownik->zarobki.' zl)<br/>'; ?>
Mamy tutaj dwie klasy, które dziedziczÄ… do siebie wÅ‚aÅ›ciwoÅ›ci trzeciej klasy - osoba. Jak widać na przykÅ‚adzie, odziedziczone pola oraz metodÄ™ nazwa() możemy używać tak, jakby byÅ‚a ona zadeklarowana w tych dwóch klasach. Zatem dziedziczenie nie jest widoczne dla nas, programistów. Nie musimy stosować żadnej specjalnej skÅ‚adni, aby dostać siÄ™ do tamtych elementów.
Zauważ, iż dziedziczenie zachodzi tylko w jednÄ… stronÄ™ - w klasie bazowej osoba nie możemy używać elementów klas student oraz pracownik.
Osobnego omówienia wymaga dziedziczenie metod, gdyż tutaj mogÄ… wystÄ…pić różne dziwne "komplikacje". Przede wszystkim - co, jeÅ›li chcemy do istniejÄ…cej metody dodać w klasie pochodnej jeszcze jakiÅ› kod? Stworzyć drugÄ…? Nie - możemy stworzyć metodÄ™ nazywajÄ…cÄ… siÄ™ tak samo. Wtedy nadpisze ona tÄ™ samÄ… metodÄ™ z klasy pochodnej, dajÄ…c jednak możliwość wewnÄ™trznego odwoÅ‚ania siÄ™ do "rodzica".
<?php class osoba{ private $imie; private $nazwisko; public function __construct($imie, $nazwisko){ $this -> imie = $imie; $this -> nazwisko = $nazwisko; } // end __construct(); public function nazwa(){ return $this->imie.' '.$this->nazwisko; } // end nazwa(); public function powitanie(){ return 'Dzien dobry'; } // powitanie(); } class raper extends osoba{ private $ksywa; public function __construct($imie, $nazwisko, $ksywa){ parent::__construct($imie, $nazwisko); $this -> ksywa = $ksywa; } // end __construct(); public function nazwa(){ return parent::nazwa().' (aka '.$this->ksywa.')'; } // end nazwa(); public function powitanie(){ return 'Joł, ziomalu!'; } // end powitanie(); } $osoba = new osoba('Jan', 'Kowalski'); // ewentualna zbieznosc ksywy ewentualnie przypadkowa $raper = new raper('Adam', 'Adamski', 'kOOkOOs'); echo $osoba -> nazwa().' mowi '.$osoba -> powitanie().'<br/>'; echo $raper -> nazwa().' mowi '.$raper -> powitanie().'<br/>'; ?>
Efekt działania:
Jan Kowalski mowi Dzien dobry Adam Adamski (aka kOOkOOs) mowi Joł, ziomalu!
Co my tu widzimy? Ano że w klasie raper nadpisujemy wszystkie trzy metody z klasy bazowej osoba. Jednak w dwóch pierwszych korzystamy z odziedziczonego dorobku poprzez statyczny wskaźnik parent dostÄ™pny jedynie w klasach, które dziedziczÄ…. UżyliÅ›my go do wywoÅ‚ania konstruktora oraz metody nazwa() z klasy bazowej, gdyż chcemy tylko rozszerzyć ich funkcjonalność, a nie pisać je od nowa. W dodatku w klasie osoba wszystkie pola sÄ… zadeklarowane jako prywatne, zatem raper ich nie widzi - użycie pierwotnych wersji metod jest wiÄ™c niezbÄ™dne. Ale już metoda powitanie() jest nadpisywana tak, by robiÅ‚a coÅ› zupeÅ‚nie innego (wszak raperzy raczej "DzieÅ„ dobry" nie mówiÄ… :)).
Przyjrzyjmy siÄ™ wywoÅ‚aniu np. konstruktora klasy raper - wprowadzamy do niego trzy dane: imiÄ™, nazwisko oraz ksywÄ™ rapera. Dwie pierwsze przekazywane sÄ… do rÄ™cznie odpalanego konstruktora klasy osoba, by ten wprowadziÅ‚ je do pól obiektu. TrzeciÄ… obsÅ‚uguje już konstruktor pochodny. W ten sposób rozszerzyliÅ›my funkcjonalność metody __construct() o dodatkowy kod.
Co należy zapamiÄ™tać z tego rozdziaÅ‚u? Ano, że jeÅ›li chcemy jakÄ…kolwiek metodÄ™ rozszerzyć, a nie nadpisać (dotyczy to również konstruktorów, destruktorów i wszystkich innych metod specjalnych), musimy rÄ™cznie wywoÅ‚ać jej starÄ… wersjÄ™ z klasy bazowej poprzez statyczny "wskaźnik" parent.
Przy poprzedniej lekcji wspomniaÅ‚em, iż pola i metody zadeklarowane jako prywatne w klasie bazowej, nie sÄ… widoczne ani w jej obiektach, ani w klasie pochodnej. Co jednak, gdy w niej muszÄ… siÄ™ one znajdować? Z pomocÄ… przychodzi tutaj deklaracja ich jako elementy chronione sÅ‚owem kluczowym protected. Wtedy co prawda dalej nie bÄ™dzie można siÄ™ do nich dostać zewnÄ™trznie, ale przynajmniej bÄ™dÄ… one widoczne i dostÄ™pne dla klas pochodnych. WÅ‚aÅ›ciwość ta przydaje siÄ™ w poniższym przykÅ‚adzie, gdzie mamy klasÄ™ obsÅ‚ugujÄ…cÄ… czytanie z pliku. Aby można byÅ‚o jÄ… Å‚atwo rozszerzać, udostÄ™pniono w niej pola $polaczenie i $nazwa identyfikujÄ…ce kolejno: połączenie i nazwÄ™ otwartego pliku, wÅ‚aÅ›nie jako chronione. Nie ma obawy, że jakiÅ› nieuważny programista używajÄ…cy naszych klas coÅ› popsuje, ma natomiast możliwość popsucia czegoÅ› przy próbie rozszerzenia ich funkcjonalnoÅ›ci :).
<?php class plik{ protected $polaczenie; protected $nazwa; public function __construct($plik){ if(file_exists($plik)){ $this -> polaczenie = fopen($plik, 'r'); } $this -> nazwa = $plik; } // end __construct(); public function __destruct(){ if(is_resource($this->polaczenie)){ fclose($this -> polaczenie); } } // end __destruct(); public function __toString(){ return $this -> nazwa; } // end __toString(); public function sukces(){ if(is_resource($this -> polaczenie)){ return 1; } return 0; } // end sukces(); public function czytaj($bajtow){ if(!feof($this -> polaczenie)){ return fread($this->polaczenie, $bajtow); } } // end czytaj(); } class plik_ex extends plik{ public function __construct($plik, $typ){ if($typ == 0){ parent::__construct($plik); }else{ if(file_exists($plik)){ $this -> polaczenie = fopen($plik, 'w'); } $this -> nazwa = $plik; } } // end __construct(); public function ustaw_tryb($tryb){ if(is_resource($this->polaczenie)){ fclose($this -> polaczenie); } if($tryb == 0){ $this -> polaczenie = fopen($this->nazwa, 'r'); }else{ $this -> polaczenie = fopen($this->nazwa, 'w'); } } // end ustaw_tryb(); public function zapisz($dane){ fwrite($this -> polaczenie, $dane); } // end zapisz(); } // Plik podstawowy - mozemy tylko czytac $plik = new plik('test.txt'); if($plik -> sukces()){ echo $plik -> czytaj(50).'<br/>'; } unset($plik); // Plik rozszerzony - mozemy zarowno zapisywac $plik = new plik_ex('test.txt', 1); if($plik -> sukces()){ $plik -> zapisz('Ala ma kota, a kot ma AIDS'); } // ... jak i czytac, przy uzyciu metod odziedziczonych po klasie "plik" $plik -> ustaw_tryb(0); if($plik -> sukces()){ echo $plik -> czytaj(50).'<br/>'; } echo 'A plik nazywa siÄ™ ',$plik,'<br/>'; ?>
Możesz spróbować "wÅ‚amania" do obiektu lub zmiany typu pól zadeklarowanych w klasie plik na prywatne - w obu tych przypadkach skrypt przestanie funkcjonować. Dodam, iż w ten sam sposób możemy "ubezpieczać" metody - w ich deklaracji po prostu piszemy sÅ‚owo protected zamiast public, czy innego.
W powyższym kodzie pojawiÅ‚a nam siÄ™ ponadto tajemnicza metoda specjalna - __toString(). Jest ona wywoÅ‚ywana przez PHP, gdy gdy próbujemy użyć naszej zmiennej obiektowej jako ciÄ…gu tekstowego, a co za tym idzie, pozwala na konwersjÄ™ obiektów do tekstów :). UżyÅ‚em jej tu do pobierania nazwy obsÅ‚ugiwanego pliku. DziÄ™ki temu wystarczy, że napiszemy echo $plik;, a otrzymamy jego nazwÄ™ i nie musimy kombinować z echo $plik -> nazwa (tak nam z resztÄ… PHP nie pozwoli) lub echo $plik -> pobierz_nazwe();.
Do omówienia pozostaÅ‚o nam jeszcze sÅ‚owo kluczowe final. Jego zadanie jest zależne od tego, gdzie go użyjemy. I tak:
Spróbuj uruchomić poniższy przykÅ‚ad, a zobaczysz, że PHP rzeczywiÅ›cie blokuje nas przed "niedozwolonymi" dziaÅ‚aniami.
<?php final class xxx{ } class yyy extends xxx{ } class ppp{ final function para(){ echo 'tetete'; } // end para(); } class śśś extends ppp{ public function para(){ echo 'para'; } } ?>
WÅ‚aÅ›ciwość ta jest przydatna przy pisaniu jakiejÅ› biblioteki ogólnego użytku, gdyż możemy w ten sposób zabezpieczyć siÄ™ przed zbyt ciekawskimi programistami :).
Programowanie obiektowe przewiduje możliwość tworzenia klas przeznaczonych wyłącznie do rozszerzenia, od których nie można utworzyć obiektów. Nazywamy je klasami abstrakcyjnymi. Abstrakcyjna może być zarówno caÅ‚a klasa, jak i jej część (wybrane metody, które muszÄ… zostać nadpisane w klasie pochodnej). Przydaje siÄ™ to np. przy tworzeniu elementów wyjÅ›ciowych dla różnych klas robiÄ…cych wprawdzie to samo, ale na różnych zasobach danych. Tutaj zostanie to przedstawione na przykÅ‚adzie skryptu do kontroli danych z formularzy/URL'i, nazwanego dla naszych potrzeb "maperem". Zasada jego dziaÅ‚ania jest prosta. Ustawiamy kolejnym polom flagi opisujÄ…ce, jak ma wyglÄ…dać ich zawartość. Jeżeli wszystko bÄ™dzie siÄ™ zgadzać, zostanÄ… utworzone od nich referencje, dajÄ…c nam możliwość korzystania z nich.
<?php define('NREQUIRED', 0); define('REQUIRED', 1); abstract class mapper{ protected $mapped_data; public $mapping_ok = 1; public function __get($name){ if(isset($this->mapped_data[$name])){ return $this->mapped_data[$name]; } return NULL; } // end __get(); abstract function add_mapping($name, $type); } class url_mapper extends mapper{ public function add_mapping($name, $type){ if($type == REQUIRED){ if(strlen(trim($_GET[$name])) == 0){ return ($this -> mapping_ok = 0); }else{ $this -> mapped_data[$name] = &$_GET[$name]; $this -> mapping_ok = $this -> mapping_ok && 1; return 1; } }else{ if(strlen(trim($_GET[$name])) == 0){ $this -> mapped_data[$name] = NULL; }else{ $this -> mapped_data[$name] = &$_GET[$name]; } return 1; } return 0; } // end add_mapping(); } $mapper = new url_mapper(); $mapper -> add_mapping('par1', REQUIRED); $mapper -> add_mapping('par2', NREQUIRED); if($mapper -> mapping_ok){ echo 'Par1: '.$mapper -> par1.'<br/>'; if($mapper -> par2 != NULL){ echo 'Par2: '.$mapper -> par2.'<br/>'; } }else{ echo 'Nie wprowadziłeś wymaganego parametru "par1"!'; } ?>
Skrypt musi być wywoÅ‚any przynajmniej z parametrem "par1" w URL'u. Dodatkowo można dodać też "par2". Na co należy zwrócić w nim uwagÄ™? Przede wszystkim na konstrukcjÄ™ klasy "mapper". ZadeklarowaliÅ›my w niej jednÄ… z metod jako abstrakcyjnÄ…, wiÄ™c całą klasÄ™ również musimy tak oznaczyć. Ponadto do tejże metody nie możemy wprowadzić żadnego kodu. Po przedstawieniu listy parametrów wstawiamy Å›rednik i na tym koniec zabawy. Niech siÄ™ teraz martwi programista klasy pochodnej :).
Autor: Tomasz "Zyx" Jędrzejewski, www.zyxist.com
Waszym zdaniem:
Nikt jeszcze nie dodał swojego komentarza. Możesz być pierwszy!