środa, 18 stycznia 2017

[PHP][OOP] Abstrakcja, interfejsy i wzorzec Factory

TRUE
4704597062652094618
Aktualizacja: 20.01.2017 15:00. Większość początkujących w programowaniu obiektowym programistów ma problemy ze zrozumieniem w jaki sposób prawidłowo wykorzystywać mechanizmy takie jak dziedziczenie, abstrakcje i interfejsy. Różne tutoriale, czy poradniki dostępne w sieci nie ułatwiają tego, gdyż zwykle opisują one jedynie zasadę działania tych mechanizmów, bez pokazania praktycznego tego zastosowania - do takiej wiedzy trzeba już sięgnąć do książek, ale i te nierzadko traktują sprawę po macoszemu. O ile koncepcja tego wszystkiego jest prosta, to dużo ludzi ma na początku problemy z wyczuciem tego w jaki sposób prawidłowo wykorzystywać to w praktyce. Początkujący programiści jedynie wiedzą, że takie coś jest, ale nie bardzo są w stanie zrozumieć, do czego to wszystko tak naprawdę może posłużyć. W tym artykule przeanalizujemy sobie więc do czego tak naprawdę użyć tych wszystkich mechanizmów i sprawimy, że słowo abstrakcja nie będzie się już kojarzyć nikomu z żadną czarną magią, tudzież z czymś trudnym do zrozumienia. Dzisiaj zaprzyjaźnimy się z ową abstrakcją..

Czym tak naprawdę jest abstrakcja?

Na pewno wiecie, czym są klasy abstrakcyjne, a przynajmniej jak takie klasy definiujemy. Jak wiadomo klasy abstrakcyjne definiujemy za pomocą abstract class, następnie podajemy w definicji klasy metody abstrakcyjne, które będą musiały zostać pokryte we wszystkich (tym razem już nie-abstrakcyjnych) klasach dziedziczących po naszej nadrzędnej abstrakcyjnej klasie. Czym więc tutaj jest ta magiczna abstrakcja? Otóż wymusza ona niejako na wszystkich klasach potomnych utworzenie dokładnie takich metod, jakie w klasie bazowej (tej abstrakcyjnej) zostały określone. Klasa abstrakcyjna sama w sobie nie posiada definicji tychże metod, posiada jedynie ich deklaracje. Możemy więc powiedzieć, że do momentu utworzenia tych metod w klasach potomnych (dziedziczących) wszystkie te metody tak naprawdę nie istnieją, istnieją jedynie abstrakcyjnie. Niby są, a jednak jeszcze ich nie ma, gdyż istnieje dopiero póki co ich deklaracja, że takowe prawdopodobnie się gdzieś dalej w kodzie pojawią. Tym właśnie jest abstrakcja.

Przyjrzyjmy się najprostrzej klasie abstrakcyjnej:
[code]
<?php
abstract class AbstractHuman {

  // kod
}
[/code]

Powyżej deklarujemy klasę abstrakcyjną o nazwie AbstractHuman. Stworzymy za chwilę klasy dziedziczące po niej, czyli Man i Woman. Nadrzędna klasa AbstractHuman pozostanie jedynie abstrakcją, gdyż niejako AbstractHuman jest to dość szerokie pojęcie. Dopiero klasy Man i Woman tak naprawdę sprawią, że pojęcie AbstractHuman w jakiś sposób przyjmie wartość "namacalną". Tak jak w prawdziwym życiu - mówiąc o człowieku, mamy na myśli człowieka jako takiego, ale dopiero podanie pełniejszych informacji - o kim tak naprawdę mowa daje nam fizyczny obraz danej, konkretnej osoby. Samo słowo "człowiek" jest zbyt abstrakcyjne, zbyt ogólne, aby można było w jego przypadku określić o kim mówimy. Idźmy więc dalej, utwórzmy sobie 2 kolejne klasy, które będą dziedziczyć po naszej klasie abstrakcyjnej. Przy okazji pamiętajmy, aby klasy abstrakcyjne oznaczać sobie w nazwie, najlepiej za pomocą prefixu Abstract - pozwoli nam to na szybsze rozróżnienie później w kodzie, czy mamy do czynienia ze zwykłą, czy z abstrakcyjną klasą, ponadto taki wzorzec nazewnictwa jest zalecany przez standard PSR-2.
[code]
<?php
class Man extends AbstractHuman {

  // kod
}

class Woman extends AbstractHuman {

