SOLID – zasada segregacji interfejsów

Czym jest Zasada Segregacji Interfejsów?

Zasada segregacji interfejsów mówi, że:

Klient nie powinien być zmuszany do zależności od interfejsów, których nie używa.

Innymi słowy, zamiast tworzyć obszerne, “grube” interfejsy (tzw. “fat interfaces”), powinniśmy dzielić je na mniejsze, bardziej specyficzne jednostki. Dzięki temu klasy implementują tylko te metody, których faktycznie potrzebują, co prowadzi do czystszego, bardziej elastycznego i łatwiejszego w utrzymaniu kodu.

Kluczowe Aspekty ISP

  • Granularność interfejsów: Interfejsy powinny być małe i skupione na jednej, konkretnej odpowiedzialności lub roli.
  • Unikanie “fat interfaces”: Duże, ogólne interfejsy zmuszają klasy do implementowania metod, które nie mają dla nich sensu. Prowadzi to do pustych implementacji lub, co gorsza, rzucania wyjątków.
  • Poprawa elastyczności: Dzięki segregacji interfejsów możemy łatwiej rozszerzać system. Nowe klasy mogą implementować tylko te interfejsy, które są dla nich istotne, bez wpływu na istniejący kod.
  • Zgodność z innymi zasadami SOLID: ISP doskonale uzupełnia Zasadę Pojedynczej Odpowiedzialności (SRP), ponieważ każdy interfejs ma jeden, jasno określony cel.

Przykład Naruszenia Zasady (Antywzorzec)

Wyobraźmy sobie system e-commerce, w którym mamy różne produkty. Tworzymy jeden ogólny interfejs Product_bad, który ma opisywać każdy możliwy produkt.

Diagram UML

Kod Naruszający Zasadę

Nasz “gruby” interfejs Product_bad zawiera metody dotyczące ceny, wagi oraz… systemu operacyjnego.

Java
public interface Product_bad {
    int getPrice();
    void setPrice(int price);

    int getWeight();
    void setWeight(int weight);

    String getOs();
    void setOs(String os);
}

Teraz spróbujmy zaimplementować ten interfejs dla dwóch różnych produktów: komputera i szkolenia online.

  • Klasa Computer_bad:

Dla komputera wszystkie metody mają sens, więc klasa implementuje cały interfejs.

Java
public class Computer_bad implements Product_bad {
    // ... Implementacje wszystkich metod: cena, waga, OS
}
  • Klasa Training_bad:

Tutaj zaczyna się problem. Szkolenie ma cenę, ale nie ma wagi ani systemu operacyjnego. Jesteśmy jednak zmuszeni do zaimplementowania wszystkich metod z interfejsu.

Java
public class Training_bad implements Product_bad {
    @Override
    public int getPrice() { return 100; }

    @Override
    public void setPrice(int price) { /* ... */ }

    @Override
    public int getWeight() {
        throw new UnsupportedOperationException("Szkolenie nie ma wagi!");
    }

    @Override
    public void setWeight(int weight) {
        throw new UnsupportedOperationException("Szkolenie nie ma wagi!");
    }

    @Override
    public String getOs() {
        throw new UnsupportedOperationException("Szkolenie nie ma systemu operacyjnego!");
    }

    @Override
    public void setOs(String os) {
        throw new UnsupportedOperationException("Szkolenie nie ma systemu operacyjnego!");
    }
}

Problem z Naruszeniem ISP

Powyższy kod jest tykającą bombą zegarową.

  • Zmuszanie do niepotrzebnych implementacji: Klasa Training_bad musi implementować metody, które są dla niej bezsensowne. Rzucanie wyjątków UnsupportedOperationException jest anty-wzorcem, który może prowadzić do nieoczekiwanych błędów w czasie wykonania programu.
  • Brak elastyczności: Każdy kod, który operuje na obiektach typu Product_bad, musi być napisany defensywnie, zakładając, że wywołanie metody getWeight() może zakończyć się wyjątkiem.
  • Zanieczyszczenie kodu: Klasy stają się “zaśmiecone” pustymi lub rzucającymi wyjątki metodami, co zaciemnia ich prawdziwą odpowiedzialność i utrudnia zrozumienie oraz utrzymanie kodu.

