Blog

14 sierpnia 2015 Tomasz Paloc

Interface Segregation czyli Adaptery vs Dziedziczenie interfejsów

Interface Segregation czyli Adaptery vs Dziedziczenie interfejsów

Mam kolegę. W pracy siedzi obok mnie. Poczciwy chłop ale nie chciałem się tu przechwalać, że mam jakichś znajomych. Chodzi o to, że ten kolega ma książkę – Agile programowanie zwinne – Robert C. Martin, Micah Martin. Zwykle leży na jego biurku. Czasem “dla oderwania się” do niej zagląda. Ostatnio w oczekiwaniu na zakończenie czasochłonnej migracji danych postanowiłem zrobić to samo. Jako, że ostatnio w zespole na tapecie pojawił się temat Interface Segregation moją uwagę szybko zwrócił rozdział o tym pryncypium. Zostały tam opisane dwa podejścia. Pierwsze – z zastosowaniem adapterów, drugie – z wykorzystaniem dziedziczenia interfejsów. Pierwsze z nich jest w naszym zespole stosowane dość często. Natomiast zaciekawiło mnie to drugie, które jakiś czas temu stało się przedmiotem szerszej dyskusji. Najpierw pokrótce przybliżę temat ISP, i jako że w naszej firmie jedną z wiodących technologii jest PHP, pozwolę sobie spojrzeć na to zagadnienie z perspektywy tego języka.

Interface Segregation Principle

Zasada segregacji interfejsów mówi o tym, że klasa powinna udostępniać tylko niezbędny interfejs zdeterminowany wymaganiami jej klienta. Czy też innymi słowy – klient nie powinien mieć dostępu do metod, z których nie korzysta. Dlaczego? Dlatego, że w ten sposób uzależniamy klienta od metod, które są mu zbędne.

Załóżmy, że w naszym leciwym już systemie znajdujemy klasę reprezentującą fakturę. Klasa ta ma dwie klasy klienckie: serwis systemu do generatora plików PDF oraz serwis zajmujący się przygotowaniem koperty dla faktury (jeśli ma zostać wysłana tradycyjną pocztą).

class Invoice {
    public function getShippingPayerAddress() {...}
    public function getPayerAddress() {...}
    public function getItems() { … }
    public function getBankAccountNumber() {...}
    public function getTotalPrice() {…}
    public function getIssueDate() { … }
}

class EnvelopeGeneratorService {
    public function generate(Invoice $invoice) {
        $payerAddres = $invoice->getShippingPayerAddress();
    }
//some code
}

class PdfGeneratorService {
    public function generate(Invoice $invoice) {
        $payerAddres = $invoice->getPayerAddress();
        $bankAccountNo = $this->getBankAccountNumber();
    }
//some code
}

W powyższym przykładzie oba serwisy mają dostęp do wszystkich metod obiektu klasy Invoice. Co za tym idzie w przypadku konieczności wprowadzenia zmiany w metodzie getShippingPayerAddress wymuszonej zmianami w EnvelopeGeneratorService może okazać się, że ma ona wpływ na PdfGeneratorService, a przecież jest to odrębny i teoretycznie niezależny serwis.

Problem można rozwiązać stosując:

Adaptery!

więc definiujemy dwa interfejsy zdeterminowane tym czego wymagają klienci. Czyli przykładowo

interface  EnvelopeGenerated {
    public function getShippingPayerAddress();
}

interface  PdfGenerated {
    public function getPayerAddress();
    public function getBankAccountNumber();
}

Następnie tworzymy dwa adaptery, które mógłby wyglądać mniej więcej tak:

class InvoiceEnvelopeAdapter implements EnvelopeGenerated {
    
    private $invoice;

    public function __costruct(Invoice $invoice) {
        $this->invoice = $invoice;
    }

    public function getShippingPayerAddress() {
        return $this->invoice->getShippingPayerAddress();
    }
}

class InvoicePdfAdapter implements PdfGenerated {

    private $invoice;
    
    public function __costruct(Invoice $invoice) {
        $this->invoice = $invoice;
    }

    public function getPayerAddress() {
        return $this->invoice->getPayerAddress();
    }

    public function getBankAccountNumber() {
        return $this->invoice->getBankAccountNumber();
    }
}

natomiast klasy klienckie należałoby odpowiednio zmienić na:

class EnvelopeGeneratorService {
    public function generate(EnvelopeGenerated $envelope) {
        $payerAddres = $envelope->getShippingPayerAddress();
    }
    
    //some code
}

class PdfGeneratorService {
    public function generate(PdfGenerated $pdf) {
        $payerAddres = $pdf->getPayerAddress();
        $bankAccountNo = $pdf->getBankAccountNumber();
    }

    //some code
}

 

