PHP/Dziedziczenie
Dziedziczenie klas
edytujW naszych rozważaniach z początku rozdziału mówiliśmy o dziedziczeniu, które wyrażało nam, że "X jest Y-kiem". Dziedziczenie pozwala nam rozszerzyć już istniejącą klasę poprzez przejęcie jej pól oraz metod i dołożenie nowych. Jednak współdzielenie kodu to nie wszystko. Obiekty klasy dziedziczącej są jednocześnie obiektami wszystkich klas dziedziczonych, co oznacza, że możemy ich użyć tam, gdzie program spodziewa się dostać ogólniejszy obiekt.
Implementacja dziedziczenia
edytujPrzypuśćmy, że chcemy zbudować formularz WWW. W jego skład wchodzą różne elementy, lecz podstawowa zasada ich działania jest identyczna. Każdemu możemy ustawić nazwę i przypisać wartość. Skorzystamy z dziedziczenia, aby utworzyć klasę bazową Element zawierającą ogólny interfejs, a następnie stworzymy jej specjalizacje reprezentujące pole tekstowe, listę rozwijaną itd.
<?php
class FormElement
{
private $_name;
private $_value;
public function setName($name)
{
$this->_name = $name;
} // end setName();
public function getName()
{
return $this->_name;
} // end getName();
public function setValue($value)
{
$this->_value = $value;
} // end setValue();
public function getValue()
{
return $this->_value;
} // end getValue();
} // end FormElement;
class FormInput extends FormElement
{
public function display()
{
echo '<input type="text" name="'.$this->getName().'" value="'.htmlspecialchars($this->getValue()).'" />';
} // end display();
} // end FormInput;
class FormTextarea extends FormElement
{
private $_width = 60;
private $_height = 15;
public function setDimensions($width, $height)
{
if($width < 1 || $height < 1)
{
return false;
}
$this->_width = $width;
$this->_height = $height;
return true;
} // end setDimensions();
public function display()
{
echo '<textarea name="'.$this->getName().'" rows="'.$this->_height.'" cols="'.$this->_width.'">'.htmlspecialchars($this->getValue()).'</textarea>';
} // end display();
} // end FormTextarea;
Aby wykonać dziedziczenie, po nazwie klasy dopisujemy słowo kluczowe extends i podajemy nazwę klasy, którą chcemy rozszerzyć. Dzięki temu zarówno FormInput, jak i FormTextarea mogą korzystać z metod getName()
oraz getValue()
mimo, iż nigdzie ich nie deklarowaliśmy. Ponadto dołożyły one własne metody umożliwiające wyświetlanie danego pola w określony sposób, a FormTextarea dołożył dodatkowo możliwość ustawiania rozmiarów pola tekstowego. Poniżej pokazany jest przykład wykorzystania naszych klas:
<?php
require('./FormElements.php');
$fields = array();
$field = new FormInput;
$field->setName('nickname');
$field->setValue('Wpisz tu swoją nazwę');
$fields[] = $field;
$field = new FormTextarea;
$field->setName('comment');
$field->setDimensions(60, 5);
$fields[] = $field;
// Wyswietl formularz
foreach($fields as $field)
{
$field->display();
}
Kontrola dostępu w dziedziczeniu
edytujDo tej pory poznaliśmy dwa modyfikatory dostępu: public oraz private. W naszym poprzednim przykładzie zauważyliśmy, że w metodzie display()
odwoływaliśmy się do nazwy elementu poprzez getName()
, zamiast odczytywać ją bezpośrednio z pola $_name
. Wynika to z tego, że klasa pochodna (tj. FormInput, FormTextarea itd.) nie ma dostępu do prywatnych pól i metod klasy bazowej. Modyfikator private ogranicza nam dostęp do obiektów aktualnej i tylko aktualnej klasy, bez żadnych wyjątków. My tymczasem potrzebujemy czegoś pośredniego - zablokować dostęp z zewnątrz, a jednocześnie umożliwić go klasom potomnym. Dlatego wykorzystamy trzeci modyfikator, protected. Dzięki niemu możemy rozbudować naszą klasę FormElement o kilka przydatnych rzeczy.
Chronione elementy klasy są często wykorzystywane do tworzenia metod pomocniczych, które może wykorzystać programista rozszerzający klasę. U nas przydałaby się jakaś szybka możliwość tworzenia listy atrybutów dla znaczników formularzy. Wpisywanie ich na sztywno jest kiepskim pomysłem, ponieważ nie zawsze wszystkie atrybuty są potrzebne. Dlatego dodamy metodę, która przyjmie listę atrybutów jako tablicę i wyświetli tylko te niepuste, a następnie wykorzystamy ją w klasie potomnej.
<?php
class FormElement
{
protected $_name = null;
protected $_value = null;
public function setName($name)
{
$this->_name = $name;
} // end setName();
public function getName()
{
return $this->_name;
} // end getName();
public function setValue($value)
{
$this->_value = $value;
} // end setValue();
public function getValue()
{
return $this->_value;
} // end getName();
protected function _buildAttributes(array $attributes)
{
$code = '';
foreach($attributes as $name => $value)
{
if($value !== null)
{
$code .= ' '.$name.'="'.htmlspecialchars($value).'"';
}
}
return $code;
} // end _buildAttributes();
} // end FormElement;
class FormInput extends FormElement
{
public function display()
{
echo '<input'.$this->_buildAttributes(array(
'type' => 'text',
'name' => $this->getName(),
'value' => $this->getValue()
)).' />';
} // end display();
} // end FormInput;
W ramach ćwiczenia analogicznie zmodyfikuj klasę FormTextarea.
Stwórz obiekt klasy FormInput i ustaw mu nazwę, ale nie ustawiaj wartości. Wyświetl go i sprawdź źródło. Możemy zauważyć, że metoda pomocnicza _buildAttributes()
pominęła nam atrybut value, gdyż jego wartość była pusta i nie było sensu go wyświetlać. Ponadto tym razem możemy już z poziomu naszych klas potomnych odwoływać się bezpośrednio do $_name
oraz $_value
, ponieważ zadeklarowaliśmy je jako pola chronione.
Uwaga!
|
Unieważnianie metod
edytujPHP pozwala na tworzenie w klasach potomnych pól oraz metod, które już istnieją w klasie bazowej. Działanie to nazywa się unieważnianiem, ponieważ nowa wersja metody zastępuje (unieważnia) dotychczasową i nie wymaga żadnej dodatkowej składni. Po prostu tworzymy nową metodę o identycznej nazwie i argumentach, jak istniejąca.
<?php
class FormCheckboxList extends FormElement
{
private $_options = array();
protected $_value = array();
public function setOptions(array $options)
{
$this->_options = $options;
foreach($options as $name => $description)
{
$this->_value[$name] = false;
}
} // end setOptions();
public function setValue($value)
{
if(!is_array($value))
{
return false;
}
foreach($value as $name => $checked)
{
if(isset($this->_options[$name]))
{
$this->_value[$name] = (boolean)$checked;
}
}
return true;
} // end setValue();
public function display()
{
echo '<ul>';
foreach($this->_options as $name => $description)
{
echo '<li><input'.$this->_buildAttributes(array(
'type' => 'checkbox',
'name' => $this->_name.'['.$name.']',
'checked' => ($this->_value[$name] ? 'checked' : null)
)).' /> '.$description.'</li>';
}
echo '</ul>';
} // end display();
} // end FormCheckboxList;
Nasz nowy rodzaj elementów formularzy, FormCheckboxList, wyświetla listę checkbox-ów. Wartość elementu nie jest już pojedynczą liczbą czy tekstem, ale zbiorem wartości, który musi być odpowiednio przetworzony. Dlatego stworzyliśmy nową wersję metody setValue()
, która upewnia się, że dostała tablicę, a później odpowiednio pakuje jej zawartość do wewnętrznych struktur obiektu. Zmieniliśmy także deklarację pola $_value
tak, aby początkowo zawsze zawierało tablicę.
Przejrzystość wymaga, aby klasa FormElement dostała pustą metodę display()
. Chcemy dzięki temu powiedzieć, że elementy formularzy mogą się wyświetlać, lecz nie precyzujemy jak. Dokładne zasady wyświetlania ustala sobie każdy rodzaj elementu z osobna. Utwórz pustą, publiczną metodę display()
w klasie FormElement.
Przyjrzymy się teraz, jak podczas unieważniania zachowują się modyfikatory dostępu oraz argumenty.
Nowa wersja metody powinna przyjmować takie same argumenty z takimi samymi typami. Można zmieniać wartości domyślne opcjonalnych argumentów oraz dodawać nowe opcjonalne argumenty. |
Stosowanie się do tej zasady zapewni nam maksymalną zgodność ze standardami PHP. Poniższy przykład ilustruje poprawne konstrukcje:
<?php
class Foo
{
public function metoda1($foo)
{
echo $foo;
} // end metoda1();
public function metoda2($foo, $bar = '')
{
echo $foo;
} // end metoda2();
public function metoda3($foo)
{
echo $foo;
} // end metoda3();
} // end Foo;
class Bar extends Foo
{
protected $_bar = array();
public function metoda1($foo)
{
echo 'Nowe wywołanie';
} // end metoda1();
public function metoda2($foo, $bar = 'Nowa wartość domyślna')
{
echo 'Nowe wywołanie';
} // end metoda2();
public function metoda3($foo, $bar = 'Nowy argument opcjonalny')
{
echo 'Nowe wywołanie';
} // end metoda3();
} // end Bar;
Zobaczmy teraz, co się stanie, gdy złamiemy jedną z tych reguł. Sprawmy, by nowy argument w metodzie metoda3()
był wymagany (tj. usuwamy domyślną wartość):
public function metoda3($foo, $bar)
Jeżeli nie grzebaliśmy nic przy ustawieniach raportowania błędów, które zostały zaproponowane na początku podręcznika, dostaniemy niemiły komunikat:
Strict Standards: Declaration of Bar::metoda3() should be compatible with that of Foo::metoda3() in /sciezka/do/pliku.php on line 35
Błędy Strict Standards
to upomnienia, że naruszyliśmy jakąś regułę. Mimo iż skrypt pozornie działa dalej, nigdy nie należy ich ignorować, ponieważ ich łamanie może spowodować, że nasz skrypt przestanie działać na przyszłych wersjach PHP lub że jego wykonywanie może prowadzić do błędów w innym miejscu. Zauważmy: obiekty Bar są jednocześnie obiektami klasy Foo. Jeżeli w pewnym miejscu skrypt oczekuje obiektów Foo, ma prawo zakładać, że metoda3()
posiada jeden wymagany argument i tak też ją wywoła. Tymczasem my beztrosko wprowadzamy mu obiekt klasy Bar, który teoretycznie powinien tu zadziałać, lecz powoduje nam niespodziewany błąd, gdyż tutaj metoda3()
ma już dwa wymagane argumenty. To jest cena naszej ignorancji.
Modyfikatory dostępu mogą być zmieniane w ograniczonym zakresie. Możemy osłabiać dostęp (np. zastąpić metodę prywatną chronioną oraz chronioną przez publiczną), ale nie możemy czynić go bardziej restrykcyjnym. Jeśli w klasie bazowej metoda zadeklarowana jest jako chroniona, próba zastąpienia jej metodą prywatną skończy się natychmiastowym błędem krytycznym.
Unieważniając metody, mamy dostęp do jeszcze jednej przydatnej możliwości. Przypuśćmy, że chcemy jedynie rozszerzyć istniejącą funkcjonalność, a nie całkowicie ją zastępować. Na szczęście PHP pozwala na wywołanie pierwotnej wersji metody:
public function metoda($argument)
{
echo 'Trochę dodatkowych operacji';
parent::metoda($argument);
} // end metoda();
Konstruktory i destruktory w dziedziczeniu
edytujKonstruktory oraz destruktory zachowują się prawie identycznie, jak zwykłe metody. Gdy klasa potomna nie będzie zawierać konstruktora albo destruktora, PHP po prostu odziedziczy go po klasie bazowej. Jednak jeśli zdecydujemy się go unieważnić, nie wykona się on, dopóki tego nie powiemy explicite poprzez znaną już nam konstrukcję parent::metoda()
. Jedyna różnica polega na tym, że nie ma ograniczeń co do zmiany ilości argumentów w konstruktorach klas potomnych. Jest tak, ponieważ konstruktor jest wywoływany automatycznie tylko podczas tworzenia obiektu konkretnej klasy, do której należy.
Dodajmy konstruktor do klasy FormElement tak, aby można było natychmiast ustawić nazwę:
<?php
class FormElement
{
protected $_name;
protected $_value;
public function __construct($name)
{
$this->_name = $name;
} // end __construct();
// dalsza część klasy
} // end FormElement;
To wystarczy, aby przy tworzeniu wszystkich elementów trzeba było określić ich nazwę. Klasy potomne odziedziczą ten konstruktor i wykorzystają go u siebie. Jednak gdy zdecydujemy się wprowadzić nowy konstruktor, musimy jawnie powiedzieć, że chcemy wywołać także stary:
<?php
class FormCheckboxList extends FormElement
{
private $_options = array();
protected $_value = array();
public function __construct($name, array $options)
{
parent::__construct($name);
$this->setOptions($options);
} // end __construct();
// dalsza część klasy
} // end FormCheckboxList;
Jak widać w konstruktorze dodaliśmy nowy argument wymagany.
Aby uczynić klasę bardziej elastyczną, możemy dodać wartosć domyślną do argumentu konstruktora:
<?php
class FormElement
{
protected $_name;
protected $_value;
public function __construct($name = null)
{
$this->_name = $name;
} // end __construct();
// dalsza część klasy
} // end FormElement;
Dzięki temu określenie nazwy elementu bezpośrednio podczas tworzenia obiektu jest możliwe, ale nie jest już wymagane. Analogicznie możesz zmodyfikować klasę FormCheckboxList
Klasy abstrakcyjne
edytujW naszej rzeczywistości nie wszystkie pojęcia muszą mieć praktyczne zastosowanie. Niektóre pozwalają po prostu wygodnie odnosić się do grupy wielu różnych rzeczy. W historyjce z początku rozdziału mówiliśmy o towarach: chlebie, napojach, warzywach itd., ale nigdzie nie pojawił się towar, który był tylko towarem i niczym więcej. Dlatego powiemy, że towar jest pojęciem abstrakcyjnym. Nie może występować samodzielnie, ale może pomagać określać inne pojęcia. W świecie programowania obiektowego towar byłby tzw. klasą abstrakcyjną. Jest to specjalny rodzaj klasy stworzony specjalnie po to, aby po nim dziedziczyć. Tworzenie obiektów klas abstrakcyjnych jest zabronione, za to można tworzyć obiekty ich klas potomnych.
Klasę abstrakcyjną deklaruje się, dodając przed słowem class dodatkowy modyfikator abstract. Ponownie przyjrzyjmy się naszemu systemowi wyświetlania formularzy. Takim abstrakcyjnym bytem jest tam FormElement. Sam w sobie nie nadaje się do niczego, ponieważ nie ma zdefiniowanego wyświetlania, za to stanowi podbudowę dla innych klas. Przepiszmy tę klasę po raz kolejny:
<?php
abstract class FormElement
{
protected $_name = null;
protected $_value = null;
public function __construct($name)
{
$this->_name = $name;
} // end __construct();
public function setName($name)
{
$this->_name = $name;
} // end setName();
public function getName()
{
return $this->_name;
} // end getName();
public function setValue($value)
{
$this->_value = $value;
} // end setName();
public function getValue()
{
return $this->_value;
} // end getName();
abstract public function display();
protected function _buildAttributes(array $attributes)
{
$code = '';
foreach($attributes as $name => $value)
{
if($value !== null)
{
$code .= ' '.$name.'="'.htmlspecialchars($value).'"';
}
}
return $code;
} // end _buildAttributes();
} // end FormElement;
Teraz próba napisania new FormElement('nazwa')
zakończy się błędem wykonania skryptu.
W podanym kodzie możemy zauważyć jeszcze jedną rzecz, a mianowicie metodę abstrakcyjną. Analogicznie jak w przypadku klasy, jest to metoda, która jest przeznaczona do tego, aby ją unieważnić, dlatego też nie może zawierać żadnej treści. Po nawiasach zamykających listę argumentów możemy postawić już jedynie średnik. Idealnie nadaje się to dla naszej metody display()
. Chcemy powiedzieć, że elementy mogą być wyświetlane, ale szczegóły pozostawiamy do zaimplementowania klasom potomnym.
Jeżeli klasa zawiera metody abstrakcyjne, musi być także zadeklarowana jako abstrakcyjna. |
Definicja ta mówi nam jeszcze jedno. Przypuśćmy, że stworzyliśmy klasę dziedziczącą po FormElement, która nie zaimplementowała metody display()
. Dopóki jej nie dodamy, nowa klasa także musi być zadeklarowana jako abstrakcyjna!
Do koncepcji abstrakcji będziemy często wracać w następnych rozdziałach.
Słowo "final"
edytujSłowo abstract wymusza na programiście konieczność dziedziczenia po klasie oraz unieważnienia metody. W PHP istnieje także jego odwrotność, czyli słowo final, która uniemożliwia takie działanie. Jedyna różnica polega na tym, że klasa posiadająca metody finalne, nie musi być deklarowana jako finalna.
Metody finalne należy stosować tam, gdzie obecność jakiegoś algorytmu w takiej postaci, w jakiej go napisaliśmy, jest krytyczna dla działania całości, a próba jego unieważnienia mogłaby się dla skryptu skończyć tragicznie.
Obiekt potomka jest obiektem klasy bazowej
edytujPrzyjrzyjmy się teraz praktycznej obserwacji dotyczącej dziedziczenia. Do tej pory używaliśmy go wyłącznie jako wygodny sposób na współdzielenie kodu, jednak mechanizm ten sięga o wiele głębiej. Napiszmy klasę zarządcy dla naszego formularza:
<?php
class FormBuilder
{
protected $_elements = array();
final public function addElement(FormElement $element)
{
$this->_elements[] = $element;
return $element;
} // end addElement();
public function display()
{
foreach($this->_elements as $element)
{
$element->display();
}
} // end display();
} // end FormBuilder;
Metoda addElement()
została zadeklarowana jako finalna, ponieważ nie przewidujemy możliwości zmiany sposobu dodawania nowych elementów do formularza. Programista może rozszerzyć tę klasę, by np. udoskonalić wyświetlanie, ale proces dodawania nowych elementów ma zostawić w spokoju. Zwróćmy także uwagę na argument, który musi być obiektem klasy FormElement
. Spójrzmy teraz, jak używać nowej klasy:
<?php
require('./FormElements.php');
require('./FormBuilder.php');
$form = new FormBuilder;
$form->addElement(new FormInput('name'));
$form->addElement(new FormInput('email'));
$form->addElement(new FormTextarea('comment'))->setDimensions(70, 15);
$form->display();
Ukazuje on praktyczne znaczenie dziedziczenia. Choć tworzymy obiekty klasy FormInput, możemy je przekazać jako argument FormElement, ponieważ dziedziczenie mówi nam jasno: FormInput używa interfejsu z FormElement, posiada wszystkie jego zachowania oraz właściwości, dlatego obiekt FormInput jest jednocześnie obiektem FormElement. Przy okazji spójrzmy na następującą linijkę:
$form->addElement(new FormTextarea('comment'))->setDimensions(70, 15);
Jest to ilustracja, że referencja do obiektu wcale nie musi być przechowywana w zmiennej. Jeżeli jakaś funkcja lub metoda zwraca obiekt, możemy tuż po niej napisać -> i dostać się do jego elementów bez potrzeby stosowania zmiennej tymczasowej. Zastosowaliśmy tu pewną sztuczkę: metoda addElement()
po prostu zwraca element przekazany jako argument po to, aby nie trzeba było przepisać obiektu do zmiennej, gdy chcemy ustawić jeszcze jakieś dodatkowe właściwości. Możemy to rozbudować jeszcze dalej i sprawić, że metody takie, jak setValue()
czy setDimensions()
będą po prostu zwracać $this
, dzięki czemu taki łańcuszek wywołań metod można ciągnąć w nieskończoność. Technika ta nosi nazwę fluent interface i spotkamy się z nią jeszcze w dalszej części podręcznika.
Ćwiczenie
edytujAby przećwiczyć poznane wiadomości, wróćmy do przykładu z systemem konfiguracji. Nadaje się on idealnie do dalszej rozbudowy poprzez dziedziczenie. Zauważmy, że konfiguracji nie musimy ładować wyłącznie z plików INI. Zamiast tego, możemy utworzyć abstrakcyjną klasę ConfigLoader definiującą interfejs ładowania konfiguracji, która następnie będzie rozszerzana przez rozmaite specjalizacje, np. IniFileConfigLoader, PhpConfigLoader itd. Twoim zadaniem jest odpowiednia przebudowa tamtego kodu. Zdefiniuj interfejs klasy abstrakcyjnej ConfigLoader. Określ, które metody muszą być abstrakcyjne, a które finalne oraz gdzie zastosować słowo protected. Dodaj niezbędne konstruktory i destruktory oraz interfejs fluent interface do głównej klasy. W IniFileConfigLoader dodaj możliwość wyłączenia sprawdzania, czy plik konfiguracyjny istnieje. Przetestuj swoje modyfikacje na podanym przykładzie:
<?php
$config = new Config;
$config->addLoader(new IniFileConfigLoader('./basic.ini'))->setFileExistsCheck(true);
$config->addLoader(new PhpConfigLoader('./extra.php'));
echo $config->get('basic_option');
echo $config->get('php_option');
Zakończenie
edytujPo poznaniu dziedziczenia potrafimy już całkiem sporo wyrazić przy pomocy obiektów. Dziedziczenie ma jednak pewne ograniczenie: nie można dziedziczyć po dwóch klasach naraz:
<?php
class Klasa extends A, B
{
// ...
} // end Klasa;
W następnym rozdziale poznamy mechanizm pozwalający częściowo poradzić sobie z tym problemem, czyli interfejsy.