Jak programować obiektowo? cz. 8 – interfejsy
|
O interfejsach miałem już przyjemność pisać wielokrotnie i zapewne jeszcze niejednokrotnie będę ten temat poruszał. Wynika to z tego, że interfejsy są chyba najważniejszą strukturą w programowaniu obiektowym, a ich poprawne wykorzystanie stanowi o jego jakości, o tym czy łatwo i przyjemnie będzie się z nim pracowało oraz o tym, czy jego rozwój będzie możliwy bez konieczności ogromnych oaz kosztownych refaktoryzacji.
Niemniej jednak, można spotkać aplikacje, gdzie kod jest przesycony interfejsami i choć rzeczywiście jest bardzo elastyczny to równocześnie nadmiarowy i w niektórych miejscach interfejsy są zbędne. Dostajemy kod, który z powodzeniem mógłby istnieć bez licznych zależności i nadal robić to, co się od niego oczekuje. W dodatku taki odchudzony kod jest z pewnością czytelniejszy.
Interfejsy to potężna broń, której każdy programista musi nauczyć się używać, aby tworzyć jak najlepsze projekty. Minimalizować zależności, nie ograniczając przy tym niezbędnej elastyczności.
Declarations are all you need
Jak to możliwe, że struktura, której ideą są jedynie deklaracje metod może być tak istotna? Żadnej funkcjonalności, żadnego kodu, który wykonuje jakieś operacje? Jedynie obietnice, że jakaś klasa dostarczy implementację, która wykona to, co interfejs obiecuje. Ot, taka umowa. Kontrakt. Tak niewiele, ale właśnie w tym kontrakcie, tej obietnicy tkwi sens interfejsów. Właśnie tego typu obietnice, gdy są odpowiednio przemyślane i zawsze spełnione, decydują o jakości aplikacji.
Piękno interfejsów, a właściwie ich użyteczność możemy jednak docenić dopiero, gdy projektujemy pewną funkcjonalność, od której oczekujemy choćby minimalnej elastyczności. Jeżeli klasy mają poprawnie określone odpowiedzialności, a nasz kod oprzemy na asocjacjach, to okaże się, że rozwój aplikacji, dodawanie kolejnych funkcjonalności czy też modyfikacja obecnych przestaje być zadaniem ponad miarę zwykłego śmiertelnika. Co więcej, kod po takich zmianach nie jest zlepkiem if’ów oraz innych bloków warunkowych. Jest nadal spójną całością. I, co równie ważne, czytelną całością.
Choć struktura interfejsów jest oparta jedynie na deklaracjach publicznych metod, umiejętności ich poprawnego wykorzystania nie nabędziecie po przeczytaniu jednego mądrego artykułu/tutoriala/książki. Nie chcę powtarzać frazesów, że praktyka czyni mistrza, ale naprawdę sporo kodu trzeba zaprojektować i napisać, z wieloma problemami wynikającymi z błędów projektowych trzeba się zmierzyć, żeby interfejsy naprawdę dobrze poczuć. Niestety nauka na projektach, które po chwili wylądują w koszu lub zostaną zapomniane mija się z celem, ponieważ dopiero, gdy aplikacja zaczyna żyć, rozrastać się, powiększać, dopiero wtedy macie okazję doświadczyć potęgi poprawnie wykorzystanych interfejsów. Tego, ilu godzin refaktoryzacji i czytania kodu dzięki nim oszczędzacie.
Dlatego też, warto zacząć już dzisiaj 🙂
Dlaczego PUBLIC? I w dodatku ABSTRACT?
Każda metoda, której deklarację umieścimy w interfejsie ma być publiczna. W dodatku, skoro mówimy o deklaracjach to przekłada się bezpośrednio na brak jakiegokolwiek ciała metody, a co za tym idzie – te metody są abstrakcyjne. Oczywiście nic nie dzieje się przypadkiem i również tych decyzji jest uzasadnienie.
Jak już pisałem wyżej, interfejsy gwarantują kontrakt, na którym może się opierać klasa, której obiekty są od niego zależne (takie, które wchodzą z obiektami implementującymi interfejs w jakąś interakcję). Skoro mowa o interakcji dwóch obiektów, to aby była ona możliwa deklaracje metod muszą być publiczne.
A dlaczego metody są abstrakcyjne? Interfejs mówi o tym CO ma się stać, nie ma natomiast żadnego wpływu na to JAK ma dana operacja zostać wykonana. Daje programiście dowolność. Interfejs jest gwarantem tego, że operacje zostaną wykonane, że zostanie zrobione to, co interfejs obiecał, że zostanie zrobione. W innym wypadku (np. implementacja metody sort() usuwająca wszystkie elementy) nie możemy oczekiwać poprawnych rezultatów, ponieważ kontrakt został złamany.
Trochę kodu, aby pokryć nowe wymagania
To najwyższy chyba czas, aby wrócić do naszego przykładu. Jakiś czas temu tworzyliśmy kod niezbędny do wysyłania faktur. Klient jednak wpadł na pomysł, że faktury to nie jedyna rzecz, którą potrzebuje przesyłać do klienta. Są jeszcze faktury proforma, korekty do faktur oraz oferty. Bez obsługi tych dokumentów jego praca będzie utrudniona, a nasza aplikacja nie ułatwi mu życia w takim stopniu w jakim by chciał zarówno nasz klient jak i my.
Mamy więc kilka typów dokumentów:
class Invoice { /** code */ }
class Proform { /** code */ }
class Correction { /** code */ }
class Offer { /** code */ }
Wszystkie te dokumenty musimy mieć możliwość przesyłać, więc może nam powstać coś takiego:
class Sender {
public function sendInvoice(Invoice $invoice) { /** code */ }
public function sendProforma(Proforma $proforma) { /** code */ }
public function sendCorrection(Correction $correction) { /** code */ }
public function sendOffer(Offer $offer) { /** code */ }
}
I teraz stajemy przed zadaniem zaimplementowania tych wymagań. Co musimy zrobić? Przed wysłaniem dobrze byłoby jeszcze zamienić nasze obiekty domenowe na jakiś dokument, który będzie można załączyć do wysyłanej wiadomości.
Ok, po wylistowaniu tego, co trzeba zrobić, aby wysyłanie czegokolwiek miało jakikolwiek sens nasz kod przyjmie odrobinę inną formę:
class Sender {
public function sendDocument(PdfDocument $document) { /** code */ }
}
class PdfDocumentConverter {
public function convertFromInvoice(Invoice $invoice) { /** code */ }
public function convertFromProforma(Proforma $proforma) { /** code */ }
public function convertFromCorrection(Correction $correction) { /** code */ }
public function convertFromOffer(Offer $offer) { /** code */ }
}
gdzie każda metoda klasy PdfDocumentConverter
zwraca nam obiekt klasy PdfDocument
.
Idźmy dalej. Do zbudowania poprawnego dokumentu potrzebujemy informacji dotyczących sprzedającego, kupującego oraz listy pozycji, które są obiektem zainteresowania. Tak więc, implementacja metody convertFromInvoice()
mogła by wyglądać następująco:
public function convertFromInvoice(Invoice $invoice) {
$pdfDocument = new PdfDocument();
$this->attachHeader($pdfDocument);
$this->attachSellerInformation($pdfDocument, $invoice->getSellerData());
$this->attachBuyerInformation($pdfDocument, $invoice->getBuyerData());
$this->attachItemsInformation($pdfDocument, $invoice->getItemsData());
$this->attachSummaryInformation($pdfDocument, $invoice->getSummaryData());
$this->attachlFooter($pdfDocument);
}
I taka mała uwaga na tym etapie. W rzeczywistej aplikacji radziłbym zamiast obiektów domenowych jako parametrów używać obiektów DTO. Kod jest czystszy, a obiekty nie mają tylu zależności i nie są obarczone tak dużą odpowiedzialnością.
Wracając do wątku głównego, teraz przyszedł czas na pewne spostrzeżenie dotyczące tego, jak będą wyglądały ciała kolejnych metod. Oczywiście, poza parametrem wejściowym, nie będą się absolutnie niczym różniły. W każdym przypadku potrzebujemy zbudować podobny dokument z podobnych danych.
Reasumując, potrzebujemy, aby wykonanie metod na każdym typie dokumentu zwracało nam oczekiwany rezultat, jednak nie jesteśmy zainteresowani konkretną implementacją, a logiczną spójnością. Najlepiej obrazuje nam to metoda getSummaryData()
, która za każdym razem zwraca informacje podsumowujące transakcję, której dokument dotyczy, ale za każdym razem te informacje są różne np. informacja o ofercie (Oferta), cena końcowa (Faktura), itp.
W takim wypadku może warto skorzystać z interfejsu?
class PdfDocumentConverter {
public function convertFromInvoice(DocumentDTO $documentDto) { /** code */ }
}
interface DocumentDTO {
public function getSellerData();
public function getBuyerData();
public function getItemsData();
public function getSummaryData();
}
class InvoiceDTO implements DocumentDTO { /** code */ }
class ProformaDTO implements DocumentDTO { /** code */ }
class OfferDTO implements DocumentDTO { /** code */ }
class CorrectionDTO implements DocumentDTO { /** code */ }
Plusem takiego rozwiązania jest to, że w chwili gdy dochodzi nam jakikolwiek nowy typ dokumentu to zarówno dla funkcjonalności wysyłającej jak i tej tworzącej PDF’y będzie to transparentne, a dodanie takiego kawałka kodu będzie bardzo proste.
Jeszcze na koniec propozycja ćwiczenia. Wyobraźcie sobie, że nasz klient chce wysyłać dokumenty nie tylko w formacie PDF, ale również docx oraz txt. Zastanówcie się jak ugryźć ten problem z wykorzystaniem interfejsów. Miłej zabawy 🙂
To dopiero początek
Interfejsy to temat, o którym można napisać bardzo wiele. Jest to jeden z bezpośrednich dowodów na to, jak potężne są. Niestety krótki artykuł wystarczy, aby dotknąć jedynie podstaw. Jeżeli jednak jesteście głodni dalszych informacji to zapraszam do zapoznania się z innymi artykułami dotyczącymi Interfejsów.
Pozostałe artykuły z cyklu
- Jak programować obiektowo? cz. 9 – klasy abstrakcyjne
- Jak programować obiektowo? cz. 8 – interfejsy
- Jak programować obiektowo? cz. 7 – final
- Jak programować obiektowo? cz. 6 – wartości stałe
- Jak programować obiektowo? cz. 5 – ach ten static…
- Jak programować obiektowo? cz. 4 – metod ciąg dalszy
- Jak programować obiektowo? cz. 3 – czym jest metoda?
- Jak programować obiektowo? cz. 2 – atrybuty klasy
- Jak programować obiektowo? cz. 1 – wstęp