Interfejs a klasa abstrakcyjna

Interfejsy i klasy abstrakcyjne zostały wprowadzone do php od wersji 5.0. Konstrukcje te znacznie pomagają w uporządkowaniu struktury aplikacji, pozwalają na zachowanie kontroli typów, integralność oraz logiczną budowę aplikacji.

Dzięki wdrożeniu tych konstrukcji do aplikacji możemy w lepszy i logiczniejszy sposób odwzorować rzeczywistość a zarazem ustrzec się potencjalnych błędów, poza tym zyskujemy większą kontrolę nad modelowaną obiektową rzeczywistością.

Klasy abstrakcyjne
Klasy abstrakcyjne służą do modelowania rzeczywistości, którą chcemy przenieść do logiki swojej aplikacji. Klasa abstrakcyjna definiuje w dużej ogólności pewien podstawowy model i zachowanie obiektu.

Powiedzmy, że chcemy chcemy stworzyć wirtualne zoo. W zoo znajdować się będą różnorakie zwierzęta np: małpy, lisy, wiewiórki, motyle, pszczoły, żaby, jaszczurki.

Każde zwierzę opisywane jest przez podstawowe atrybuty jak: wiek, rozmiar oraz cechy taki jak: rodzenie, chodzenie, jedzenie.

Tworzymy zatem podstawową klasę abstrakcyjną 'Zwierze’ zawierającą abstrakcyjne metody: born, go, eat:

abstract class Animal {

	protected $age;
	protected $width;
	protected $height;

	abstract protected function born();
	abstract protected function go();
	abstract protected function eat();

	protected function setAge( $years ) {
		$this->age = $years;
	}

	protected function setSize( $width, $height ) {
		$this->width = $width;
		$this->height = $height;
	}
}

Dzięki użyciu klasy abstrakcyjnej mamy pewność, że nikt nie stworzy obiektu klasy 'Zwierze’. Z logicznego punktu jest to niemożliwe gdyż nie ma zwierzęcia 'Zwierze’ – taki byt w rzeczywistości nie istnieje gdyż jest to zbyt ogólne pojęcie.

Dodatkowo zadeklarowaliśmy metody born, go, eat jako abstrakcyjne co wymusi ich implementacje w klasach dziedziczących – to też jest jak najbardziej logiczne gdyż nie ma zwierzęcia, które nie może wykonywać tych czynności. Metody te deklarujemy jako abstrakcyjne gdyż nie znając zwierzęcia nie wiemy jeszcze jak je zaimplementować. Metody abstrakcyjne muszą być puste.

Zadeklarowaliśmy również podstawowe atrybuty ($age, $width, $height), które każde zwierze posiadać musi, oraz settery które je ustawiają. Przy setterach ustawiamy specyfikator dostępu protected, dzięki temu będą mogły być wywoływane jedynie z poziomu metod klas dziedziczących. Nie ustawiamy ich jako abstrakcyjnych gdyż ich implementacja w klasie 'Animal’ jest wystarczająca i klasy potomne będą z nich korzystać.

Tworzymy teraz klasę konkretnego zwierzęcia np. małpy:

require_once 'animal.php';

class Monkey extends Animal {

	public function born() {/*@TODO*/}
	public function go() {/*@TODO*/}
	public function eat() {/*@TODO*/}

	public function __construct( $age, $size ) {
		$this->setAge( $age );
		$this->setSize( $size[0], $size[1] );
	}
}

$monkey1 = new Monkey( 2, array( 100, 20 ) );
var_dump( $monkey1 );

Musi ona posiadać metody abstrakcyjne, w ciele klasy 'Monkey’, powinny one przybrać docelową formę. Jak widać nie ma setterów, jest za to konstruktor przyjmujący parametry obiektu i wywołuje odpowiednie settery z klasy bazowej 'Animal’.

Interfejs
Klasa abstrakcyjna jest dość mocno związana logicznie z obiektami z niej dziedziczącymi. Widać jasno, że małpa i lis są integralnie związane z definicją zwierzęcia. Interfejs natomiast jedynie narzuca klasie jakie metody powinna posiadać, interface definiuje pewne właściwości, które klasa musi spełniać. Sam w sobie nie definiuje logicznej zależności pomiędzy sobą a klasą go implementującą, definiuje natomiast powiązanie pomiędzy wszystkimi klasami go implementującymi. Agreguje klasy obiektów na których można wykonać pewne operacje.

Kontynuując przykład zoo… załóżmy, że zoo może sprzedawać i kupować zwierzęta zatem możemy stworzyć interface 'Trade’:

interface Trade {
	public function sell();
	public function buy();
}

Dodatkowo, zwierzęta podlegają ubezpieczeniom zatem tworzymy interface 'Insurance’:

interface Insurance {
	public function make();
	public function withdrawn();
}

