Poprzedni rozdział: Dziedziczenie
Następny rozdział: Wyjątki

Interfejsy

edytuj

Dziedziczenie jest jednym z najważniejszych mechanizmów programowania obiektowego, gdyż pozwala rozszerzać istniejące klasy oraz traktować je bardziej ogólnie. Podobnie jak większość języków programowania, PHP nie zezwala jednak na tzw. dziedziczenie wielokrotne, czyli możliwość rozszerzenia więcej niż jednej klasy naraz:

<?php
class MultipleInheritance extends FirstClass, SecondClass
{

} // end MultipleInheritance;

Przy tradycyjnej konstrukcji systemu obiektowego w języku dziedziczenie wielokrotne wprowadza wiele patologii, a jego poprawne używanie wymaga od programisty ścisłej dyscypliny i szczegółowej wiedzy o hierarchii klas. Zamiast niego, PHP wykorzystuje zaczerpniętą z Javy ideę interfejsów.

Z definicji wynika, że interfejs możemy traktować, jak coś w rodzaju klasy czysto abstrakcyjnej. Może on zawierać wyłącznie stałe klasowe, z którymi zapoznamy się w dalszej części podręcznika, oraz abstrakcyjne prototypy metod pozbawione implementacji. Tę dostarcza dopiero klasa, dlatego mówimy, że implementuje ona interfejs. Interfejsów nie dotyczą ograniczenia dziedziczenia. Programista może jednocześnie rozszerzać inną klasę oraz obok implementować dowolną liczbę interfejsów.

Przykładowe użycie

edytuj

Rozpatrzmy prosty system uprawnień. Główna klasa stanowi interfejs dostępu, który potrafi odpowiadać na pytania, czy użytkownik powinien mieć dostęp do wskazanego zasobu. Oprócz tego chcemy mieć zestaw innych klas, które potrafiłyby generować listę uprawnień dla naszego systemu. Nie chcemy ograniczać możliwości ich dziedziczenia, dlatego zamiast tego utworzymy interfejs opisujący, czego system uprawnień wymaga od twórcy generatora uprawnień, aby mógł on poprawnie działać.

<?php

interface AclGeneratorInterface
{
   public function generate();
   public function isAllowed($resource);
} // end AclGenerator;

class AclSystem
{
   private $_permissions = array();

   public function loadGenerator(AclGeneratorInterface $generator)
   {
      foreach($generator->generate() as $resource => $access)
      {
         if(!isset($this->_permissions[$resource]))
         {
            if($access == 2)
            {
               // Tutaj dostęp będzie generowany dynamicznie
               $this->_permissions[$resource] = $generator;
            }
            elseif($access == 0 || $access == 1)
            {
               $this->_permissions[$resource] = $access;
            }
         }
      }
   } // end loadGenerator();

   public function isAllowed($resource)
   {
      if(!isset($this->_permissions[$resource]))
      {
         return false;
      }
      if(is_object($this->_permissions[$resource]) && $this->_permissions[$resource] instanceof AclGeneratorInterface)
      {
         return $this->_permissions[$resource]->isAllowed($resource);
      }
      return (bool)$this->_permissions[$resource];
   } // end isAllowed();
} // end AclSystem;

Aby utworzyć interfejs, po słowie kluczowym interface wpisujemy jego unikalną nazwę, a następnie w nawiasach klamrowych wymieniamy listę wszystkich prototypów metod, których ma on dostarczać. Wszystkie metody interfejsu muszą być z założenia publiczne, dlatego nie można tu używać innych modyfikatorów dostępu, jednak powszechną konwencją jest dopisywanie słowa kluczowego public dla celów czytelności. Powyższy skrypt nie przedstawia póki co żadnej klasy, która by implementowała AclGeneratorInterface, jednak mamy pokazany kod odpowiedzialny za jego wykorzystanie. Jak widać w linii 13, interfejsy mogą być stosowane do typowania argumentów, identycznie jak klasy. W naszym przykładzie chcemy mieć pewność, że wszystkie obiekty, które przekażemy jako argument do loadGenerator() posiadały metody generate() oraz isAllowed() bez względu na ich położenie w hierarchii klas.

W linii 38 widoczna jest jeszcze jedna konstrukcja, czyli specjalny operator instanceof. Pozwala on testować czy dany obiekt jest instancją wybranej klasy lub implementuje określony interfejs.

Zobaczmy teraz, jak zaimplementować interfejs w klasie tak, by PHP o tym wiedział. Napiszemy przykładowy generator, który wczyta uprawnienia z pliku.

<?php

class FileGenerator implements AclGeneratorInterface
{
   private $_role = '';

   public function __construct($role)
   {
      $this->_role = (string)$role;
   } // end __construct();