Teraz do klas serwisów przekazywany jest nie obiekt faktury lecz adapter implementujący interfejs danego serwisu. Dzięki temu każdy z serwisów ma dostęp tylko do tych metod które są dla niego niezbędne.

lub

Dziedziczenie interfejsów!

Zgodnie z tym co proponowane jest we wspomnianej wcześniej książce należy stworzyć nowy interfejs, który z wykorzystaniem wielodziedziczenia będzie dziedziczył po EnvelopeGenerated i PdfGeneratored . Nowopowstały interfejs byłby implementowany przez klasę Invoice… i już napotkaliśmy pierwszy problem. W PHP nie ma wielodziedziczenia…

Myślę, że możemy w klasie Invoice zadeklarować implementację dwóch interfejsów i osiągniemy zbliżony efekt. W serwisach powinniśmy oczekiwać konkretnego interfejsu. Kod mógłby wyglądać następująco

class Invoice implements EnvelopeGenerated, PdfGenerated {
...
}

class EnvelopeGeneratorService {
    public function generate(EnvelopeGenerated  $document) {
        $payerAddres =  $document->getShippingPayerAddress();
    }

    //some code
}

class PdfGeneratorService {
    public function generate(PdfGenerated $document) {
        $payerAddres =  $document->getPayerAddress();
        $bankAccountNo = $pdf->getBankAccountNumber();
    }
    
    //some code
}

 

A co w sytuacji jeśli…

…dochodzi nam kolejny klient, który wymaga kilku metod z interfejsu EnvelopeGenerated i z PdfGenerated?

Najlepiej jest dopisać nowy interfejs/adapter zawierający potrzebny zestaw metod. Próby wprowadzenia dziedziczenia pomiedzy interfejsami/adapterami mogą nie przynieść zadowalających efektów w długofalowym rozwoju aplikacji.

…w systemie istnieje już klasa, która całkowicie spełnia interfejs nowopowstałego klienta?

Tutaj powinniśmy stosować niezmiennie to samo podejście. Powinien powstać nowy interfejs/adapter i klient powinien zostać uzależniony od abstrakcji. Pamiętajmy, że kod nieustannie się rozwija. Dodanie nowej metody w “starej” klasie automatycznie złamałoby ISP względem naszego świeżo napisanego klienta.

Co wybrać?

Zdaniem autorów Agile programowanie zwinne lepszym rozwiązaniem jest dziedziczenie interfejsów. Jako argument przytaczają oni fakt, że nie potrzeba tworzyć dodatkowych klas adapterów, a następnie ich obiektów które przecież zżerają jakieś dodatkowe zasoby serwera. Jako przykład zostały podane systemy czasu rzeczywistego, w których optymalizacja ma kluczowe znaczenie. Tak trudno jest się z tym nie zgodzić, jak trudno jest sobie wyobrazić system czasu rzeczywistego napisany w PHP. No właśnie…

Myślę, że programista pracujący na codzień nad aplikacją korzystającą z jakiegoś frameworka MVC do tego ORM i np. biblioteki do parsowania plików XLS, nie powinien rozważać, czy tworzyć kilka dodatkowych klas Adapterów. Trzeba zadać sobie pytanie czy stworzenie tych klas przyniesie jakąś korzyść dla kodu aplikacji. Dziedziczenie interfejsów zrealizowane w pokazanej powyżej formie ma wadę: w miarę jak będzie przybywało klientów, będzie rosła lista interfejsów po których dziedziczy klasa Invoice. Już przy kilku interfejsach czytelność takiego kodu budzi wątpliwości. Idąc dalej dodawanie nowych interfejsów czy rozbudowa istniejących o nowe metody będzie prowadziła do powstania przerośniętej klasy o liczbie metod dorównującej sumie kilometrów wybieganych przez uczestników biegu podróżnika.

Dodatkowo adapter daje nam możliwość przetłumaczenia interfejsu co często jest niezbędne przy pracy z systemami Legacy.

W wyborze podejścia powinniśmy decydować w zależności od problemu, ale myślę, że w większości przypadków głównym kryterium powinna być czytelność kodu, a nie różnica w ilości zajętych zasobów.
Jakie jest Wasze zdanie? Może dostrzegacie jakieś inne wady i zalety obu podejść, czy też znacie inne sposoby radzenia sobie z ISP?

Zobacz na blogu

09.09.2022
Marcin Jahn
It’s Not Just HTTP It’s Not Just HTTP

In today’s world of cloud-based solutions, distributed systems, and microservices-based architectures, network communication is a...

23.08.2022
Adam Mrowiec
Konferencja IPC 2022 Berlin Konferencja IPC 2022 Berlin

Pandemia wreszcie się kończy, dlatego w tym roku postanowiliśmy wrócić do naszych wyjazdów na konferencje....