Jedna klasa może implementować wiele interfejsów, w przykładzie zoo 'Animal’ będzie implementował zarówno 'Trade’, jak i 'Insurance’. Jak widać kolejne interfejsy nie są ze sobą związane jednak wpływają na zachowanie się obiektu zwierzęcia.

class Monkey extends Animal implements Trade, Insurance {
	public function born() {/*@TODO*/}
	public function go() {/*@TODO*/}
	public function eat() {/*@TODO*/}
	
	public function sell() {/*@TODO*/}
	public function buy() {/*@TODO*/}
	
	public function make() {/*@TODO*/}
	public function withdrawn() {/*@TODO*/}
	

	public function __construct( $age, $size ) {
		$this->setAge( $age );
		$this->setSize( $size[0], $size[1] );
	}
}

Ale przecież w zoo są nie tylko zwierzęta ale także budynki i ludzie. Budynki mogą być odprzedawane a ludzie ubezpieczani zatem klasy reprezentujące te obiekty także powinny implementować te interfejsy.

Klasa 'People’ powinna implementować 'Insurance’ a klasa 'Building’ interfejs 'Trade’.

class People implements Insurance {
	public function make() {/*@TODO*/}
	public function withdrawn() {/*@TODO*/}
}
class Building implements Trade {
	public function sell() {/*@TODO*/}
	public function buy() {/*@TODO*/}
}

Widzimy zatem, że klasy reprezentujące zupełnie różne obiekty (zwierzęta, budynki) w pewnym zakresie zachowują się tak podobnie (można je kupić i sprzedać) zatem implementują interface 'Trade’.

Dzięki zapisowi tych zależności nasza aplikacja nabiera logicznego kształtu a dodatkowo dzięki parserowi zyskujemy darmową walidację, gdyż w przypadku braku metody z interfejsu php zwróci błąd – zatem nie zapomnimy o jej implementacji.

15 komentarzy do “Interfejs a klasa abstrakcyjna”

  1. Bardzo ładnie wytłumaczone zasady OOP.
    Mam tylko jedną uwagę. Czy w zdaniu „Przy setterach ustawiamy specyfikator dostępu protected, dzięki temu będą mogły być wywoływane jedynie z poziomu metod klas dziedziczących” zamiast słowa „jedynie” nie powinno być „również”?

  2. Dzięki. Skoro klasa Animal jest abstrakcyjna to nie można utworzyć jej obiektu (parser wyrzuci błąd) tylko trzeba stworzyć obiekt klasy dziedziczącej. Zatem nie ma możliwości odwołać się do settera z poziomu jej obiektu, z poza obiektu także nie można się odwołać (musiła być public) więc zostaje jedynie ostatnia możliwość wywołania go 'z poziomu metod klas dziedziczących’.

  3. Dzięki za bardzo przystępny opis – dzięki temu jest dużo łatwiej zrozumieć ten problem.

  4. Czasownik opisujący fakt narodzin to „bear” albo „birth”, nie „born”. Odnosi się on jednak wyłącznie do wydawania na świat potomstwa. Nie wiem, czy w angielskim istnieje czasownik w stronie czynnej oznaczający „rodzenie się” (bo chyba takie znaczenie miała metoda, którą opisywałeś).

  5. Dziękuje panowie za uwagi z zakresu stylistyki języka angielskiego. Wybaczycie jednak, że nie będe poprawiał tekstu gdyż temat artykułu nie dotyczy kultury języka a przesłanie autora chyba jest dość jasne.

  6. Dzięki pomogłeś i mi.
    Jednakże na tym przykładze nie mogę zrozumieć po co są implementacje metod jak np
    public function born() {/*@TODO*/}
    Skoro przy formie abstract – obiekty są ze sobą integralnie powiązane to jakie tutaj mogłoby być wytłumaczenie (związane z tym przykładem) tych różnych implementacji.

  7. Chodzi o to, że klasy obiektów, które dziedziczą po klasie abstrakcyjnej mogą różnie obsługiwać tą metodę (ale muszą). Czyli kontynuując przykład zwierząt w metodzie born klasy Monkey będą ustawiane atrybuty klasy: legs=4, type=mammal (małpa ma 4 nogi i jest ssakiem) a gdy stworzymy klase „Snail” (ślimak) to ustawimy mu typ na „ślimak” i 1 noga ;-)

    Generalnie każda metoda, która dziedziczy po klasie abstracyjnej musi implementować abstrakcyje metody tej klasy.

  8. O to chodziło- tego właśnie szukałem.
    Będę przeglądał blog, bo zauważyłem,
    że można na nim znaleźć wiele ciekawych rzeczy.

  9. Dobre podstawy OOP. Warto by wspomniec też o enkapsulacji tak BTW modyfikatorów dostępu.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *