PHP/Klasy i obiekty

< PHP

Klasy i obiekty

edytuj

W tym rozdziale nauczymy się, jak tworzyć klasy i obiekty w PHP.

Tworzenie klas

edytuj

W językach programowania klasy traktowane są zawsze jako rodzaj typów danych. Można powiedzieć, że klasa jest definicją lub szablonem obiektów. W PHP deklarujemy je słowem kluczowym class, po którym podajemy jej unikatową nazwę. Zasady jej tworzenia są podobne, jak w przypadku nazw zmiennych, tj. nie mogą one zaczynać się od cyfry. Następnie w nawiasach klamrowych umieszczamy informacje o dozwolonych polach oraz metodach, jakie klasa będzie posiadać:

<?php
class Person
{
   public $name;
   public $surname;
                
   public function setFullName($name, $surname)
   {
      $this->name = $name;
      $this->surname = $surname;
   } // end setFullName();
                
   public function getFullName()
   {
      return $this->name.' '.$this->surname;               
   } // end getFullName();  
}

Zwyczajowo pola deklaruje się na początku klasy, natomiast później - metody, które okazują się być bardzo podobne do funkcji. Podobieństwo jest jak najbardziej uzasadnione. Właściwie funkcje także reprezentują pewne zachowanie, więc nic nie stoi na przeszkodzie, aby wykorzystać tę funkcjonalność do zdefiniowania zachowań obiektów. Zasady działania metod są bardzo podobne - pobierają one argumenty i mogą zwracać wartość. Jedyna istotna różnica to obecność specjalnego wskaźnika $this, który wskazuje zawsze na obiekt, na którym daną metodę wywołujemy. Dzięki niemu możemy dostać się do wartości przechowywanych w polach obiektu oraz wywoływać inne metody. Służy do tego specjalny operator ->. Zauważ, że odwołując się do pól, pomijamy znak dolara. Nie przejmuj się słowem kluczowym public. Jego znaczenie poznamy za chwilę.

Pola klasy zachowują się, jak zwykłe zmienne. Mogą być częścią wyrażeń, możemy do nich przypisywać wartości i wykonywać wszystkie inne operacje, które są prawidłowe dla zmiennych.

Stwórzmy teraz kilka obiektów klasy Person reprezentujących różne osoby. W przeciwieństwie do większości języków kompilowanych, a podobnie jak w Javie, obiekty nie są jednym z rodzajów wartości, ale specjalnym bytem. Wartość obiektowa to jedynie referencja do istniejącego już obiektu. Gdy wykonujemy przypisanie lub przekazujemy obiekt jako argument funkcji/metody, kopiujemy jedynie referencję, a nie obiekt. Wbrew pozorom takie zachowanie jest bardzo praktyczne. W PHP4, gdzie obiekt był jednocześnie wartością, korzystanie z programowania obiektowego było przez to bardzo problematyczne i prowadziło do wielu błędów.

Obiekty tworzymy operatorem new, po którym podajemy nazwę klasy. Zwraca on referencję do obiektu, którą możemy zapisać w zmiennej:

<?php
// Dolaczamy plik z nasza klasa
require('./Person.php');

$janusz = new Person;
$janusz->setFullName('Janusz', 'Kowalski');

$adam = new Person;
$adam->setFullName('Adam', 'Nowak');

echo 'Witaj, jestem '.$janusz->getFullName().'<br/>';
echo 'A ja jestem '.$adam->getFullName().'<br/>';

Możemy też odwołać się bezpośrednio do odpowiednich pól: $adam->name.

Wywołanie metod jest niezwykle proste. Wystarczy wziąć obiekt i po operatorze -> wywołać metodę dokładnie w taki sam sposób, jak to robiliśmy z funkcjami. Metody zawsze działają w imieniu tego obiektu, na którym zostały wywołane, a to prowadzi nas do jednej z kluczowych reguł projektowania obiektowego:

Do klasy Person nie będziemy dodawać metody w stylu polaczSieZBazaDanych(), ponieważ klasa jest definicją obiektów - przypisujemy więc właściwości i metody związane bezpośrednio z opisem człowieka. Łączenie się z bazą danych jest oddzielnym zadaniem. Co więcej, lepiej zrobić to projektując połączenie uniwersalne, na przykład w oddzielnej klasie.

Zmienne obiektowe są referencjami

edytuj

Wspomnieliśmy, że w PHP5 obiekty nie są rodzajem wartości, lecz oddzielnym bytem, do którego skrypt posiada jedynie referencje. Spójrzmy, czym to skutkuje w praktyce. Rozpatrzmy prosty skrypt:

<?php
function modify($value)
{
   $value += 5;
} // end modify();

$number = 6;
modify($number);
echo $number;

Skrypt ten wyświetli nam wartość "6". Zmienna, na której operuje funkcja modify() jest jedynie kopią zmiennej globalnej $number, dlatego jej modyfikacja nie wpływa na wartość oryginału. Inaczej jest w przypadku obiektów:

<?php
require('./Person.php');

function modify($object)
{
   $object->surname = 'Nowak';
} // end modify();

$janusz = new Person;
$janusz->setFullName('Janusz', 'Kowalski');
modify($janusz);
echo $janusz->getFullName();

Tym razem skrypt wyświetlił nam wartość Janusz Nowak, co oznacza, że zmiana stanu obiektu wprowadzona przez funkcję jest widoczna globalnie. Zachowanie jest jak najbardziej prawidłowe. Funkcja modify() operuje nie na kopii, ale na referencji do obiektu. Obie referencje: globalna $janusz oraz lokalna $object są oddzielne, ale wskazują na dokładnie ten sam obiekt. Dlatego wykonanie operacji poprzez jedną z nich sprawi, że druga także zauważy zmiany.

Spróbuj w ramach ćwiczenia wykonać taką samą sztuczkę z operatorem przypisania.

Kontrola dostępu (hermetyzacja)

edytuj

W przeciwieństwie do funkcji i programowania strukturalnego, klasy posiadają precyzyjne mechanizmy kontroli dostępu do swojego wnętrza. Proces ukrywania części funkcjonalności przed programistą nosi nazwę hermetyzacji i pomaga zwiększyć niezawodność oprogramowania. Tworząc klasę, będziemy zawsze starali się określić tzw. publiczny interfejs, z którego może korzystać programista, odwołując się do tworzonych obiektów, jednocześnie ukrywając wszystkie wewnętrzne aspekty działania klasy. Interpreter będzie pilnować, aby użytkownicy nie wywołali żadnej "wewnętrznej" metody, co mogłoby doprowadzić do błędów w działaniu lub użycia jej niezgodnie z przeznaczeniem.

Poznaliśmy już jeden z modyfikatorów dostępu, public, który podaliśmy przed każdą metodą oraz polem. Jest on wyjątkowy, ponieważ w przypadku metod można go pominąć, zostawiając samo function nazwa(), a w przypadku pól zastąpić synonimem var. My jednak będziemy stosować konwencję, w której zawsze jawnie określamy widzialność elementu.

Pozostałe modyfikatory dostępu to protected oraz private. Pierwszy z nich dotyczy dziedziczenia, dlatego zajmiemy się nim później, a tymczasem przyjrzymy się drugiemu z nich. Mówi on, że dane pole lub metoda jest prywatnym elementem klasy. W praktyce oznacza to, że odwołać się do niego możemy wyłącznie z poziomu innej metody w naszej klasie, a z zewnątrz jest on niedostępny. W naszych przykładach będziemy stosować konwencję, w której prywatne elementy będą posiadać nazwy zaczynające się od podkreślenia.

<?php
class SomeClass
{
   private $_field;

   public function getField()
   {
      // Poprawne odwołanie
      return $this->_field;
   } // end getField();

} // end SomeClass;

$someObject = new SomeClass;

// dostęp jest możliwy poprzez metodę
echo $someObject->getField();

// błąd, próba dostępu do pola prywatnego!
echo $someObject->_field;

Powyższy skrypt się nie wykona. W ostatniej linijce PHP zgłosi nam błąd, jakim jest próba dostania się do prywatnego elementu klasy spoza obiektu. Jednocześnie działa odwołanie umieszczone wewnątrz metody getField(). Taki rodzaj metody zwie się po angielsku getter, a oprócz niego mamy też setter, który pozwala ustawić wartość określonemu polu. Jest to jedna z konwencji stosowanych w hermetyzacji, w myśl której klasa nie powinna, poza szczególnymi wyjątkami, zawierać publicznych pól. Jeżeli użytkownik ma mieć dostęp do jakiegoś pola, musi to robić za pośrednictwem dodatkowych metod, czyli getterów i setterów. Zauważmy, że takie podejście pozwala nam tworzyć pola dostępne publicznie tylko do odczytu bez możliwości ich modyfikacji. Wystarczy zadeklarować je jako prywatne i stworzyć publiczny getter, bez settera. Przepiszmy zatem naszą klasę Person zgodnie z regułami hermetyzacji, ograniczając nieco dostęp:

<?php
class Person
{
   private $_name = null;
   private $_surname = null;
                
   public function setName($name)
   {
      if($this->_name === null)
      {
         $this->_name = $name;
         return true;
      }
      return false;
   } // end setName();

   public function setSurname($surname)
   {
      if($this->_surname === null)
      {
         $this->_surname = $surname;
         return true;
      }
      return false;
   } // end setSurname();
                
   public function getName()
   {
      return $this->_name;            
   } // end getName();

   public function getSurname()
   {
      return $this->_surname;            
   } // end getSurname();

   public function getFullName()
   {
      return $this->_name.' '.$this->_surname;
   } // end getFullName();
} // end Person;

Teraz pola $_name oraz $_surname są prywatne, a ich wartość można odczytać wyłącznie poprzez gettery getName() oraz getSurname(). Dostępne są także analogiczne settery setName() oraz setSurname(), jednak zauważmy, że w praktyce można je wywołać tylko raz. Oznacza to, że gdy raz ustawimy danemu obiektowi nazwisko, nie jesteśmy w stanie go już później zmienić, gdyż metoda będzie zawsze zwracała wartość false, odmawiając modyfikacji. Jest to typowy przykład kontroli dostępu i ma on szczególne znaczenie w dużych aplikacjach liczących sobie setki klas, gdzie umożliwia to wymuszenie stosowania się do określonych konwencji i pomaga zapobiegać bałaganowi w kodzie.

Praktyczne zastosowanie

edytuj

Spróbujmy teraz napisać kod, który będzie przydatny w aplikacji WWW. Stworzymy prosty, obiektowy i rozszerzalny mechanizm konfiguracji, na przykładzie którego pokażemy kilka technik projektowania obiektowego. Będzie on składać się z dwóch klas:

  1. Config - zarządza konfiguracją i udostępnia ją skryptowi.
  2. ConfigLoader - rodzaj ładowarki, czyli klasy potrafiącej załadować skądś konfigurację.

Zacznijmy od napisania klasy głównej. Programista będzie mógł dodawać do niej różne ładowarki, a ona będzie udostępniała wczytaną konfigurację pozostałej części skryptu. Opcje konfiguracyjne będą wczytywane leniwie, tj. gdy zajdzie taka potrzeba. Domyślnie ładowarka będzie jedynie kolejkowana w tablicy $_awaitingLoaders. Dopiero przy próbie odczytu nieistniejącej opcji, skrypt będzie prosić kolejne ładowarki o wczytanie swojej części konfiguracji, dopóki nie trafi na taką, która wczyta to, czego potrzebujemy.

<?php

class Config
{
   private $_config = array();
   private $_awaitingLoaders = array();

   public function get($option)
   {
      // Jeśli podana opcja istnieje, zwróć jej wartość
      if(isset($this->_config[$option]))
      {
         return $this->_config[$option];
      }

      // Opcja nie istnieje, sprawdzamy, czy któraś z oczekujących ładowarek ją ma.
      foreach($this->_awaitingLoaders as $id => $loader)
      {
         $this->_config = array_merge($this->_config, $loader->load());
         unset($this->_awaitingLoaders[$id]);
         if(isset($this->_config[$option]))
         {
            return $this->_config[$option];
         }
      }
      return null;
   } // end get();

   public function addLoader(ConfigLoader $loader)
   {
      $this->_awaitingLoaders[] = $loader;
   } // end addLoader();
} // end Config;

Pojawił się tu nowy element składni: public function addLoader(ConfigLoader $loader) - przed nazwą zmiennej pojawiła się nazwa obiektu. Jest to jedno z kolejnych udoskonaleń programowania obiektowego w PHP, czyli określanie typów argumentów. Działa ono zarówno w metodach, jak i funkcjach i mówi, że dany argument może przyjąć wyłącznie obiekt klasy ConfigLoader. Próba podania dowolnego innego rodzaju wartości zakończy się błędem. W PHP określanie typów argumentów ograniczone jest wyłącznie do klasy, a od PHP 5.1 również do tablic (typ Array). Nie można wymuszać typów skalarnych (np. liczby całkowite) ani zasobów. W naszym przypadku daje to nam pewność, że programista nie będzie próbował nakarmić naszego systemu konfiguracji jakimiś dziwnymi danymi, które mogłyby doprowadzić do błędu.

