5.05.20226 min

Ash AllenWeb Developer

Twórz lepszy kod w PHP wykorzystując interfejsy

Sprawdź, czym są interfejsy, jak ich używać w PHP i Laravelu oraz jak je wykorzystać, by ulepszyły Twój kod.

Twórz lepszy kod w PHP wykorzystując interfejsy

W programowaniu ważne jest, aby kod był czytelny, łatwy w utrzymaniu, rozszerzalny i nadający się do testowania. Jednym ze sposobów na ulepszenie wszystkich tych czynników w naszym kodzie jest zastosowanie interfejsów.


Docelowi odbiorcy

Ten artykuł jest skierowany do programistów, którzy mają podstawową wiedzę z programowania obiektowego. Jeśli wiecie, jak używać dziedziczenia w Waszym kodzie PHP, to na pewno będziecie wiedzieć, o co chodzi.


Czym są interfejsy?

Interfejsy są opisami tego, co dana klasa powinna posiadać. Można ich użyć, aby upewnić się, że dowolna klasa implementująca interfejs będzie zawierała każdą metodę publiczną, która jest w nim zdefiniowana.


Interfejsy mogą być:

  • używane do definiowania metod publicznych dla danej klasy.
  • używane do definiowania stałych dla klasy.


Interfejsy nie mogą:

  • Być samodzielnie instancjowane.
  • Być używane do definiowania prywatnych lub chronionych metod klasy.
  • Definiować właściwości klasy.


Interfejsy są wykorzystywane do definiowania metod publicznych, które klasa powinna zawierać. Należy pamiętać, że definiowane są tylko sygnatury metod, a nie ich treść (jak to zwykle bywa w przypadku metod klasowych).

Dzieje się tak dlatego, że interfejsy są używane tylko do określania komunikacji między obiektami, a nie do definiowania komunikacji i zachowania, jak w klasie. Aby nadać temu nieco kontekstu, w tym przykładzie przedstawiono przykładowy interfejs, który definiuje kilka metod publicznych:

interface DownloadableReport
{
    public function getName(): string;

    public function getHeaders(): array;

    public function getData(): array;
}


Według php.net interfejsy pełnią dwa główne zadania:

  1. Umożliwiają programistom tworzenie obiektów różnych klas, które mogą być stosowane zamiennie, ponieważ implementują ten sam interfejs lub interfejsy. Przykładem mogą być usługi dostępu do wielu baz danych, bramki płatnicze lub różne strategie cache’owania. Poszczególne implementacje mogą być wymieniane bez konieczności wprowadzania jakichkolwiek zmian w kodzie, który z nich korzysta.
  2. Umożliwiają funkcji lub metodzie przyjmowanie i operowanie na parametrze zgodnym z interfejsem, nie dbając o to, co jeszcze obiekt może robić lub jak jest zaimplementowany. Interfejsy te są często nazywane Iterable, Cacheable, Renderable, itd. w celu opisania istoty tego zachowania.


Używanie interfejsów w PHP

Interfejsy mogą być nieocenioną częścią bazy kodu programowania obiektowego. Pozwalają one na odłączenie naszego kodu i zwiększenie możliwości jego rozbudowy. Aby to zobrazować, spójrzmy na poniższą klasę:

class BlogReport
{
    public function getName(): string
    {
        return 'Blog report';
    }
}


Jak widać, zdefiniowaliśmy klasę z metodą, która zwraca ciąg znaków. W ten sposób określamy zachowanie metody, dzięki czemu możemy zobaczyć, jak funkcja getName() buduje zwracany ciąg znaków.

Załóżmy jednak, że wywołamy tę metodę w naszym kodzie wewnątrz innej klasy. Ta inna klasa nie będzie dbała o to, w jaki sposób został zbudowany ciąg znaków, tylko o to, że został on zwrócony. Spróbujmy więc wywołać tę metodę w innej klasie:

class ReportDownloadService
{
    public function downloadPDF(BlogReport $report)
    {
        $name = $report->getName();

        // Download the file here...
    }
}


Chociaż powyższy kod działa, wyobraźmy sobie, że chcielibyśmy teraz dodać funkcję pobierania raportu użytkownika, który jest zawinięty wewnątrz klasy UsersReport.

Oczywiście nie możemy użyć istniejącej metody w ReportDownloadService, ponieważ narzuciliśmy, że można przekazać tylko klasę BlogReport. Będziemy więc musieli zmienić nazwę istniejącej metody, a następnie dodać nową metodę tak jak poniżej:

class ReportDownloadService
{
    public function downloadBlogReportPDF(BlogReport $report)
    {
        $name = $report->getName();

        // Download the file here...
    }

    public function downloadUsersReportPDF(UsersReport $report)
    {
        $name = $report->getName();

        // Download the file here...
    }
}


Chociaż tego nie widać, załóżmy, że pozostałe metody w powyższej klasie używają identycznego kodu do zbudowania pliku do pobrania. Moglibyśmy przenieść współdzielony kod do metod, ale prawdopodobnie nadal pozostanie tam trochę współdzielonego kodu.

Oprócz tego będziemy mieli wiele punktów wejścia do klasy, w których działa niemal identyczny kod. Może to potencjalnie prowadzić do dodatkowej pracy w przyszłości przy próbach rozszerzania kodu lub przeprowadzaniu testów.

Na przykład, wyobraźmy sobie, że tworzymy nowy raport AnalyticsReport; musielibyśmy teraz dodać do tej klasy nową metodę downloadAnalyticsReportPDF(). Zapewne widzicie, jak ten plik mógłby szybko zacząć się rozrastać. I to może być doskonałe miejsce na użycie interfejsu!

Zacznijmy od stworzenia takiego; nazwiemy go DownloadableReport i zdefiniujemy w ten sposób:

interface DownloadableReport
{
    public function getName(): string;

    public function getHeaders(): array;

    public function getData(): array;
}


Możemy teraz zaktualizować BlogReport i UsersReport, aby zaimplementować interfejs DownloadableReport, tak jak to widać na poniższym przykładzie. Zauważ jednak, że celowo napisałem kod dla UsersReport źle, tak aby móc coś zademonstrować!

class BlogReport implements DownloadableReport
{
    public function getName(): string
    {
        return 'Blog report';
    }

    public function getHeaders(): array
    {
        return ['The headers go here'];
    }

    public function getData(): array
    {
        return ['The data for the report is here.'];
    }
}

 

class UsersReport implements DownloadableReport
{
    public function getName()
    {
        return ['Users Report'];
    }

    public function getData(): string
    {
        return 'The data for the report is here.';
    }
}


Jeśli spróbujemy uruchomić nasz kod, otrzymamy błędy z następujących powodów:

  1. Brakuje metody getHeaders().
  2. Metoda getName() nie zawiera typu zwracanego, który jest zdefiniowany w sygnaturze metody interfejsu.
  3. Metoda getData() definiuje typ zwrotu, ale nie jest on taki sam, jak typ zwrotu zdefiniowany w sygnaturze metody interfejsu.


Zatem, aby zaktualizować raport UsersReport tak, aby poprawnie implementował interfejs DownloadableReport, moglibyśmy zmienić go na następujący:

class UsersReport implements DownloadableReport
{
    public function getName(): string
    {
        return 'Users Report';
    }

    public function getHeaders(): array
    {
       return [];
    }

    public function getData(): array
    {
        return ['The data for the report is here.'];
    }
}


Teraz gdy obie klasy raportów implementują ten sam interfejs, możemy zaktualizować nasz ReportDownloadService w następujący sposób:

class ReportDownloadService
{
    public function downloadReportPDF(DownloadableReport $report)
    {
        $name = $report->getName();

        // Download the file here...
    }

}


Możemy teraz bez żadnych błędów przekazać obiekt UsersReport lub BlogReport do metody downloadReportPDF(). Robimy to dlatego, że wiemy już, iż niezbędne metody potrzebne w klasach raportów istnieją i zwracają dane w oczekiwanym przez nas typie.

W wyniku przekazania interfejsu do metody, a nie klasy, umożliwione zostało luźne powiązanie ReportDownloadService i klas raportów na podstawie tego, co metody robią, a nie jak to robią.

Gdybyśmy chcieli stworzyć nowy AnalyticsReport, moglibyśmy zaimplementować ten sam interfejs, co pozwoliłoby nam przekazać obiekt raportu do tej samej metody downloadReportPDF() bez konieczności dodawania nowych metod.

Może to być szczególnie przydatne, jeśli budujecie własny pakiet lub framework i chcecie dać programiście możliwość tworzenia własnych klas. Możesz mu po prostu powiedzieć, który interfejs ma zaimplementować, a wtedy może on stworzyć swoją własną klasę.


Porównanie do Laravela

Przykładowo, w Laravel istnieje możliwość stworzenia własnej klasy sterownika pamięci podręcznej, implementując interfejs Illuminate\Contracts\Cache\Store.

Chociaż Interfejsów używam głównie do ulepszania kodu, to jestem też ich fanem dlatego, że działają one jak kod-dokumentacja. Na przykład, jeśli próbuję dowiedzieć się, co dana klasa może, a czego nie może robić, zazwyczaj najpierw sprawdzam interfejs, a dopiero potem klasę, która z niego korzysta. Interfejs informuje o wszystkich metodach, które mają być wywołane, bez konieczności zwracania szczególnej uwagi na to, jak te metody działają pod maską.

Ci z was, którzy korzystają  z Laravela, mogą zauważyć, że terminy „kontrakt” i „interfejs” są używane zamiennie. Według dokumentacji Laravela jego kontrakty są „zestawem interfejsów, które definiują podstawowe usługi dostarczane przez framework”.

Należy więc pamiętać, że kontrakt jest interfejsem, ale interfejs niekoniecznie jest kontraktem. Zazwyczaj kontrakt jest po prostu interfejsem, który jest dostarczany przez framework. Aby uzyskać więcej informacji na temat korzystania z kontraktów, zalecam zapoznanie się z tą dokumentacją, ponieważ moim zdaniem dobrze wyjaśnia ona, czym są kontrakty, jak ich używać i kiedy należy z nich korzystać.


Wnioski

Mam nadzieję, że po przeczytaniu tego artykułu dowiedzieliście się, czym są interfejsy, jak można ich używać w PHP oraz jakie są korzyści z ich stosowania.

Chętnie usłyszę w komentarzach, czy ten artykuł pomógł wam w zrozumieniu interfejsów. Nie przestawajcie tworzyć niesamowitych rzeczy! 


Oryginał tekstu w języku angielskim przeczytasz tutaj.

<p>Loading...</p>