  // kod
}
[/code]
Jak widać, mamy póki co same deklaracje, bez żadnego kodu składającego się na dane klasy. Dodajmy zatem trochę kodu, ale najpierw przemyślmy sprawę - jakie metody powinny posiadać klasy Man i Woman, aby mogły one symbolizować czynności wykonywane przez mężczyznę, czy kobietę w realnym życiu? Na pewno każda kobieta i każdy mężczyzna muszą jeść, pić, spać i oddychać, aby w ogóle żyć. Uwzględnimy zatem takie metody w naszych klasach, ale zaraz zaraz - jako, iż są to metody wspólne zarówno dla kobiet, jak i dla mężczyzn, to pojawią się one zarówno w klasie Man, jak i Woman, a tym samym będą one częścią wspólną dla każdego obiektu klasy AbstractHuman. Określmy więc te metody w deklaracji klasy AbstractHuman, gdyż zarówno Man, jak i Woman będą musiały te metody pokryć.
[code]
<?php
abstract class AbstractHuman {

  abstract public function eat();
  abstract public function drink();
  abstract public function breathe();
  abstract public function sleep();
}
[/code]
Każdą metodę abstrakcyjną definiujemy w klasie poprzedzając ją klauzulą abstract. Wszystkie tego typu zadeklarowane metody będą musiały zostać pokryte w klasach dziedziczących. Jeśli nie zostaną pokryte, to PHP wygeneruje nam błąd.

PHP wygeneruje nam również błąd jeśli spróbujemy utworzyć obiekt klasy abstrakcyjnej:
[code]
<?php
$human  = new AbstractHuman;
[/code]
Nie ma możliwości utworzenia obiektu klasy abstrakcyjnej. Klasa abstrakcyjna służy jedynie jako "podstawa", dla właściwych klas po niej dziedziczących. Należy o tym pamiętać!

W definicji klasy abstrakcyjnej możemy również zadeklarować zwykłe metody, nie tylko abstrakcyjne. Zwykłe metody nie muszą być pokrywane w klasach potomnych, np.:
[code]
<?php
abstract class AbstractHuman {

  abstract public function eat();
  abstract public function drink();
  abstract public function breathe();
  abstract public function sleep();
  public function sayHello()
  {
    echo 'Hello!';
  }
}
[/code]
W powyższym przykładzie metoda sayHello nie musi być pokryta w klasach potomnych, wszystkie 4 poprzednie abstrakcyjne już tak. Pokryjmy je zatem w naszych klasach potomnych. Finalny kod będzie wyglądał tak:
[code]
<?php
abstract class AbstractHuman {

  abstract public function eat();
  abstract public function drink();
  abstract public function breathe();
  abstract public function sleep();
  public function sayHello()
  {
    echo 'Hello!';
  }
}

class Man extends AbstractHuman {

  public function eat()
  {
    echo 'Jem hamburgera';
  }
  public function drink()
  {
    echo 'Pije piwo';
  }
  public function breathe()
  {
    echo 'Oddycham';
  }
  public function sleep()
  {
    echo 'Ide spac';
  }
}

class Woman extends AbstractHuman {

  public function eat()
  {
    echo 'Jem jogurt';
  }
  public function drink()
  {
    echo 'Pije wode';
  }
  public function breathe()
  {
    echo 'Oddycham';
  }
  public function sleep()
  {
    echo 'Ide spac';
  }
}

$man = new Man;
$man->eat(); // wyświetli: 'Jem hamburgera'
[/code]
Usuwając teraz którąś z wymaganych metod, PHP wygeneruje nam Fatal Error i wyświetli komunikat o braku pokrycia jednej z metod. Do czego więc może nam posłużyć ten fakt? Wszak przecież mogliśmy jako bazowej użyć zwykłą klasę. Tutaj mała dygresja - otóż dana metoda nie musi być pokrywana w klasie dziedziczącej, ale pod warunkiem, że klasa dziedzicząca również jest klasą abstrakcyjną. Wróćmy jednak do tematu - klasa abstrakcyjna wymusza na programiście pokrycie wszystkich zachowań nie bez powodu. Wyobraźmy sobie teraz, że mamy dość rozbudowaną aplikację, a w niej korzystamy z obiektów, które mogą być wymieniane na inne, np. z jakiegoś systemu pluginów, gdzie wykorzystywany obiekt nie jest ustalony na sztywno w naszym kodzie, a jest dołączany dynamicznie, np. za pomocą wzorca projektowego Factory (o tym za chwilę). Przykładowo:
[code]
<?php
$plugin = new MyPlugin;
$plugin->action();
[/code]
To oczywiście uproszczony przykład, ale wyobraźmy sobie, że wszystkie tego typu pluginy będą ładowne w podobny sposób, a następnie nasz kod będzie na nich operować. Co teraz się stanie, jeśli taki plugin nie będzie posiadał definicji metody action(), jak w przykładzie powyżej? Ano właśnie, dostaniemy Fatal Error i na tym skończy się działanie aplikacji. Aby więc uniknąć takiej sytuacji, wymusić musimy na wszystkich klasach tworzących nasze pluginy definicje wszystkich wymaganych metod. Przy okazji - pamiętajmy, że w pokrywanej metodzie musi zgadzać się nie tylko nazwa z jej wersją abstrakcyjną, ale również i ilość przyjmowanych argumentów! Wracając jednak do kwestii wymuszenia pokrycia naszych określonych metod, to do tego właśnie użyjemy bazowej klasy abstrakcyjnej, która wymusi na wszystkich klasach potomnych taki zestaw metod, np.:
[code]
<?php
abstract class AbstractPlugin {