   public function generate()
   {
      $acl = parse_ini_file('./access/'.$this->_role.'.ini');
      foreach($acl as &$permission)
      {
         if($permission != 0 && $permission != 1)
         {
            $permission = 0;
         }
      }
      return $acl;
   } // end generate();

   public function isAllowed($resource)
   {
      return false;
   } // end isAllowed();
} // end FileGenerator;

Aby zaimplementować interfejs, wystarczy po nazwie klasy podać słowo kluczowe implements i wymienić listę interfejsów, oddzielając je przecinkami. Musimy pamiętać o następujących ograniczeniach:

  1. Implementowane metody muszą mieć dokładnie taki sam nagłówek, jak w interfejsie.
  2. Klasa nie może implementować dwóch (lub więcej) interfejsów, które mają metodę o tej samej nazwie (od PHP 5.3.9 może, pod warunkiem, że zduplikowane metody mają taki sam nagłówek).
  3. Jeżeli klasa odziedziczyła jakąś metodę, której wymaga implementowany interfejs, jej nagłówek musi być zgodny z zawartością interfejsu.

Poniżej pokazany jest przykład błędnego nazewnictwa metod:

<?php
interface GooInterface
{
	public function bar($arg1, $arg2);
} // end GooInterface;

class Foo
{
	public function foo()
	{
		echo 'foo';
	} // end foo();
	
	public function bar($joe = 'joe')
	{
		echo 'bar';
	} // end bar();
} // end Foo;

class Bar extends Foo implements GooInterface
{
	public function joe()
	{
		echo 'joe';
	} // end joe();
} // end GooInterface;

Po uruchomieniu tego skryptu zobaczymy:

Fatal error: Declaration of Foo::bar() must be compatible with that of GooInterface::bar() in /test.php on line 22

Mówi on, że odziedziczona metoda bar() jest niezgodna z interfejsem, który właśnie próbujemy zaimplementować.

Dziedziczenie interfejsów

edytuj

Interfejsy można również dziedziczyć dokładnie tak samo, jak klasy, przy pomocy słowa kluczowego extends. Tutaj jednak wielokrotne dziedziczenie jest jak najbardziej dozwolone pod warunkiem, że nie ma konfliktów metod. Dziedziczenie interfejsów jest rzadko spotykane w rzeczywistych skryptach, lecz warto wiedzieć, że ono istnieje. Poniżej przedstawiony jest przykładowy skrypt pokazujący sposób wykorzystania i działanie:

<?php
interface Foo
{
	public function foo();
} // end Foo;

interface Bar
{
	public function bar();
} // end Bar;

interface Joe extends Foo, Bar
{
	public function joe();
} // end Joe;

class Abc implements Joe
{
	public function foo()
	{
		echo 'foo';
	} // end foo();
	
	public function bar()
	{
		echo 'bar';
	} // end foo();
	
	public function joe()
	{
		echo 'joe';
	} // end foo();
} // end Abc;

$class = new Abc;
if($class instanceof Foo)
{
	echo 'Ten obiekt implementuje interfejs Foo<br/>';
}
if($class instanceof Bar)
{
	echo 'Ten obiekt implementuje interfejs Bar<br/>';
}
if($class instanceof Joe)
{
	echo 'Ten obiekt implementuje interfejs Joe<br/>';
}

Interfejsy wbudowane

edytuj

Język PHP posiada kilkanaście specjalnych interfejsów rozpoznawanych przez interpreter i przeznaczonych do dodatkowych zastosowań. Wchodzą one w skład tzw. Standard PHP Library, czyli nowej standardowej biblioteki PHP zbudowanej w całości w oparciu o programowanie obiektowe. Będziemy się z nią zapoznawać po kawałku również w dalszej części podręcznika.

W poprzednich rozdziałach poznaliśmy funkcję sizeof() (znaną też jako count()), która zwraca ilość elementów w tablicy. Może ona współpracować także z obiektami, zwracając ilość publicznych pól:

<?php
class Counter
{
	public $foo;
	protected $bar;
}

$counter = new Counter;
echo sizeof($counter);

Wiedza o ilości pól jest rzadko potrzebna w praktyce, jednak nic nie stoi na przeszkodzie, aby to przeprogramować. Tutaj przyda nam się pierwszy specjalny interfejs o nazwie Countable. Dostarcza on metodę count(), którą PHP wywołuje, gdy chce uzyskać informacje o ilości elementów.

