PHP/Elementy statyczne

Z Wikibooks, biblioteki wolnych podręczników.
< PHP
Poprzedni rozdział: Wyjątki
Następny rozdział: Metody magiczne

Elementy statyczne[edytuj]

Poznane do tej pory mechanizmy programowania obiektowego bazowały w całości na obiektach, czyli rzeczywistych, niezależnych bytach reprezentujących odpowiednie klasy. Jednak w niektórych sytuacjach tworzenie obiektu tylko po to, by móc wykonać jakąś metodę jest przerostem formy nad treścią. Dlatego PHP, podobnie jak większość innych języków obiektowych, oferuje możliwość tworzenia tzw. elementów statycznych klasy. Do działania nie potrzebują one jej obiektów, ale ponieważ są powiązane z klasą, wciąż mogą wykorzystywać np. dziedziczenie. Możemy to traktować jako rozszerzenie zwykłych funkcji i zmiennych globalnych o niektóre właściwości obiektów.

Tworzenie statycznych pól oraz metod[edytuj]

Aby utworzyć statyczne pole lub metodę, dodajemy do jego deklaracji słowo kluczowe static. Następnie możemy wykorzystać operator zakresu :: poprzedzony nazwą klasy, by dostać się do nich. Nie potrzebujemy przy tym żadnego obiektu tej klasy. W poniższym przykładzie używamy elementów statycznych do kontrolowania, ile obiektów klasy zostało już utworzonych.

<?php

class TypicalClass
{
   private $_value;
   static private $_objectCount = 0;

   public function __construct($value)
   {
      $this->_value = $value;
      self::$_objectCount++;
   } // end __construct();

   public function getValue()
   {
      return $this->_value;
   } // end getValue();

   static public function getObjectCount()
   {
      return self::$_objectCount;
   } // end getObjectCount();
} // end TypicalClass;

$object1 = new TypicalClass('foo');
$object2 = new TypicalClass('bar');
$object3 = new TypicalClass('joe');

echo 'Do tej pory utworzona została następująca ilość ';
echo 'obiektów TypicalClass: '.TypicalClass::getObjectCount();

self jest odpowiednikiem $this dla metod statycznych i wskazuje na aktualną klasę. Oprócz tego mamy też parent, z którego korzystaliśmy już przy omawianiu dziedziczenia. Jak łatwo się domyślić, wskazuje on na klasę bazową do naszej. Zauważmy, że w przeciwieństwie do ->, po operatorze zakresu musimy podać znak dolara, odwołując się do statycznego pola klasy.

Metody statyczne posiadają pewne ograniczenie w stosunku do ich zwykłych odpowiedników. Nie są wywoływane w kontekście obiektu, dlatego nie można w nich korzystać ze zmiennej specjalnej $this. Pomimo tego, PHP dopuszcza ich wywoływanie na dwa sposoby:

// sposob 1
SomeClass::staticMethod();

// sposob 2
$object = new SomeClass;
$object->staticMethod();

Okazuje się, że nic nie przeszkadza, aby metodę statyczną wywołać jak zwykłą, lecz nie będzie to miało żadnego znaczenia. Zalecamy, aby unikać takiego mieszania, gdyż wprowadza ono nieład w kodzie i utrudnia jego analizę innym osobom. Działanie w drugą stronę, tj. wywoływanie metod niestatycznych jako statyczne powoduje wygenerowanie komunikatu E_STRICT.

Statyczne elementy w dziedziczeniu[edytuj]

Elementy statyczne także podlegają dziedziczeniu. Z poziomu klasy B rozszerzającej A możemy dostać się do wszystkich statycznych metod i pól zadeklarowanych w tej drugiej, a także nadpisać je. Modyfikatory dostępu takie, jak protected i private pozwalają ograniczyć możliwość stosowania wyłącznie do innych metod klasy aktualnej oraz klas pochodnych. Przydaje się to, gdy potrzebujemy zamknąć jakąś często wykonywaną operację w postaci funkcji, lecz jednocześnie nie chcemy udostępniać jej użytkownikom naszej klasy.

W poniższym przykładzie wykorzystamy tzw. wzorzec projektowy, czyli ogólny przepis na osiągnięcie pewnego efektu w programowaniu obiektowym. Wzorcom projektowym przyjrzymy się bliżej w dalszej części podręcznika, dlatego nie będziemy ich tutaj dokładnie objaśniać. Jednym ze wzorców jest fabryka. Czasami zwykły konstruktor oraz operator new jest zbyt mało elastyczny, aby móc utworzyć obiekt. Przykładowo, chcielibyśmy, aby był on już od razu prawidłowo skonfigurowany do pracy lub aby na podstawie dostarczonych danych utworzony był obiekt jednej z klas rozszerzających wybieranej dynamicznie. Najogólniej ujmując, rozwiązanie polega na utworzeniu statycznej metody factory(), która tworzy nowy obiekt, konfiguruje go i udostępnia skryptowi. Nasza uproszczona fabryka będzie musiała uwzględniać to, że klasa może być dziedziczona i wtedy programista musi mieć możliwość poprawienia jej, aby tworzyć także obiekty nowego rodzaju.

<?php

class BaseItem
{
   static protected $_instances = 0;

   private function __construct()
   {
      echo 'Tworzymy obiekt podstawowy<br/>';
   } // end __construct();

   public function doSomeConfig()
   {
      echo 'Konfigurujemy obiekt '.((get_class($this) == 'BaseItem') ? 'podstawowy' : 'specjalny').'<br/>';
   } // end doSomeConfig();

   public function work()
   {
      echo 'Ja działam: '.get_class($this).'<br/>';
   } // end work();

   public static function factory()
   {
      $object = new BaseItem;
      $object->doSomeConfig();
      self::$_instances++;

      return $object;
   } // end factory();
} // end BaseItem;

class SpecialItem extends BaseItem
{
   private function __construct()
   {
      echo 'Tworzymy obiekt specjalny<br/>';
   } // end __construct();

   public static function factory()
   {
      $object = new SpecialItem;
      $object->doSomeConfig();
      parent::$_instances++;

      return $object;
   } // end factory();
} // end SpecialItem;

$baseObject = BaseItem::factory();
$specialObject = SpecialItem::factory();

$baseObject->work();
$specialObject->work();

Zwróćmy uwagę, że obie klasy posiadają prywatny konstruktor. Oznacza to, że programista może utworzyć ich obiekty jedynie za pośrednictwem naszej fabryki, która jest częścią klasy i dzięki temu może wywołać konstruktor (ograniczenia widoczności działają na podst. porównywania klas, a nie obiektów). W klasie BaseItem zadeklarowany został chroniony licznik obiektów współdzielony także przez wszystkie klasy rozszerzające. Jednak rozszerzając ją, nie możemy zostawić fabryki niezmienionej. W metodzie factory() mamy jasno powiedziane, jakiej klasy obiekt tworzymy, przez co wywołanie SpecialItem::factory() dalej tworzyłoby nam w rzeczywistości obiekty BaseItem. Musimy nadpisać wspomnianą metodę, zmieniając tę jedną linijkę i pozostawiając resztę kodu niezmienioną. Przy okazji widać także, że do statycznych elementów klasy bazowej można odwoływać się poprzez parent.

Jako ćwiczenie sprawdź następujące rzeczy:

  1. Czy zastąpienie parent przez self także zadziała i czy nie zmieni wyniku? Dodaj statyczną metodę count(), która zwróci wartość licznika i użyj jej, aby się o tym przekonać.
  2. Zakomentuj metodę factory() w klasie SpecialItem i spróbuj wyjaśnić zachowanie skryptu pamiętając o tym, że konstruktory obu klas są prywatne.

Wnikliwi czytelnicy powinni dostrzec w kodzie wywołanie funkcji get_class(). Zwraca ona nazwę klasy obiektu podanego w argumencie. W szczególności, jeśli za argument podamy $this, możemy dowiedzieć się wszystkiego o obiekcie, który wywołał aktualną metodę. Spróbuj pomyśleć, jak metoda może wykorzystać tę funkcję do dowiedzenia się, czy została wywołana na obiekcie klasy bazowej czy pochodnej?

Stałe klasowe[edytuj]

PHP 5.1.0 wprowadził koncepcję stałych klasowych. Zachowują się one dokładnie tak samo, jak poznane już zwykłe stałe, lecz są powiązane z konkretną klasą, a dostęp do nich odbywa się za pośrednictwem operatora :: poprzedzonego nazwą klasy. Stałą deklarujemy przy pomocy słowa kluczowego const, po którym podajemy jej nazwę oraz żądaną wartość. Stałe klasowe są stosowane do nazywania różnych specjalnych wartości, które są przeznaczone do wykorzystywania z obiektami naszej klasy. Ponieważ ich częścią jest nazwa klasy, nie musimy obawiać się o konflikt nazw oraz tworzyć bardzo długich, złożonych identyfikatorów. Przykład zastosowania podany jest poniżej:

<?php

class File
{
   const READ = 1;
   const WRITE = 2;

   private $_ptr;
   private $_fileName;
   private $_mode;

   public function __construct($fileName, $mode = self::READ)
   {
      $this->_fileName = $fileName;
      $this->_mode = $mode;

      if(!file_exists($fileName))
      {
         throw new FileException('Podany plik nie istnieje: '.$fileName);
      }

      switch($mode)
      {
         case self::READ:
            $this->_ptr = fopen($fileName, 'r');
            break;
         case self::WRITE:
            $this->_ptr = fopen($fileName, 'w');
            break;
         case self::READ | self::WRITE:
            $this->_ptr = fopen($fileName, 'rw');
      }
   } // end __construct();

