Jak wspomniałem we wcześniejszych artykułach, zasady SOLID nie występują jako osobne byty. Bardzo często kod zgodny z jedną z nich „niechcący” spełnia także inną. Analogicznie – zły kod często łamie kilka tych zasad naraz.

Dziś przyjrzymy się zasadzie ISP (interface segregation principle), która mówi, że klasa nie powinna być zmuszana do implementacji metod, których nie potrzebuje.

Jako przykład, rozpatrzymy sobie system ewidencji sprzętu komputerowego w biurowcu, mianowicie skupimy się na drukarkach, które implementują interfejs PrinterInterface:

interface PrinterInterface
{
    public function print(string $doc): void;
    public function scan(string $doc): void;
    public function fax(string $doc): void;
    public function staple(string $doc): void;
}

Powyższy kontrakt określa funkcje, jakie musi posiadać drukarka (wydruk, skan, fax oraz zszywanie). Weźmy zatem urządzenie wielofunkcyjne:

class MultifunctionDevice implements PrinterInterface
{
    public function print(string $doc): void  { echo "Drukuję: $doc\n";  }
    public function scan(string $doc): void   { echo "Skanuję: $doc\n";  }
    public function fax(string $doc): void    { echo "Faksuję: $doc\n";   }
    public function staple(string $doc): void { echo "Zszywam: $doc\n";   }
}

Jak widzimy, nasze urządzenie bez trudu wykonuje wszystkie zadania narzucone przez kontrakt, jednak mamy jeden problem. Urządzenie wielofunkcyjne jest naprawdę drogie i nieporęczne, więc nie ma potrzeby aby każdy pracownik posiadał swój „kombajn”. Co zatem z mniejszymi, bardziej przystępnymi cenowo drukarkami? Zobaczmy:

class SimplePrinter implements PrinterInterface
{
    public function print(string $doc): void
    {
        echo "Drukuję: $doc\n";
    }

    public function scan(string $doc): void
    {
        throw new \BadMethodCallException("Brak skanera!");  // ← wymuszone
    }

    public function fax(string $doc): void
    {
        throw new \BadMethodCallException("Brak faksu!");     // ← wymuszone
    }

    public function staple(string $doc): void
    {
        throw new \BadMethodCallException("Brak zszywacza!"); // ← wymuszone
    }
}

Nasza prosta drukarka nie posiada skanera, faksu ani zszywacza, więc nie może spełnić wymagań narzuconych przez kontrakt PrinterInterface. Jest to naruszenie ISP, ponieważ zmusza prostą drukarkę do implementacji metod, do których nie została zaprojektowana.

Jakie jest rozwiązanie? Myślę, że po poznaniu literki S (single responsibility principle) już się domyślamy. Wystarczy nasz ogromny interfejs PrinterInterface „pociąć” na kilka mniejszych, bardziej modułowych fragmentów, oto one:

interface PrintInterface
{
    public function print(string $doc): void;
}

interface ScanInterface
{
    public function scan(string $doc): void;
}

interface FaxInterface
{
    public function fax(string $doc): void;
}

interface StapleInterface
{
    public function staple(string $doc): void;
}

Każda z metod zostaje oddzielona do osobnego interfejsu, co daje nam większą swobodę w dobieraniu konfiguracji urządzenia, urządzenie wielofunkcyjne bez problemu zaimplementuje wszystkie interfejsy:

class MultifunctionDevice implements PrintInterface, ScanInterface, FaxInterface, StapleInterface
{
    public function print(string $doc): void  { echo "Drukuję: $doc\n";  }
    public function scan(string $doc): void   { echo "Skanuję: $doc\n";  }
    public function fax(string $doc): void    { echo "Faksuje: $doc\n";   }
    public function staple(string $doc): void { echo "Zszywa: $doc\n";   }
}

A co z naszą małą drukareczką, która potrafi jedynie drukować? Nie stanowi to już dla nas problemu:

class SimplePrinter implements PrintInterface
{
    public function print(string $doc): void
    {
        echo "Drukuję: $doc\n";  // ← tylko to, co potrzeba
    }
}

Drukarka robi to, co potrafi najlepiej, czyli po prostu drukuje.

Jeśli np. do firmy zakupiono by tylko skaner sieciowy, który skanuje dokumenty i przesyła je faxem, to także nie stanowi to problemu:

class NetworkScanner implements ScanInterface, FaxInterface
{
    public function scan(string $doc): void { echo "Skanuję: $doc\n"; }
    public function fax(string $doc): void  { echo "Faksuje: $doc\n";  }
}

I tak dalej, nasz kod mocno zyskuje na czytelności i łatwości dalszej obsługi i rozwoju. Jeżeli zechcemy dodać nowe funkcje (wydruk w kolorze, dupleks albo możliwość połączenia poprzez Wi-Fi), to wystarczy utworzyć nowy interfejs i zaimplementować go jedynie w urządzeniach, które daną funkcję posiadają. Ponadto, jeśli chcemy dodać nowe urządzenie, to wykorzystując posiadane interfejsy (lub dodając nowe), konstruujemy je jak budowlę z klocków.

W następnym wpisie postaram się omówić najciekawszą (a przynajmniej moim zdaniem) zasadę SOLID – Dependency Inversion.