  abstract public function action();
}


class MyPlugin extends AbstractPlugin {

  public function action()
  {
    // kod
  }
}
[/code]
Wymusiliśmy na klasie MyPlugin pokrycie metody action() z abstrakcyjnej klasy bazowej. Ale to nie koniec. Tworząc tego typu system pluginów musimy skorzystać z czegoś jeszcze.


Wzorzec Fabryki - Factory Design Pattern

Jest to jeden z najczęściej wykorzystywanych wzorców projektowych, z którym zapewne już się spotkaliście. Opiszę go szerzej w którymś z kolejnych artykułów w dziale o wzorcach projektowych, a tymczasem powiemy sobie pokrótce czym on jest i jak go wykorzystać. Wzorzec Factory (Fabryka) pozwala nam na ładowanie w danym miejscu kodu dowolnej, wybranej przez nas klasy, a nie klasy ustalonej na sztywno w kodzie. Działa to na takiej zasadzie, że to dopiero obiekt Fabryki zwraca nam klasę, która użyta zostanie w naszym kodzie. Wyobraźmy sobie np., że tworzymy aplikację, która będzie mogła zapisywać łańcuch tekstowy - jakiś dowolny tekst zarówno w bazie danych jak i w pliku tekstowym. Zarówno klasa zapisująca do bazy, jak i klasa zapisująca do pliku posiadać będą np. po 2 metodach: setText() - która pobierze tekst do zapisu i save() - która ten tekst po swojemu zapisze. Mamy więc dwie klasy, z czego jedna zapisuje dane do bazy, a druga do pliku tekstowego. A co jeśli w przyszłości będziemy chcieli dodać funkcję zapisu np. do pliku PDF? Będziemy musieli stworzyć kolejną klasę, ale jednocześnie i przepisać nasz kod, tak aby uwzględniał nową opcję. Wzorzec fabryki pozwoli nam na ominięcie tego drugiego etapu. Wyobraźmy sobie, że teraz stworzymy sobie obiekt nadrzędny, który to obiekt dopiero zwracać nam będzie odpowiednią klasę służącą do zapisu według naszych potrzeb. Zobaczmy to na przykładzie:

Najpierw pierwotny przykład, bez zastosowania wzorca Factory:
[code]
<?php

$text = 'Hello World!';
$saveMode = 'db';

switch($saveMode)
{
  case 'db':
    $obj = new DBSaver;
    $obj->setText($text);
    $obj->save();
  break;

  case 'file':
    $obj = new FileSaver;
    $obj->setText($text);
    $obj->save();
  break;

  case 'pdf':
    $obj = new PDFSaver;
    $obj->setText($text);
    $obj->save();
  break;
}
[/code]
Jak widać jest to mało eleganckie, a za każdym razem, gdy zapragniemy rozbudowę o np. kolejny sposób zapisu, będziemy musieli przepisywać nasz kod wszędzie tam, gdzie będzie istniała funkcja zapisu. Mało praktycznie. I tutaj z pomocą przychodzi nam właśnie wzorzec Factory. To czym zajmuje się w powyższym kodzie instrukcja switch() przeniesiemy do naszej Fabryki, która w zależności od potrzeby zwróci nam odpowiednią klasę służącą do zapisu danych. Spójrzmy na przykład:
[code]
<?php

class SaveFactory {

  private $savers = [];
  private $name;

  public function __construct($name) {

    $this->name = $name;
  }

  public function registerSaver($name, $class)
  {
    $this->savers[$name] = new $class;
  }

  public function load()
  {
    return $this->savers[$this->name];
  }
}

[/code]
Jak widzimy, nasza fabryka w tablicy $savers przechowuje zestaw możliwych do użycia obiektów, które mogą posłużyć do zapisu naszych danych i dopiero po wywołaniu metody load() zwraca nam odpowiednią, określoną za pomocą zmiennej $name klasę. No dobrze, zobaczmy teraz jak to wszystko będzie wyglądać w naszym kodzie.
[code]
<?php

$text = 'Hello World!';
$name = 'db';

// ładujemy fabrykę
$saverFactory = new SaveFactory($name);
$obj = $saverFactory->load();