Poprawne Zastosowanie Zasady (Refaktoryzacja)

Rozwiązaniem jest podział naszego “grubego” interfejsu na mniejsze, bardziej wyspecjalizowane jednostki, z których każda odpowiada za jedną, konkretną cechę produktu.

Diagram UML (po poprawie)

Poprawiony Kod

Tworzymy trzy małe, celowe interfejsy:

Java
// Interfejs dla produktów, które mają cenę
public interface IProduct {
    int getPrice();
    void setPrice(int price);
}

// Interfejs dla produktów, które można dostarczyć (mają wagę)
public interface IDeliverable {
    int getWeight();
    void setWeight(int weight);
}

// Interfejs dla produktów komputerowych (mają OS)
public interface IComputer {
    String getOs();
    void setOs(String os);
}

Teraz nasze klasy mogą implementować tylko te interfejsy, których naprawdę potrzebują.

  • Klasa Training:

Szkolenie ma tylko cenę, więc implementuje wyłącznie IProduct. Kod jest czysty i bezpieczny.

Java
public class Training implements IProduct {
    @Override
    public int getPrice() { /* ... */ }

    @Override
    public void setPrice(int price) { /* ... */ }
}
  • Klasa Computer:

Komputer ma wszystkie te cechy, więc implementuje wszystkie trzy interfejsy.

Java
public class Computer implements IProduct, IDeliverable, IComputer {
    @Override
    public int getPrice() { /* ... */ }

    @Override
    public void setPrice(int price) { /* ... */ }

    @Override
    public int getWeight() { /* ... */ }

    @Override
    public void setWeight(int weight) { /* ... */ }

    @Override
    public String getOs() { /* ... */ }

    @Override
    public void setOs(String os) { /* ... */ }
}

Dzięki temu podejściu klient (kod używający tych klas) może teraz zależeć tylko od potrzebnego mu interfejsu, np. funkcja obliczająca koszt dostawy będzie operować na IDeliverable, a system fakturowania na IProduct.


Porównanie Przykładów

AspektWersja Naruszająca ISPWersja Zgodna z ISP
Struktura interfejsuJeden duży, ogólny interfejs Product_bad.Wiele małych, specyficznych interfejsów (IProduct, IDeliverable).
Implementacja w klasachKlasy są zmuszone implementować niepotrzebne metody, co prowadzi do wyjątków.Klasy implementują tylko te interfejsy, których potrzebują. Brak zbędnego kodu.
Bezpieczeństwo koduNiskie. Klient używający Product_bad ryzykuje UnsupportedOperationException.Wysokie. Klient zależy od konkretnego interfejsu, co gwarantuje istnienie potrzebnych metod.
Elastyczność i skalowalnośćNiska. Dodanie nowego typu produktu (np. e-book) wymagałoby dalszych kompromisów.Wysoka. Nowe klasy mogą swobodnie komponować potrzebne interfejsy.

Podsumowanie

Zasada Segregacji Interfejsów to klucz do tworzenia modułowych i elastycznych systemów. Zamiast zmuszać klasy do noszenia “za dużego płaszcza” w postaci jednego, obszernego interfejsu, dajemy im zestaw mniejszych, dopasowanych narzędzi. Prowadzi to do czystszego kodu, mniejszej liczby błędów i znacznie łatwiejszego utrzymania oraz rozbudowy aplikacji. Stosowanie ISP sprawia, że nasza architektura staje się bardziej przemyślana i skalowalna.

Bibliografia

  • Martin, R. C. (2008). Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall.
  • Martin, R. C. (2017). Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall.