Praca nad projektem idzie do przodu, jednak ciężko oprzeć  się wrażeniu, że powstaje cała masa abstrakcji, a w zasadzie nie mamy jeszcze nic konkretnego. I póki co tak ma być! Ma powstawać masa interfejsów, żeby później kod był łatwiejszy do edycji i dzisiaj coś właśnie o nich, a mianowicie kolejna z zasad SOLID: zasada segregacji interfejsów.

Muszę przyznać, że nie spodziewałem się aż takiego poświęcenia czasu na pracę nad projektem i blogiem. Pogodzenie pracy full time, zleceń robionych po godzinach i udziału w #dajsiepoznac, a do tego życia prywatnego, to nie lada wyzwanie, mimo że nie mam problemów z zarządzaniem czasem i samozaparciem. Zaczyna po prostu powoli brakować doby na wszystkie pomysły, a mimo tego wcale nie czuję żeby to było męczące, wręcz przeciwnie!

Ostatnio dużo było o abstrakcjach, więc chciałbym kontynuować ten temat póki właśnie nad tym pracuję. Spoglądając w kod projektu możesz zauważyć, że jest tam już całkiem sporo interfejsów i ich ilość przybywa naprawdę w dużym tempie. Prawie dla każdej klasy mamy abstrakcję, która jej odpowiada i definiuje jednocześnie zawartość dostępną z zewnątrz. Dlaczego w ogóle tak się dzieje?

Zasada segregacji interfejsów

W większości przypadków za taką sytuację odpowiada zasada segregacji interfejsów (ang. Interface segregation principle). Wbrew temu jak brzmi jej nazwa, wcale nie chodzi tutaj o segregowanie interfejsów według jakiejś kolejności. Zasada segregacji interfejsów mówi o tym, by każdy interfejs pisać dla konkretnej funkcjonalności tak, aby programista nie był zmuszany do implementowania metod, które w danym miejscu są kompletnie bezużyteczne. Odpowiada to mniej więcej definicji znalezionej na Wikipedii:

Klienci nie powinni zależeć od interfejsów, których nie używają.

W tym momencie, aby jak najlepiej wyjaśnić Ci temat, odejdziemy od algorytmów genetycznych i znajdziemy przykład bardziej “z życia”. Wszystko możemy porównać do obsługi samochodu rajdowego przez niedzielnego kierowcę, który na co dzień jeździ pojazdem z automatyczną skrzynią biegów. Zamodelujemy to następującymi interfejsami:

interface IObslugaRajdowki
{
    void GazDoDechy();
    void ZmienBieg(int numer);
    void HamujPrzod();
    void HamujTyl();
    void BlokujDyfer();
}

interface IOblsugaOsobowki
{
    void Gaz();
    void Hamuj();
}

Wyraźnie widać różnicę między interfejsami – pierwszy z nich jest zdecydowanie bardziej specjalistyczny i najprawdopodobniej SlabyKierowca nawet nie będzie wiedział jak obsłużyć dane polecenia, a w wyniku jak zaimplementować konkretne metody. Być może uda mu się to zrobić, ale na przykład część z nich pozostanie pusta.

class SlabyKierowca : IObslugaRajdowki
{
    public void BlokujDyfer()
    {
        //Co to w ogóle dyfer?!
    }

    public void GazDoDechy()
    {
        WciskamPedalPoPrawej(); //Tylko gdzie ta deska?
    }

    public void HamujPrzod()
    {
        WciskamPedalPoLewej(); //Ale tutaj jest jeszcze środkowy O.o
    }

    public void HamujTyl()
    {
        //Jak to, osobno?!
    }

    public void ZmienBieg(int numer)
    {
        //To sam się nie zmieni?
    }
}

Nie ma szans, żeby taki kierowca obsłużył samochód rajdowy. Tak samo jest z naszymi interfejsami i klasami, które je implementują. Klasa musi implementować interfejs dostosowany do jej możliwości, bez żadnych nadprogramowych metod. Lepiej jest mieć więcej interfejsów do konkretnych potrzeb, niż jeden który ma wszystkie metody, ale klasy połowy z nich nawet nie obsługują. SlabyKierowca powinien więc implementować drugi z zaproponowanych interfejsów:

class SlabyKierowca : IOblsugaOsobowki
{
    public void Gaz()
    {
        WciskamPedalPoPrawej();
    }

    public void Hamuj()
    {
        WciskamPedalPoLewej();
    }
}

Jak widać powinno się projektować interfejs do konkretnej klasy klienckiej. Powinien on posiadać taki zakres metod, aby sprostać oczekiwaniom a jednocześnie nie być rozbudowany o niepotrzebne składniki. W naszym przypadku widać, że klasa kliencka SlabyKierowca potrzebuje jedynie dwóch metod: Gaz i Hamuj, a cała reszta z interfejsu ISamochodRajdowy byłaby niepotrzebna.


Zasada segregacji interfejsów nakazuje więc, aby interfejs podpowiadał potrzebom konkretnej klasy, a nie na odwrót. Oczywiście przy tym pamiętać trzeba też o zasadzie odwracania zależności – implementacja ma zależeć od abstrakcji. Wynikiem tych dwóch zasad jest to, że w sytuacjach gdy widzimy, że dany interfejs zmusza nas do pisania zbyt wielu metod, warto napisać inny, który będzie wygodniejszy w implementacji.