Niedoświadczeni programiści często maja problem ze zrozumieniem idei programowania obiektowego. A jest ona bardzo prosta. W programowaniu obiektowym odchodzimy od modelowania przestrzeni danych, operując na przestrzeni obiektu.
Staramy się po prostu za pomocą klas opisać modelowana rzeczywistość. Modelowanie rozpoczynamy od obiektów najbardziej ogólnych, powoli je uszczegóławiając.
Załóżmy, ze chcemy zaprogramować lampkę nocną oraz lampę ogrodową, która włącza się po zmroku za sprawą czujnika ruchu. Można do problemu podejść na skróty i napisać od razu dwie klasy opisujące odpowiednie lampy. My jednak problem potraktujemy oddzielnie (utrudnimy sobie tym samym w przyszłości możliwość tworzenia kolejnych lamp). Co więcej, lampy te nie będą miały ze sobą nic wspólnego (co również nie będzie dobrym posunięciem). Zastanówmy się jakie cechy wspólne maja obie lampy:
- świecą, bo maja żarówki,
- włączają się i wyłączają.
Stworzymy wiec ogólną klasę bazową Lampa.
Klasa ta będzie zawierała metody (funkcje wchodzące w skład klasy): zapal() oraz zgas(). Dodatkowo klasa ta będzie zawierała pole (odpowiednik zmiennej globalnej, dostępnej jednak bezpośrednio z poziomu metod klasy) odpowiadające żarówce (po zapoznaniu się z projektem lamp zastanówmy się nad podobnym projektem dla różnych żarówek: zwykłej, jarzeniowej, halogenowej i itp.).
Klasy {stala}LampkaNocna{/stala} oraz {stala}LampaOgrodowa{/stala} będą dziedziczyły po klasie Lampa, co oznacza, ze będą miały wszystkie jej cechy, rozszerzając je jednocześnie o cechy charakterystyczne dla nich samych. Co więcej, każda lampka dziedzicząca po klasie Lampa będzie typu Lampa.
Klasa {stala}LampkaNocna{/stala} zawiera dodatkowe metody {stala}rozjasnij(){/stala} oraz {stala}sciemnij(){/stala}. Lampa ogrodowa zaś na pierwszy rzut oka niczym się nie różni od klasy, po której dziedziczy, jednak ma dodatkowe pole {stala}czujnikRuchu{/stala}, będące obiektem innej klasy – klasy zarządzającej czujnikiem ruchu w ogrodzie.
Dziedziczymy wówczas, gdy dana klasa jest typu klasy, po której dziedziczymy – lampka nocna jest rodzajem lampy, również lampa ogrodowa jest rodzajem lampy. Jeśli chcemy zaznaczyć, ze dany obiekt korzysta z innego obiektu, bądź inny obiekt wchodzi w jego skład, użyjemy kompozycji, np. żarówka czy pole {stala}czujnikRuchu{/stala} lampy ogrodowej.
Zapisujemy to w PHP
Klasy w PHP maja następującą postać:
class NazwaKlasy {
[public|protected|private] $pole;
//inne pola...
[public|protected|provate] function nazwaMetody($parametry,...) {
}
//inne metody...
}
A tak może wyglądać klasa Lampa:
class Lampa {
protected $zarowka;
public function _construct (Zarowka $zarowka) {
echo \'Wkrecam .arówke
\';
$this->zarowka = $zarowka;
}
public function zapal() {
$this->zarowka->zapal();
echo \'Lampka zapalona
\';
}
public function zgas() {
$this->zarowka->zgas();
echo \'Lampka zgaszona
\';
}
}
Wygląda to zupełnie inaczej niż kod, z którym mieliśmy do czynienia do tej pory. Przeanalizujmy go dokładnie.
Definiujemy klasę o nazwie Lampa, która ma pole {stala}$zarowka{/stala}, będące zmienna przechowującą obiekt żarówki danej lampki, oraz trzy metody: {stala}_construct(){/stala}, {stala}zapal(){/stala} oraz {stala}zgas(){/stala}.
Same funkcje wyglądają podobnie do tych już znanych. Nowe jest jednak słowo public przed każda z nich, które oznacza, ze funkcja ta może być wywoływana przez każdego. Zamiast niego, można użyć słów:
- private – metoda będzie dostępna jedynie dla innych metod tej samej klasy,
- protected – metoda będzie dostępna dla innych metod tej samej klasy oraz dla klas dziedziczących po danej klasie.
Słów kluczowych {stala}public/protected/private{/stala} używa się także w odniesieniu do pól (pole {stala}$zarówka{/stala} jest chronione (protected), jest wiec dostępne jedynie z poziomu klasy Lampa oraz klas po niej dziedziczących).
Wiemy do czego służą metody {stala}zapal(){/stala} oraz {stala}zgas(){/stala}. Czym jednak jest metoda {stala}_construct(){/stala}? Ma ona dziwna nazwę i kod, który wykonuje, tez jest inny od już znanych.
Metoda {stala}_construct(){/stala} jest tak zwanym konstruktorem, czyli metoda wykonywana zawsze podczas tworzenia obiektu, jeszcze przed wykonaniem jakiejkolwiek innej metody. Nasz konstruktor ma za zadanie przypisanie do pola {stala}$zarówka{/stala} obiektu przekazanego w parametrze (przed nazwa parametru umieszczono słowo Żarówka – jest to nazwa typu klasy, której zmienna musi być przekazana jako parametr konstruktora (jeśli spróbujemy przekazać cokolwiek innego niż obiekt klasy Żarówka, PHP zwróci błąd):
$this->zarowka = $zarowka;
Nie podajemy tu zwykłej nazwy zmiennej, która
została użyta podczas definiowania pola,
a poprzedzamy ją słowem {stala}$this{/stala} (po polsku: to,
ten), które określa, ze odnosimy się do pola aktualnej
klasy. Podobnie zrobimy w przypadku, gdy
z poziomu którejś z metod klasy chcemy wywołać
inna z jej metod, np.:
public function test() {
$this->zapal();
}
Klasa ściemniacza może wyglądać tak:
class SciemniaczZarowki {
private $zarowka;
private $poziomJasnosci = 100;
public function _construct (Zarowka $zarowka) {
$this->zarowka = $zarowka;
}
public function rozjasnij() {
if ($this->poziomJasnosci < 100) {
$this->poziomJasnosci++;
echo \'Rozjasniam swiatlo zarowki
\';
}
}
public function sciemnij() {
if ($this->poziomJasnosci > 1) {
$this->poziomJasnosci--;
echo \'Przyciemniam swiatlo zarowki
\';
}
}
}
Pole {stala}$zarowka{/stala} nie jest tu używane, gdyż sposób rozjaśniania i ściemniania żarówki zależy od tego w jaki sposób rozwiążemy projekt klas żarówek. A oto kod testujący lampkę nocna:
$zarowka = new Zarowka();
$lampka = new LampkaNocna($zarowka);
$lampka->zapal();
$lampka->rozjasnij();
$lampka->sciemnij();
$lampka->sciemnij();
$lampka->rozjasnij();
$lampka->rozjasnij();
$lampka->rozjasnij();
$lampka->zgas();
Mimo ze klasa {stala}LampkaNocna{/stala} nie ma zdefiniowanych metod {stala}zapal(){/stala} i {stala}zgas(){/stala}, zostają wywołane odpowiednie metody klasy Lampa. Klasa {stala}LampaOgrodowa{/stala} jest jeszcze prostsza:
class LampaOgrodowa extends Lampa {
private $czujnikRuchu;
public function _construct (Zarowka $zarowka) {
parent::_construct($zarowka);
$this-> czujnikRuchu = new CzujnikRuchu($this->zarowka);
}
}
Korzysta dodatkowo z klasy {stala}CzujnikRuchu{/stala}:
class CzujnikRuchy {
private $zarowka;
public _construct(Zarowka $zarowka) {
$this->zarowka = $zarowka;
}
public function uruchom() {
echo \'Czujnik ruchu uruchomiony
\';
//wykrywanie ruchy...
}
private function wykrytoRuch() {
echo \'Wykryto ruch
\';
$this->zarowka->wlacz();
}
private function ruchZanikl() {
echo \'Ruch zanikl
\';
$this->zarowka->wylacz();
}
}
Klasy {stala}LampkaNocna{/stala} i {stala}LampaOgrodowa{/stala} są typu Lampa i każda metoda bądź kod potrafiący operować klasa Lampa, potrafi operować także naszymi klasami lamp:
function test(Lampa $lampa) {
$lampa->zapal();
$lampa->zgas();
}
$zarowka = new Zarowka();
$lampkaNocna = new LampkaNocna ($zarowka);
test($lampkaNocna);
Funkcja test oczekuje jako parametr obiektu klasy Lampa i bez błędu przyjmuje tutaj obiekt klasy {stala}LampkaNocna{/stala}. Dzieje się tak dzięki dziedziczeniu. Klasy dziedzicząc po innej klasie stają się typu klasy, po której dziedziczą.
I po co to wszystko?
Programowanie obiektowe to dziś codzienność. W ten właśnie sposób buduje się dziś zdecydowana większość programów. I to niezależnie od języka – czy to PHP, C++, Java, Python czy zdobywający popularność Ruby, programowanie obiektowe jest podstawa.
Od wersji 5 w PHP wprowadzono wiele udogodnień w zakresie programowania obiektowego. Dodano także nowe biblioteki, pozwalające wykorzystać potencjał drzemiący w obiektach.
Do tworzenia obiektów klasy używa się słowa
kluczowego new. Służy ono do tworzenia instancji
obiektu. Tak wiec:
$zarowka = new Zarowka();
tworzy nowy obiekt klasy Żarówka, zaś:
$lampa = new Lampa($zarowka);
tworzy obiekt Lampa i przekazuje do konstruktora wcześniej utworzony obiekt klasy Żarówka. Przyjrzymy się teraz metodzie zapal:
public function zapal() {
echo \'Lampka zapalona
\';
$this->zarowka->zapal();
}
Poza wyświetleniem napisu ma ona za zadanie zapalenie żarówki wchodzącej w skład lampki. W tym celu pobieramy obiekt przechowywany w polu {stala}$zarówka{/stala}:
$this->zarowka
a następnie wywołujemy jego metodę zapal (a wiec metodę wchodząca w skład klasy Żarówka):
$this->zarowka->zapal();
Analogicznie działa metoda zgaś. Pamiętajmy wiec, że projektowane klasy Żarówka powinny zawierać metody zapal oraz zgaś.
Teraz można utworzyć obiekt klasy Lampa i przetestować go. Poza klasą wpiszemy:
$zarowka = new Zarowka();
$lampka = new Lampa($zarowka);
$lampka->zapal();
$lampka->zgas();
Nasza klasa Lampa nie definiuje jednak żadnego konkretnego rodzaju lampy, nie powinno być więc możliwości utworzenia jej obiektu. Dodajmy przed słowem kluczowym class dodatkowe słówko abstract. Spowoduje to uczynienie z klasy Lampa klasy abstrakcyjnej.
Ważne jest, że nie da się utworzyć obiektów klas abstrakcyjnych, można po nich jedynie dziedziczyć. Co więcej, w klasach abstrakcyjnych można definiować metody abstrakcyjne. Metoda abstrakcyjna jest swojego rodzaju zapowiedzią metody, której ciało ma zostać zdefiniowane w klasach dziedziczących. Metoda abstrakcyjna zawiera jedynie nazwę i określoną listę parametrów, np.:
abstract public function zapal();
abstract public function zgas();
Nie zawiera zaś kodu w nawiasach klamrowych. Metod abstrakcyjnych używa się wówczas, gdy nie da się z góry określić sposobu ich działania, wiadomo jednak, ze muszą one wystąpić w każdej z klas dziedziczących po klasie abstrakcyjnej. Jeśli klasa dziedziczącą nie dostarczy ciała dla wszystkich metod abstrakcyjnych klasy po której dziedziczy, sama także staje się abstrakcyjna. Klasa {stala}LampkaNocna{/stala} będzie dziedziczyła po klasie Lampa, będzie więc zawierała wszystkie jej cechy oraz dodatkowo możliwość ściemniania i rozjaśniania światła:
class LampkaNocna extends Lampa {
private $sciemniaczZarowki;
public function _construct (Zarowka $zarowka) {
parent::_construct($zarowka);
$this->sciemniaczZarowki = new SciemniaczZarowki($this->zarowka);
}
public function rozjasnij() {
$this->sciemniaczZarowki-> rozjasnij();
}
public function sciemnij() {
$this->sciemniaczZarowki-> sciemnij();
}
}
Klasa ta zawiera więcej metod oraz korzysta z dodatkowej klasy {stala}SciemniaczZarowki{/stala}, która ma za zadanie odpowiednio sterować jasnością światła wkręconej w nią żarówki. Zauważmy, że konstruktor jest tu nieco inny. Wywołanie:
parent::_construct($zarowka);
powoduje uruchomienie konstruktora klasy, po której nasza nowa klasa dziedziczy (parent – ang. rodzic), a wiec konstruktora klasy Lampa. Następnie tworzony jest obiekt klasy {stala}SciemniaczZarowki{/stala}, do której konstruktora przekazywany jest obiekt żarówki, która klasa ściemniacza ma sterować:
$this->sciemniaczZarowki = new SciemniaczZarowki($this->zarowka);
Jak widać, mimo ze w klasie {stala}LampkaNocna{/stala} nie zostało zdefiniowane pole {stala}$zarowka{/stala}, klasa ta ma dostęp do pola o tej nazwie z klasy, po której dziedziczy, gdyż w klasie Lampa pole to zostało ustalone jako chronione (protected).