Pierwszym specjalnym interfejsem, jaki poznamy, jest Countable, który dostarcza metodę count(). Informuje on interpreter, że klasa, która go implementuje, jest zbiorem elementów, które można policzyć (identycznie, jak elementy w tablicy). Rozbudujmy klasę Config z pierwszego rozdziału tak, aby można było uzyskać informacje o ilości aktualnie załadowanych opcji. Zakładamy, że wykonałeś ćwiczenie z poprzedniego rozdziału dotyczące jej rozbudowy:

<?php
class Config implements Countable
{
   private $_config = array();
   private $_awaitingLoaders = array();

   public function count()
   {
      return sizeof($this->_config);
   } // end count();

   // pozostała część klasy
} // end Config;

Teraz możemy prosto dowiedzieć się, ile opcji aktualnie znajduje się w konfiguracji:

<?php
require('./Config.php');
$config = new Config;
echo 'Ilość elementów w konfiguracji: '.sizeof($config);

Obiekty można jeszcze bardziej upodobnić do tablic dzięki interfejsowi ArrayAccess. Dostarcza on czterech metod:

  • offsetGet($key) - wywoływana przy próbie odczytu: $obiekt['klucz']
  • offsetSet($key, $value) - wywoływana przy próbie zapisu: $obiekt['klucz'] = 5
  • offsetExists($key) - wywoływana przy próbie sprawdzenia, czy element o podanym kluczu istnieje: isset($obiekt['klucz'])
  • offsetUnset($key) - wywoływana przy próbie usunięcia elementu o podanym kluczu: unset($obiekt['klucz'])

Aby przećwiczyć interfejsy w praktyce, cofnijmy się do przykładu z systemem formularzy z poprzedniego rozdziału. Do klasy abstrakcyjnej FormElement dodamy specjalne chronione pole $_attributes będące tablicą przechowującą dodatkowe atrybuty dla elementu formularza (np. klasa CSS, ID itd.). Klasa musi implementować interfejsy ArrayAccess oraz Countable, które pozwolą na proste zarządzanie atrybutami tak, jakby obiekt elementu był tablicą. Zaimplementuj wszystkie wymagane metody tak, aby odpowiednio modyfikowały pole $_attributes. Zmodyfikuj klasy odpowiednich elementów tak, aby uwzględniały dodane atrybuty w generowanym kodzie HTML. Poniżej znajduje się przykładowy plik testowy:

<?php
 
require('./FormElements.php');
require('./FormBuilder.php');
 
$form = new FormBuilder;
$element = $form->addElement(new FormInput('name'));
$element['class'] = 'name';
$element['id'] = 'f_name';

if(isset($element['id']))
{
   unset($element['id']);
}
 
$form->display();

Interfejsów specjalnych jest jeszcze więcej. Wśród nich specjalną grupę stanowią tzw. iteratory. Poświęcimy im cały osobny rozdział.

Kiedy stosować?

edytuj

Nie ma jednej, uniwersalnej reguły mówiącej, kiedy należy stosować interfejsy, a kiedy dziedziczenie klas. Zasadniczo gdy chcemy dostarczyć klasie użytkownika pewną, gotową część implementacji, jesteśmy skazani na dziedziczenie, gdyż PHP nie udostępnia żadnych innych mechanizmów jej wstrzykiwania. Jednak gdy pragniemy jedynie zdefiniować listę zachowań, których oczekujemy, bez wnikania w szczegóły ich działania, interfejsy są o wiele lepszym pomysłem, gdyż mogą być implementowane niezależnie oraz nie zamykają drogi do dziedziczenia. To do nas, jako projektantów architektury aplikacji, należy odpowiedź na jakiej funkcjonalności najbardziej nam zależy i odpowiednio wybrać dostępne środki. Polecamy analizować obiektowo napisane skrypty i biblioteki, aby zapoznać się z ich budową. Naśladowanie dobrych wzorców to jedna z najlepszych szkół.

Musimy mieć świadomość, że interfejsy w połączeniu z dziedziczeniem nie gwarantują nam pełnej swobody wielokrotnego wykorzystania kodu. Dzięki interfejsom możemy swobodnie przenosić listę wymaganych zachowań, lecz nie da się przenieść implementacji metod bez użycia dziedziczenia, które ma ograniczenia. Istnieje szansa, że przyszłe wersje PHP będą oferować jeszcze jeden mechanizm do obejścia tego problemu.

Zakończenie

edytuj

Poznaliśmy już prawie wszystkie główne mechanizmy obiektowe, które dostarcza nam PHP. Interfejsy znacząco poszerzyły nasze możliwości wyrażania zależności między klasami. Kolejny rozdział poświęcony będzie profesjonalnym mechanizmom raportowania błędów przy pomocy wyjątków. Od strony technicznej nie są one częścią programowania obiektowego, lecz w PHP silnie na nim bazują i dlatego ich omówienie znajduje się właśnie tutaj.