   // pozostałe metody
} // end File;

try
{
   $file = new File('./plik.txt', File::READ);

   // inne działania
}
catch(FileException $exception)
{
   die('Nie można otworzyć pliku');
}

W przykładzie tworzymy klasę File, która ma reprezentować otwarty plik. Chcielibyśmy w przejrzysty sposób reprezentować tryby otwarcia, dlatego utworzyliśmy dla nich kilka stałych klasowych wykorzystywanych jako binarne flagi. Aby powyższy przykład zadziałał, należy we własnym zakresie dopisać klasę wyjątku FileException.

Stałe klasowe mogą być też deklarowane w interfejsach. Wtedy można się do nich odwoływać zarówno za pośrednictwem nazwy interfejsu, jak i nazw klas go implementujących.

Późne wiązanie statyczne[edytuj]

Wróćmy do naszej fabryki. Ma ona pewien drobny mankament. Jeśli programista chce utworzyć więcej klas pochodnych, musi dla każdej z nich od zera napisać fabrykę. Pół biedy, gdy jest ona prosta, ale wyobraźmy sobie, że składa się z ponad 100 linii kodu. Jaką mamy gwarancję, że programista nie popsuje czegoś? Co zrobić, gdy będziemy chcieli dodać funkcjonalność do klasy bazowej, która będzie musiała być uwzględniona przez wszystkie fabryki potomne? Nie zawsze musimy mieć wpływ na to, co napisze użytkownik naszej klasy.

Problem polega na tym, że metoda factory() jest monolitem, który nie rozróżnia samego utworzenia obiektu od jego konfiguracji. Dlatego wprowadźmy dodatkową, chronioną metodę _concreteFactory(), której zadaniem jest wyłącznie utworzenie obiektu i przekazanie go ogólnej fabryce, która dokona reszty. Programista musi jedynie zmodyfikować _concreteFactory(), pozostawiając konfigurację w naszych rękach.

<?php

class BaseItem
{
   static protected $_instances = 0;

   private function __construct()
   {
      echo 'Tworzymy obiekt podstawowy<br/>';
   } // end __construct();

   public function doSomeConfig()
   {
      echo 'Konfigurujemy obiekt '.((get_class($this) == 'BaseItem') ? 'podstawowy' : 'specjalny').'<br/>';
   } // end doSomeConfig();

   public function work()
   {
      echo 'Ja działam: '.get_class($this).'<br/>';
   } // end work();

   public static function factory()
   {
      $object = self::_concreteFactory();
      $object->doSomeConfig();
      self::$_instances++;

      return $object;
   } // end factory();

   protected static function _concreteFactory()
   {
      return new BaseItem;
   } // end _concreteFactory();
} // end BaseItem;

class SpecialItem extends BaseItem
{
   private function __construct()
   {
      echo 'Tworzymy obiekt specjalny<br/>';
   } // end __construct();

   protected static function _concreteFactory()
   {
      return new SpecialItem;
   } // end _concreteFactory();
} // end SpecialItem;

$baseObject = BaseItem::factory();
$specialObject = SpecialItem::factory();

$baseObject->work();
$specialObject->work();

Ups, okazuje się, że skrypt nie do końca działa tak, jak chcemy. Ci, którzy odpowiedzieli na pytania z podrozdziału "Statyczne elementy w dziedziczeniu", powinni już znać wyjaśnienie tego, co się stało. Rezultatem działania skryptu jest:

Tworzymy obiekt podstawowy
Konfigurujemy obiekt podstawowy
Tworzymy obiekt podstawowy
Konfigurujemy obiekt podstawowy
Ja działam: BaseItem
Ja działam: BaseItem

Innymi słowy, wywołanie SpecialItem::factory() całkowicie zignorowało istnienie nadpisanej metody SpecialItem::_concreteFactory() i ponownie wywołało jej odpowiednik w klasie bazowej. Okazuje się, że PHP wiąże wywołanie z konkretną metodą w kodzie już w fazie kompilacji. Kompilując metodę factory(), od razu powiązał ją na sztywno z _concreteFactory() w tej samej klasie, bez oglądania się na dziedziczenie. W pierwszym rozdziale o programowaniu obiektowym wspominaliśmy o polimorfiźmie oraz o tym, że w PHP wszystkie metody są polimorficzne z definicji i nie trzeba się tym przejmować. Doprecyzujmy teraz: wszystkie niestatyczne metody są polimorficzne, a w przypadku statycznych, musimy skorzystać z tzw. późnego wiązania statycznego (ang. late static binding) wprowadzonego w PHP 5.3.0. Chcemy poinformować PHP, że odwołanie do _concreteFactory() może prowadzić do różnych metod w zależności od tego czy wywołamy BaseItem::factory() czy SpecialItem::factory(). Dokonujemy tego poprzez zastąpienie słowa kluczowego self przez static:

<?php

class BaseItem
{
   static protected $_instances = 0;

   private function __construct()
   {
      echo 'Tworzymy obiekt podstawowy<br/>';
   } // end __construct();

   public function doSomeConfig()
   {
      echo 'Konfigurujemy obiekt '.((get_class($this) == 'BaseItem') ? 'podstawowy' : 'specjalny').'<br/>';
   } // end doSomeConfig();

   public function work()
   {
      echo 'Ja działam: '.get_class($this).'<br/>';
   } // end work();

   public static function factory()
   {
      $object = static::_concreteFactory();
      $object->doSomeConfig();
      self::$_instances++;

      return $object;
   } // end factory();

   protected static function _concreteFactory()
   {
      return new BaseItem;
   } // end _concreteFactory();
} // end BaseItem;

class SpecialItem extends BaseItem
{
   private function __construct()
   {
      echo 'Tworzymy obiekt specjalny<br/>';
   } // end __construct();

   protected static function _concreteFactory()
   {
      return new SpecialItem;
   } // end _concreteFactory();
} // end SpecialItem;

$baseObject = BaseItem::factory();
$specialObject = SpecialItem::factory();

$baseObject->work();
$specialObject->work();

Teraz nasz kod działa tak, jak tego chcemy. Późnego wiązania należy używać wtedy, gdy przewidujemy możliwość rozszerzania klasy z metodami statycznymi, i to w taki sposób, by robić z tego dziedziczenia prawdziwy użytek.

Zastosowanie[edytuj]

Programiści najczęściej wykorzystują elementy statyczne do tworzenia dodatkowych mechanizmów związanych z inicjacją obiektów oraz ogólnych operacji powiązanych z klasą, które:

  • nie wymagają obecności obiektu,
  • lub których wyniki muszą być dostępne dla wszystkich obiektów.

Innym zastosowaniem jest pozostawienie klasy wyłącznie w charakterze pojemnika chroniącego dostęp do danych i traktowanie metod statycznych jako bardziej rozbudowanych funkcji. Pokażemy teraz jedno z rozwiązań programistycznych stosowanych w wielu skryptach, które bazuje w całości na elementach statycznych klas.

W rozdziale o funkcjach poznaliśmy słowo kluczowe global przenoszące zmienną z globalnej do lokalnej przestrzeni funkcji. W aplikacjach obiektowych korzystanie z global jest traktowane jako zła praktyka, która może prowadzić do nieprzewidzianych zachowań. Wszystko powinno być udostępniane za pośrednictwem obiektowych interfejsów, które pilnują, aby w danym miejscu programista miał dostęp wyłącznie do określonych usług. Wbrew pozorom, ma to sens, ponieważ ogranicza samowolę i wymusza stosowanie się do wytycznych twórcy systemu, zmniejszając ryzyko popełnienia błędu. Powagę sytuacji podkreśla fakt, że toczone były dyskusje czy nie usunąć global z PHP 6.0.

Pomimo tego, czasami potrzebny jest taki publicznie dostępny rejestr najważniejszych obiektów, z których moglibyśmy korzystać. Skoro tak, to trzeba go napisać:

<?php

class Registry
{
   private static $_objects = array();

   public static function set($name, $value)
   {
      if(!is_object($value))
      {
         throw new RuntimeException('Trying to assign a non-object value to '.$name.' in the registry.');
      }
      self::$_objects[$name] = $value;
   } // end set();

   public static function get($name)
   {
      if(!isset(self::$_objects[$name]))
      {
         throw new OutOfBoundsException($name.' is not a valid registry key.');
      }
      return self::$_objects[$name];
   } // end get();
} // end Registry;

$config = new Config;
$config->addLoader(new FileConfigLoader('./config.ini.php'));

Registry::set('config', $config);

// gdzies indziej

$config = Registry::get('config');

Dzięki takiemu rejestrowi mogliśmy dodać raportowanie błędów przy pomocy wyjątków oraz nałożyć różne ograniczenia. Nasza klasa Registry może przechowywać wyłącznie obiekty. Tablice oraz wartości skalarne nie są dozwolone.

Zakończenie[edytuj]

Elementy statyczne to potężne narzędzie, jednak należy z niego korzystać rozważnie. Ponieważ zachowują się one bardziej jak funkcje i zwykłe zmienne, znacznie trudniej je testować. Zawsze dwa razy zastanów się, czy dana metoda lub pole naprawdę musi być statyczne, zanim dopiszesz do jego prototypu słowo static.