// korzystamy z klasy zwróconej przez fabrykę
$obj->setText($text);
$obj->save();
[/code]
Prawda, że bardziej elegancko? Od tej chwili to Fabryka zajmuje się udostępnieniem nam odpowiedniej klasy służącej do zapisu naszego pliku. Fabryka przyjmując argument konstruktora, zwraca nam odpowiednią klasę, a nas nie obchodzi już w kodzie jaki to jest obiekt. Jeśli natomiast kiedyś w przyszłości postanowimy rozbudować naszą aplikację o jeszcze inną formę zapisu danych, to wystarczy, że dołożymy nową klasę do tablicy zawierającej nasze klasy w klasie Fabryki. Reszta naszego kodu nie będzie już wymagała żadnej zmiany, gdyż za zwrócenie odpowiedniej klasy odpowiedzialna jest już w tym momencie tylko i wyłącznie nasza fabryka. Prześledzimy jeszcze ten wzorzec za chwilkę na innym przykładzie, ale zanim do tego dotrzemy musimy omówić sobie jeszcze jedną rzecz. Otóż korzystając z takiego wzorca zapewne zauważyliśmy już, że każda z klas zwracana nam przez Fabrykę posiadać tutaj musi 2 wymagane metody: setText() oraz save(). Jest to konieczne, gdyż pamiętajmy, że pracując na obiekcie zwróconym przez Fabrykę tak naprawdę nie sprawdzamy jaki jest to obiekt i ufamy, że posiada on implementację wszystkich wymaganych funkcjonalności (poniekąd, ponieważ i tak sprawdzimy to w kodzie za pomocą np. instanceof, ale z punktu widzenia logiki zakładamy, że powinien się tu znaleźć właśnie taki, a nie inny typ klasy). Wymusić zatem musimy na takim obiekcie pokrycie wszystkich wymaganych metod, co pokazaliśmy już sobie kilka akapitów wyżej, przy okazji klas abstrakcyjnych. I tak też właśnie możemy to zrobić, tj. wymusić, aby każda z naszych klas obsługujących zapis dziedziczyła po klasie abstrakcyjnej, w której zdefiniujemy wymagane do pokrycia metody. Stwórzmy zatem naszą bazową abstrakcyjną klasę i przebudujmy nieco naszą Fabrykę:
[code]
<?php
abstract class AbstractBaseSaver {

  abstract public function setText($text);
  abstract public function save();
}
[/code]
Następnie wszystkie z naszych klas zapisujących muszą dziedziczyć po klasie powyższej, przykładowo klasa zapisująca do pliku tekstowego:
[code]
<?php
class FileSaver extends AbstractBaseSaver {

  private $text;

  public function setText($text)
  {
    $this->text = $text;
  }

  public function save()
  {
    file_put_contents('save.txt', $this->text);
  }
}
[/code]
Na koniec rozbudujemy naszą Fabrykę o warunek sprawdzający, czy dodawana do tablicy dostępnych "saverów" klasa, aby na pewno dziedziczy po klasie AbstractBaseSaver.
Zrobimy to za pomocą podania wymaganego typu argumentu w metodzie registerSaver():

[code]public function registerSaver($name, AbstractBaseSaver $class)[/code]
Powyższym sprawdzamy, czy obiekt $class jest typu AbstractBaseSaver. Fabryka w całości:
[code]
<?php

class SaveFactory {

  private $savers = [];
  private $name;

  public function __construct($name) {

    $this->name = $name;
 
    // $this->registerSaver(...)
    // ....
  }

  public function registerSaver($name, AbstractBaseSaver $class)
  {
    $this->savers[$name] = new $class;
  }

  public function load()
  {
    return $this->savers[$this->name];
  }
}
[/code]

Lepszym jednak rozwiązaniem, niż powyższe będzie skorzystanie z interfejsów.

Interfejsy

Interfejsy na pierwszy rzut oka są zbliżone do klas abstrakcyjnych, ale to tylko pozory. Przede wszystkim interfejs implementujemy, a nie dziedziczymy. Po drugie - każda klasa może implementować więcej, niż jeden interfejs, podczas gdy w przypadku dziedziczenia dziedziczyć możemy tylko po jednej klasie. Interfejsy w użyciu są jednak zbliżone do klas, różnią się jednak lekko składnią. Należy pamiętać, że wszystkie metody określane w interfejsie są abstrakcyjne, zatem każda z nich musi zostać pokryta w klasach implementujących. I tak, o ile klasę abstrakcyjną deklarujemy za pomocą:
[code]
<?php
abstract class AbstractNazwaKlasy {

  // kod klasy
}
[/code]
tak interfejs deklarujemy nastepująco:
[code]
<?php
interface NazwaInterfejsu {

  // kod interfejsu
}
[/code]
Następna różnica jest w dziedziczeniu, o ile dziedziczenie dla klasy definiujemy tak:
[code]
<?php
class B extends class A

  // kod
}
[/code]
O tyle interfejsu nie dziedziczymy, a implementujemy:
[code]
<?php
class B implements NazwaInterfejsu {