Nasz system konfiguracji stosuje leniwe ładowanie opcji, lecz zwróćmy uwagę, że jego użytkownik wcale nie musi o tym wiedzieć. Dla niego korzystanie z tej klasy ogranicza się do pamiętania, że musi zdefiniować przynajmniej jedną ładowarkę oraz że dostęp do opcji możliwy jest do odczytu poprzez metodę get(). Sposób wczytywania opcji nie jest już przedmiotem jego zainteresowań, dlatego wewnętrzne aspekty działania klasy oraz faktyczny sposób reprezentacji danych został przed nim ukryty, by nie mógł przy nim majstrować.

Napiszmy teraz ładowarkę, która będzie wczytywać opcje z pliku INI:

<?php

class ConfigLoader
{
   private $_fileName = '';

   public function setFilename($filename)
   {
      $this->_fileName = $filename;
   } // end setFilename();

   public function load()
   {
      if(file_exists($this->_fileName))
      {
         return parse_ini_file($this->_fileName);
      }

      // jeśli pliku nie ma, zwróć pustą tablicę
      return array();
   } // end load();
} // end ConfigLoader;

Tu również zastosowaliśmy hermetyzację. Można, a nawet trzeba ustawić nazwę pliku, który chcemy odczytać, jednak nie musi być ona później dostępna, dlatego pominęliśmy całkowicie getter. Metoda load() wywoływana przez naszą klasę Config musi zwrócić tablicę z opcjami wczytanymi z pliku.

To wszystko, spójrzmy teraz, jak wykorzystać nasz system w praktyce:

<?php
require('./Config.php');
require('./ConfigLoader.php');

$config = new Config;

// utworz ladowarki wczytujace rozne fragmenty konfiguracji
$basicConfig = new ConfigLoader;
$basicConfig->setFilename('./config/basic.ini.php');

$securityConfig = new ConfigLoader;
$securityConfig->setFilename('./config/security.ini.php');

$layoutConfig = new ConfigLoader;
$layoutConfig->setFilename('./config/layout.ini.php');

$config->addLoader($basicConfig);
$config->addLoader($securityConfig);
$config->addLoader($layoutConfig);

// zaladujmy pare opcji
echo $config->get('website_name');
echo $config->get('session_time');

Naszemu systemowi brakuje wciąż parę rzeczy. Przykładowo, można by pokusić się o zrobienie różnych rodzajów ładowarek: z plików, z bazy danych itd., lecz do tego potrzebna jest nam znajomość dziedziczenia. Ponadto przydałoby się, aby system potrafił odpowiednio raportować błędy. Póki co, jeśli pomylimy się w nazwie pliku konfiguracji, dowiemy się o tym dopiero po wnikliwym śledztwie, ponieważ jedynym śladem w ładowarce jest zwrócenie pustej tablicy. Odpowiednie mechanizmy poznamy w dalszej części podręcznika.

Stworzony system konfiguracji wykorzystuje jeszcze jedną technikę programistyczną o nazwie kompozycja. Jest to alternatywny do dziedziczenia sposób rozszerzania funkcjonalności obiektów, który jednak nie wymaga żadnej dodatkowej składni. Obrazowo mówiąc, polega on na tym, że jeden obiekt przechowuje referencje do innych obiektów, które potrafi wykorzystywać lub których funkcjonalność potrafi udostępnić na zewnątrz. W przeciwieństwie do dziedziczenia, kompozycja ma charakter dynamiczny. Możemy napisać algorytm, który w locie skomponuje nam gotowy obiekt, niczym z klocków lego, np. na podstawie konfiguracji lub opisu w bazie danych. Kompozycja jest bardzo często stosowana w praktyce obok dziedziczenia.

Zakończenie

edytuj

Mamy już solidne podstawy programowania obiektowego, a także pokazaliśmy, w jaki sposób wykorzystuje się jego własności podczas tworzenia oskryptowania stron internetowych, pisząc modularny i łatwy w rozbudowie system konfiguracji. W następnym rozdziale pokażemy, jak sterować tworzeniem i niszczeniem obiektów.