Interface segregation principle to kolejna zasada, którą sformułował Robert C. Martin w ramach zbioru zasad SOLID. Można ją przetłumaczyć jako Zasada segregacji interfejsów i jej definicja mówi o tym że:

Wiele dedykowanych interfejsów jest lepsze niż jeden ogólny.

Co dokładnie znaczy ta formuła? Chodzi o to, aby przy modelowaniu interfejsów skupiać się na tym, aby były one wyspecjalizowane w jednym kierunku, a nie zawierały zbioru metod koniecznych do zaimplementowania. Nie powinniśmy stosować rozbudowanych interfejsów z wieloma metodami, a żadna klasa nie może być zmuszana do tego, aby implementować metodę której nie używa i nie potrzebuje. Jest to stosunkowo prosta zasada do zrozumienia, ale jej zastosowanie jest równie ważne jak pozostałe zasady SOLID. Stosowanie się do niej pozwala nam utrzymywać porządek w naszym kodzie, unikać rozbudowanych interfejsów oraz zachowań klas, które muszą coś implementować na siłę. Stosując wyspecjalizowane interfejsy, jesteśmy w stanie również dobrać lepsze ich nazewnictwo na podstawie tego za co są odpowiedzialne, co sprawia, że nasz kod jest dużo bardziej czytelny.

Przykład użycia

Jako przykład wyobraźmy sobie, że za pomocą klas chcemy zaimplementować różnego rodzaju pojazdy oraz zachowania jakie te pojazdy mogą generować. Pojazdy mogę jeździć, latać lub pływać — zacznijmy więc od implementacji interfejsu Vehicle:

interface Vehicle {
    public function drive(): void;
    public function swim(): void;
    public function fly(): void;
}

Następnie przejdźmy do implementacji konkretnych pojazdów. W tym przykładzie będą to trzy klasy Car, Ship i Airplane:

class Car implements Vehicle
{
    private bool $isDriving = false;

    public function drive(): void {
        $this->isDriving = true;
    }

    public function swim(): void {
        throw new Exception("Car can't swim");
    }

    public function fly(): void {
        throw new Exception("Car can't fly");
    }
}

class Ship implements Vehicle
{
    private bool $isSwiming = false;

    public function drive(): void {
        throw new Exception("Ship can't drive");
    }

    public function swim(): void {
        $this->isSwiming = true;
    }

    public function fly(): void {
        throw new Exception("Ship can't fly");
    }
}

class Airplane implements Vehicle
{
    private bool $isFlying = false;
    private bool $isDriving = false;



    public function drive(): void {
        $this->isDriving = true;
        $this->isFlying = false;
    }

    public function swim(): void {
        throw new Exception("Airplane can't fly");
    }

    public function fly(): void {
        $this->isDriving = false;
        $this->isFlying = true;
    }
}

Już na pierwszy rzut oka widać jaki problem sprawia taka budowa interfejsu Vehicle, który wymaga implementacji metod drive(), swim() oraz fly(). Jak wiadomo, samochód potrafi jedynie jeździć, dlatego w metodach swim() i fly() rzuca wyjątek z odpowiednim komunikatem, tak samo działa klasa Ship, ponieważ statek potrafi jedynie pływać i dla pozostałych metod również musi rzucić wyjątek. W przypadku samolotu już dwie metody są zasadne, ponieważ samolot potrafi zarówno jechać na podwoziu, jak i latać w powietrzu, ale w przypadku metody swim() musi być wyrzucony wyjątek. Taki kod jest niezgodny z zasadą Interface Segregation Principle, ponieważ interfejs Vehicle nie jest wyspecjalizowany i wymusza implementację metod, których nie potrzebują wszystkie klasy. Interfejs ten jest zbyt ogólny i zawiera zbyt wiele możliwości implementacji.

Jak w takim razie możemy zmienić kod tak, aby spełniał zasadę Interface Segregation Principle, a jednoczenieście umożliwiał taką implementację, aby samochód mógł jeździć, statek pływać, a samolot jeździć i latać?

Należy każdą tę czynność wydzielić do osobnego interfejsu, dzięki temu implementacja będzie mogła wybrać, który interfejs implementuje na podstawie tego, jakich czynności potrzebuje.

Stwórzmy w takim razie trzy odpowiednie interfejsy — Driveable, Swimable oraz Flyable:

interface Driveable {
    public function drive(): void;
}

interface Swimable {
    public function swim(): void;
}

interface Flyable {
    public function fly(): void;
}

Tak zaprojektowane interfejsy są wyspecjalizowane oraz posiadają nazewnictwo, na podstawie którego jesteśmy w stanie ustalić za co konkretnie są odpowiedzialne. Mamy poprawnie uporządkowane interfejsy i teraz wystarczy, aby każdy pojazd zaimplementował tylko to, czego dokładnie potrzebuje. Spójrzmy zatem na poprawioną implementację klas Car, Ship oraz Airplane:

class Car implements Driveable
{
    private bool $isDriving = false;

    public function drive(): void {
        $this->isDriving = true;
    }
}

class Ship implements Swimable
{
    private bool $isSwiming = false;

    public function swim(): void {
        $this->isSwiming = true;
    }
}

class Airplane implements Driveable, Flyable
{
    private bool $isFlying = false;
    private bool $isDriving = false;

    public function drive(): void {
        $this->isDriving = true;
        $this->isFlying = false;
    }

    public function fly(): void {
        $this->isDriving = false;
        $this->isFlying = true;
    }
}

Dzięki zastosowaniu Interface Segration Principle nasz kod jest dużo krótszy i prostszy. Nie musimy wyrzucać nadmiarowych wyjątków oraz implementujemy tyko te metody, które są niezbędne z punktu widzenia konkretnej klasy. Każdy pojazd implementuje taki interfejs zgodnie z tym, jaką czynność potrafi wykonać. Dzięki temu interfejs ten opisuje dany pojazd i jego możliwości.

Podsumowanie

Interface Segregation Principle to jedna z prostszych zasad SOLID, aczkolwiek równie ważna w zastosowaniu co pozostałe. Daje ona szereg korzyści, takich jak: porządek w kodzie, brak nie obsłużonych metod, czy wyspecjalizowane abstrakcje. Pamiętajmy o tym, że przeważnie modelujemy nasz kod od ogółu do szczegółu, czyli na pierwszy rzut projektujemy abstrakcje i interfejsy. Dlatego też ta zasada jest na tyle ważna, gdy zaczynamy projektować nasz system — to właśnie głównie na interfejsach będzie się on opierał. Posiadając zwięzłe interfejsy zapewniamy naszemu systemowi elastyczność oraz nie tworzymy niepotrzebnych barier i przeszkód, które sprawiają „tłuste” interfejsy. Za pomocą krótkich i zwięzłych interfejsów możemy opisywać obiekty i ich zachowania, a czytanie kodu staje się dużo bardziej intuicyjne.

Autorem tekstu jest Damian Kusek