  // kod
}
[/code]
lub jeśli implementujemy więcej, niż jeden interfejs, to podajemy ich nazwy po przecinku:
[code]
<?php
class B implements
  NazwaInterfejsu1,
  NazwaInterfejsu2,
  NazwaInterfejsu3,
  ....
{

  // kod
}
[/code]
Tutaj bardzo ważna uwaga, którą powinniśmy sobie zapamiętać na przyszłość - otóż warto oznaczać interfejsy, tak, aby już po samej nazwie wiadomo było, że mowa jest o interfejsie, a nie o klasie. Najlepiej jeśli dodamy do nazwy sufiks Interface. Nie jest to konieczne, ale wprowadza to dużo przejrzystości do kodu, polecam się zatem trzymać tej zasady, czyli nazywać swoje interfejsy wg schematu:

[code]NazwaInterfejsuInterface[/code]

No dobrze, czym więc są te interfejsy? Najogólniej rzecz biorąc, interfejs możemy nazwać czymś w rodzaju definicji wymaganych metod dla klas go implementujących. Podobnie jak w przypadku klas abstrakcyjnych - wszystkie zadeklarowane w interfejsie metody muszą zostać pokryte we wszystkich klasach, które go implementują. Analogicznie jak w przypadku klas abstrakcyjnych jedynie deklarujemy nazwy i zestawy argumentów dla metod, natomiast sam ich kod musi już zostać stworzony w klasach implementujących. Prześledźmy na przykładzie, tworząc sobie tym razem już nie klasę abstrakcyjną, a interfejs Human:
[code]
<?php
interface HumanInterface {

  public function eat();
  public function drink();
  public function breathe();
  public function sleep();
}

class Man implements HumanInterface {

  public function eat() { /* ... */ };
  public function drink() { /* ... */ };
  public function breathe() { /* ... */ };
  public function sleep() { /* ... */ };
}

class Woman implements HumanInterface {

  public function eat() { /* ... */ };
  public function drink() { /* ... */ };
  public function breathe() { /* ... */ };
  public function sleep() { /* ... */ };
}
[/code]
Jak widzimy, jest bardzo podobnie jak w przypadku klasy abstrakcyjnej. Tak naprawdę interfejs to również abstrakcja, jednakże pozwalająca na trochę więcej manewrów, niż w przypadku zwykłego dziedziczenia. Warto tutaj wspomnieć kilka spraw. Pierwszą z nich jest to, że implementację interfejsu można połączyć z dziedziczeniem. Zobaczmy poniższy przykład:
[code]
<?php

class LifeForm {

  public function doBirth()
  {
    // kod
  }

  public function doDie()
  {
    //kod
  }
}

interface HumanInterface {

  public function eat();
  public function drink();
  public function breathe();
  public function sleep();
}

class Man extends LifeForm implements HumanInterface {

  public function eat() { /* ... */ };
  public function drink() { /* ... */ };
  public function breathe() { /* ... */ };
  public function sleep() { /* ... */ };
}

class Woman extends LifeForm implements HumanInterface {

  public function eat() { /* ... */ };
  public function drink() { /* ... */ };
  public function breathe() { /* ... */ };
  public function sleep() { /* ... */ };
}
[/code]
Jak widzimy, klasy Man i Woman dziedziczą po klasie bazowej LifeForm (Forma Życia) i jednocześnie implementują zachowania z interfejsu HumanInterface. Warto tutaj zaznaczyć kolejną rzecz - w interfejsie (podobnie jak i w klasie abstrakcyjnej) deklarujemy jedynie publiczne metody. Abstrakcja ma z założenia definiować nam zestaw metod czegoś w rodzaju API danej klasy, zatem wszystkie te metody muszą być zadeklarowane jako public. Naturalnie wszystkie klasy implementujące interfejs, lub dziedziczące po klasie abstrakcyjnej mogą posiadać dowolną ilość innych metod, które nie zostały zdeklarowane w interfejsie, czy w klasie bazowej. Np. w klasie Woman możemy stworzyć kolejną metodę, o nazwie np.

[code]public function goPregnant() { /* ... */ }[/code]

Nic nie stoi na przeszkodzie do rozbudowy klas o metody, których nie ma w klasie bazowej lub interfejsie.

Jedziemy dalej - jak wcześniej wspomniałem, jedna klasa może implementować po kilka interfejsów i zobaczymy teraz do czego można tego użyć. Musimy jednak pamiętać bardzo ważną rzecz - wszystkie metody z implementowanych interfejsów muszą zostać w pełni pokryte! Spójrzy jednak najpierw na przykładowy kod:
[code]
<?php

class LifeForm {

  public function doBirth()
  {
    // kod
  }

  public function doDie()
  {
    //kod
  }
}

interface HumanInterface {

  public function eat();
  public function drink();
  public function breathe();
  public function sleep();
}

interface MovementInterface {
  public function walk();
  public function run();
}

class Man extends LifeForm implements
  HumanInterface,
  MovementInterface
{

  public function eat() { /* ... */ };
  public function drink() { /* ... */ };
  public function breathe() { /* ... */ };
  public function sleep() { /* ... */ };

  public function walk() { /* ... */ };
  public function run() { /* ... */ };
}

class Woman extends LifeForm implements
  HumanInterface,
  MovementInterface
{

  public function eat() { /* ... */ };
  public function drink() { /* ... */ };
  public function breathe() { /* ... */ };
  public function sleep() { /* ... */ };

  public function walk() { /* ... */ };
  public function run() { /* ... */ };
}
[/code]
Jak widzimy, dołożyliśmy tutaj implementację kolejnego interfejsu - MovementInterface, który deklaruje nam metody służące do poruszania się. Ktoś może zapytać - ale dlaczego nie zadeklarować by metod od poruszania bezpośrednio w interfejsie HumanInterface? Ano dlatego, że człowiek nie jest jedyną formą życia, która potrafi chodzić i biegać, możemy np. dodać kolejny interfejs o nazwie AnimalInterface:
[code]
interface AnimalInterface {
  public function eat();
  public function drink();
  public function breathe();
  public function sleep();
}[/code]
i klasę Dog go implementującą. Ale, ale - no właśnie, pies również potrafi chodzić i biegać, zatem dołożymy mu jeszcze implementację interfejsu MovementInterface. W przypadku, gdybyśmy akcje odpowiedzialne za poruszanie zamknęli w interfejsie HumanInterface ograniczylibyśmy możliwość poruszania się jedynie dla człowieka. Tworząc oddzielny interfejs sprawiliśmy, że możemy go wykorzystać nie tylko dla człowieka, ale również i dla innych form życia:
[code]
<?php
class Dog extends LifeForm implements
  AnimalInterface,
  MovementInterface
{

  public function eat() { /* ... */ };
  public function drink() { /* ... */ };
  public function breathe() { /* ... */ };
  public function sleep() { /* ... */ };

  public function walk() { /* ... */ };
  public function run() { /* ... */ };
}
[/code]

Warto również nadmienić, że interfejs może też dziedziczyć po innym interfejsie, np.:
[code]<?php
interface NazwaInterfejsuInterface extends InnyInterfejs1Interface, InnyInterfejs2Interface {
  //kod
}
[/code]

Jak teraz w praktyce wykorzystać mechanizmy jakie oferują interfejsy?
Pamiętacie nasz system zapisu danych jaki tworzyliśmy kilka akapitów wyżej za pomocą wzorca Factory i dziedziczenia po klasie abstrakcyjnej? Zróbmy teraz coś podobnego, ale tym razem wykorzystajmy do tego interfejs. Wyobraźmy sobie, że chcemy stworzyć aplikację, która będzie nam wyświetlała podany tekst na kilka różnych sposobów. Za każdy z tych sposobów będzie odpowiadać oddzielna klasa. Wszystkie te klasy będą implementować wspólny interfejs, a wybór odpowiedniej klasy, która aktualnie będzie zajmować się wyświetlaniem naszego tekstu będzie się odbywać za pomocą wzorca Fabryki.

Na samym początku utworzymy interfejs, który wymagać będzie zaimplementowania dwóch publicznych metod - jednej do pobrania tekstu, a drugiej do jego wyświetlenia:
[code]
<?php
interface RendererInterface {

  public function setText($text); // metoda pobierająca tekst do wyświetlenia
  public function render(); // metoda renderująca tekst
}
[/code]

Następnie stwórzmy 2 różne klasy implementujące powyższy zestaw metod z interfejsu RendererInterface, które będą zajmować się wyświetleniem naszego tekstu. Jedna z klas niech wyświetla nam tekst otoczony w nagłówek <h1>, a druga niech wyświetla go w formacie JSON:
[code]
<?php
// Tekst renderujący się w nagłówku <h1>
class HtmlRenderer implements RendererInterface {

  private $text;

  public function setText($text)
  {
    $this->text = $text;  
  }

  public function render()
  {
    echo '<h1>' . $this->text . '</h1>';  
  }
}

// Tekst renderujący się w formacie JSON
class JsonRenderer implements RendererInterface {

  private $text;

  public function setText($text)
  {
    $this->text = $text;  
  }

  public function render()
  {
    $ary = array('text' => $this->text);
    echo json_encode($ary);
  }
}
[/code]
Mamy teraz dwie różne klasy, które w różny sposób "obsłużą" nasz wejściowy tekst. Stwórzmy teraz Fabrykę, która w razie potrzeb będzie zwracała nam odpowiednią klasę renderującą:
[code]
<?php

class RendererFactory {

  private $renderers = [];
  private $name;

  public function __construct($name) {

    $this->name = $name;
 
    // dodajemy nasze klasy renderujące do tablicy
    $this->registerRenderer('html', new HtmlRenderer);
    $this->registerRenderer('json', new JsonRenderer);
 
  }

  public function registerRenderer($name, RendererInterface $class)
  {
    $this->renderers[$name] = $class;
  }

  public function load()
  {
    return $this->renderers[$this->name];
  }
}
[/code]

Finalnie pozostaje nam już jedynie napisanie kodu głównego, który skorzysta z naszej Fabryki i wyświetli tekst w odpowiednim formacie. Aby wygodnie przełączać się pomiędzy jednym, a drugim sposobem, oczekiwany format wyjściowego tekstu będziemy podawać w parametrze paska adresu, w tablicy $_GET:
[code]
<?php
$format = 'html';
if(isset($_GET['format'])) $format = $_GET['format'];

$rendererFactory = new RendererFactory($format);
$renderer = $rendererFactory->load();

$renderer->setText('Hello World');
$renderer->render();
[/code]

Całość naszego kodu wygląda zatem następująco:
[code]
<?php

interface RendererInterface {

  public function setText($text); // metoda pobierająca tekst do wyświetlenia
  public function render(); // metoda renderująca tekst
}

// Tekst renderujący się w nagłówku <h1>
class HtmlRenderer implements RendererInterface {

  private $text;

  public function setText($text)
  {
    $this->text = $text;  
  }

  public function render()
  {
    echo '<h1>' . $this->text . '</h1>';  
  }
}

// Tekst renderujący się w formacie JSON
class JsonRenderer implements RendererInterface {

  private $text;

  public function setText($text)
  {
    $this->text = $text;  
  }

  public function render()
  {
    $ary = array('text' => $this->text);
    echo json_encode($ary);
  }
}

class RendererFactory {

  private $renderers = [];
  private $name;

  public function __construct($name) {

    $this->name = $name;
 
    // dodajemy nasze klasy renderujące do tablicy
    $this->registerRenderer('html', new HtmlRenderer);
    $this->registerRenderer('json', new JsonRenderer);
 
  }

  public function registerRenderer($name, RendererInterface $class)
  {
    $this->renderers[$name] = $class;
  }

  public function load()
  {
    return $this->renderers[$this->name];
  }
}

$format = 'html';
if(isset($_GET['format'])) $format = $_GET['format'];

$rendererFactory = new RendererFactory($format);
$renderer = $rendererFactory->load();

$renderer->setText('Hello World');
$renderer->render();
[/code]
Oczywiście jest to podane w jednym pliku .php, aby zobrazować wszystko jako całość - w rzeczywistości wszystko to podzielilibyśmy sobie na oddzielne pliki:

- RendererInterface.php
- RendererFactory.php
- HtmlRenderer.php
- JsonRenderer.php

które umieścilibyśmy w odpowiednich folderach odpowiadającym ich przestrzeniom nazw, a następnie załączylibyśmy do głównego skryptu za pomocą __autoload().
Sprawdźmy jednak nasz kod w praktyce:





Jak widzimy, wszystko działa jak należy i w zależności od wartości zmiennej $format do wyrenderowania naszego tekstu używana jest albo jedna albo druga instancja klasy oferowanej nam przez Fabrykę. Nic teraz nie stoi na przeszkodzie, aby rozbudować zestaw naszych "rendererów", o kolejne, tworzące np. na wyjściu plik XML, albo PDF. Wszystko sprowadzać się będzie do utworzenia jedynie nowej klasy i zarejestrowania jej w naszej Fabryce. Na tym właśnie polega magia tego wzorca projektowego i wykorzystania interfejsów. Zastosowań tego wzorca jest całe multum - wyobraźmy sobie wszelkie możliwe systemy pluginów, zapisu plików, dostępu do bazy, generowania różnych danych - dzięki takiemu wzorcowi możemy rozbudowywać naszą aplikację bez potrzeby ingerencji w nasz główny kod, gdyż tutaj nic nas już nie interesuje - podajemy jedynie dane wejściowe do instancji klasy zwracanej nam przez Fabrykę i następnie zwraca nam ona wynik wyjściowy. Oczywiście naszą Fabrykę można jeszcze mocno usprawnić, przepisać kod tak, aby zwracał nam instancję klasy bez potrzeby tworzenia obiektu Fabryki, np. za pomocą metody statycznej:

[code]Factory::load();[/code]
co można zrobić np. tak:
[code]<?php
class RendererFactory
{

  private static $renderers = [];

  public static function addRenderer($name, $className)
  {
    self::$renderers[$name] = $className;
  }

  public static function registerRenderers()
  {
    self::addRenderer('html', 'HtmlRenderer');
    self::addRenderer('json', 'JsonRenderer');
  }

  public static function getInstance(RendererInterface $class)
  {
    return $class;
  }

  public static function load($name)
  {
    self::registerRenderers();  // rejestrujemy klasy
    $renderer = self::$renderers[$name];
    return self::getInstance(new $renderer); // pobieramy instancję wybranej klasy
  }
}
$renderer = RendererFactory::load($format);
[/code]
lub tak:
[code]<?php
class RendererFactory
{
  public static function load($name)
  {
    $className = null;
    switch($name)
    {
      case 'html':
        $className = 'HtmlRenderer';
      break;
   
      case 'json':
        $className = 'JsonRenderer';
      break;
    }
 
    if($className !== null)
    {
      $instance = new $className;
      if($instance instanceof RendererInterface) return $instance;  
    }
  }
}
$renderer = RendererFactory::load($format);
[/code]

Przykłady powyżej stworzone zostały jedynie dla prostego zobrazowania zasady działania tego wszystkiego. Warto korzystać z interfejsów, gdyż nawet jeśli nie zamierzamy rozbudowywać naszego kodu w przyszłości, to dadzą one nam pewność, że np. inny programista zajmujący się naszym kodem nie będzie miał problemów z implementacją danych funkcji. Korzystajmy także z wzorca Factory wszędzie tam, gdzie istnieje możliwość rozbudowy czegoś o nowe funkcje w przyszłości - naprawdę oszczędzi nam to wielu godzin pracy związanej z ew. przyszłą przebudową wszystkich tych elementów kodu, które musielibyśmy zmieniać w przypadku nie skorzystania z takiego wzorca. No i to tyle na dziś, niebawem kolejny artykuł, w którym postaram się opisać kilka innych wzorców projektowych i pokazać przykłady ich zastosowania.

W PHP7 spopularyzowana została jeszcze jedna rzecz w tej materii, czyli tzw. traits, które po raz pierwszy zadebiutowały w PHP 5.4, ale o tym w oddzielnym artykule.


#AKTUALIZACJA - 20 styczeń 2017, 15:00

Użytkownik Krystian, jak i kilku innych użytkowników słusznie zwrócili uwagę na facebooku na kilka rzeczy, które zostały tutaj niedomówione, a które zostały właśnie uwzględnione w przeredagowanej wersji artykułu. Serdecznie dziękuję za wszelkie sugestie.

13 komentarzy:

  1. Mistrzowsko wytłumaczone!
    Chcę więcej czytać tak fajnie rozbudowanych i wytłumaczonych wpisów!

    OdpowiedzUsuń
  2. Super. Brakuje mi bardzo takich blogów. Życzę Ci dużo czasu i zapału na pisanie nowych artykułów. Pozdrawiam

    OdpowiedzUsuń
  3. It's interesting that many of the bloggers your tips helped to clarify a few things for me as well as giving.. very specific nice content. And tell people specific ways to live their lives.Sometimes you just have to yell at people and give them a good shake to get your point across.
    Mobile App Development Company
    Android app Development Company
    ios app development Company
    Mobile App Development Companies

    OdpowiedzUsuń
  4. Nice to read your article! very informative.So, please keep posting PHP Stuff here Thanks.......

    OdpowiedzUsuń
  5. aaaaa buahhhhhaaa

    OdpowiedzUsuń
  6. We Create This Blog Learn Php at home is for beginner Programmers and Developers that should be learn web development using php step by step and can do examples of php with easy ways to Explain.

    OdpowiedzUsuń
  7. zajebiste ! Nareszcie ktoś to opisał konkretnie i bez powielania tych samych co wszędzie definicji. Życzę Ci dużo zapału i kontynuuj to co robisz, a z pewnością zaczniesz na tym zarabiać, tylko ciut lepiej się wypozycjonuj w googlach bo na pierwszym miejscu są jakieś gówniane kursy php, które nic nowego nie wnoszą do tematu, nie to co Twój... :Pozdr

    OdpowiedzUsuń
  8. Australia Best Tutor is providing online Assignment Help Services for colleges and universities students. Here Students are getting best results and better grades.

    Discount Code : ABT30

    For More Information
    Online Assignment Help
    Assignment Help
    Instant Assignment Help
    My Assignment Help
    Help with Assignment
    Accounting Assignment Help

    OdpowiedzUsuń
  9. it's very great and informative post. thanks for sharing this post with us.
    PHP training in Chandigarh

    OdpowiedzUsuń
  10. I am really happy to say it’s an interesting post to read. I learn new information from your article, you are doing a great job.
    php training in IndoreKeep Posting:)

    OdpowiedzUsuń
  11. Thanks For Sharing. It IS very helpful For Everyone ....
    If You Are Looking Best PHP training in chandigarh click here

    OdpowiedzUsuń

Masz sugestię? Znalazłeś błąd? Napisz komentarz! :)

webmaester.pl - profesjonalne projektowanie WWW i webaplikacji
webmaester.pl - profesjonalne projektowanie WWW i webaplikacji