SOLID cz.5 – ISP Interface Segregation Principle

Zasada Single Resposibility skupia się na jak największym wyspecjalizowaniu klas a tym samym zmniejszeniu ich odpowiedzialności i wielkości. Zasada Interface Segregation Principle (dalej ISP) jest w pewnym sensie odpowiednikiem SRP dla interfejsów – mówi ona o tym aby unikać dużych interfejsów. Tworzenie ciężkich interfejsów z wieloma metodami niepowiązanymi ze sobą powoduje, że każda implementacja takiego interfejsu nie będzie spełniała SRP lub w przypadku niezaimplementowania części funkcji także naruszeniem LSP.

Za duże interfejsy

Interfejsy powinny odzwierciedlać zapotrzebowania klienta, który wykorzystuje dany interfejs. Niedopuszczalne jest tworzenie uniwersalnego interfejsu dla różnych klientów, które wykorzystują tylko pewną część metod. Dobrą praktyką jest tworzenie dla każdego klienta osobnego interfejsu tylko i wyłącznie z niezbędnymi metodami.

Tworzenie podobnych interfejsów dla różnych klientów może zdawać się nadmiarowe i niepotrzebne ale każdy klient wraz z rozwojem systemu zacznie ewoluować w inną stronę niż inne klienty. Po wielu latach rozwoju systemu rozdzielenie interfejsów przyniesie wymierne korzyści w postaci bardziej elastycznego kodu. Jedynie mając 100% pewność, że 2 różne klienty potrzebują takiego samego interfejsu i jest to niezmienne w czasie możemy bez obaw wykorzystać jeden interfejs ale należy pamiętać, że zawsze łatwiej coś połączyć niż potem rozdzielić.

Dobrym przykładem nieodpowiedniego stosowania się do tej zasady jest tworzenie interfejsu typu Manager (bardzo nie lubię tego typu klas, w większości przypadków nazwanie klasy managerem wynika z braku analizy problemu, braku znajomości zasad SOLID i tworzenia dobrej architektury) – chyba, że Manager to klasa domenowa oznaczająca stanowisko.

public interface DataManager {

    User getUser(int userId);

    User saveUser(User user);

    List<User> findUserByName(String name);

    Invoice getInvoice(String invoiceNumber);

    List<Invoice> findInvoiceByUser(User user);

    List<Invoice> findInvoiceByOrder(Order order);
}

Powyższy interfejs jest przykładem interfejsu typu mydło i powidło. Czemu powyższe podejście jest złe?

  • Każda implementacja nie będzie spełniała SRP.
  • Naruszenia zasady OCP wystąpi w momencie gdy będzie trzeba dodać metodę pozwalającą wyszukiwać użytkowników po adresie e-mail.
  • Jeśli doda się metodę to trzeba będzie skompilować wszystkie moduły korzystające z danego interfejsu. Fakt, że musimy skompilować np. moduł do wyszukiwania faktur w momencie gdy dodajemy metodę do wyszukiwania użytkowników świadczy o tym, że interfejs który chcemy edytować jest nieodpowiednio zaprojektowany.

Tego typu interfejsów powinniśmy się wystrzegać, ponieważ prędzej czy później nadejdzie czas w którym będzie trzeba zrobić wielką refaktoryzację. Jeśli okaże się, że mega DataManager zawiera całą logikę biznesową aplikacji to reafktoryzacja ta będzie bardzo ryzykowna, ale konieczna. (doświadczyłem takich refaktoryzacji kilka razy a najdłuższe refaktoryzacje potrafiły trwać miesiąc! – strata pieniędzy i czasu) ale zysk po refaktoryzacji był warty trudu.

Niewyspecjalizowane interfejsy

Oprócz dużych interfejsów można napotkać także te małe, które też nie spełniają ISP. Przykładem może być nieduży i prosty interfejs będący abstrakcją dla różnego rodzaju sterowania oświetleniem:

public interface Light {
    void turnOn();

    void turnOff();

    boolean isOn();

    void setDim(float dim);
    
    float getDim();
}

całość wygląda spójnie i dobrze, dopóki nie stwierdzimy, że w naszym systemie chcemy użyć żarówek fluorescencyjnych, których nie możemy ściemniać. Powyższy interfejs narusza zasadę ISP a implementacja będzie naruszała także LSP.

Aby to naprawić i doprowadzić do zgodności z ISP należy wydzielić z interfejsu Light oddzielny interfejs, który obsługuje tylko ściemnianie:

public interface Dimmable {
    void setDim(float dim);
    
    float getDim();
}
public interface Light {
    void turnOn();

    void turnOff();

    boolean isOn();
}

Interfejs Dimmable będą implementowały tylko te klasy świateł, które taką funkcję posiadają. Światła nieściemniane nie będą implementowały interfejsu Dimmable i w ten sposób nie będą dostępne dla modułu ściemniającego ale będą nadal dostępne dla modułu włączająco-wyłączającego.

Rozdzielenie interfejsów uchroni przed pustymi metodami bądź dziwnymi konstrukcjami, które będą naruszały zasadę LSP. Przed zdefiniowaniem interfejsu należy zadać sobie jedno ale bardzo ważne pytanie. Jakie potencjale zmiany (realne) mogą zajść w systemie i czy po tych zmianach nowa implementacja interfejsu będzie spełniała zasadę LSP. Jeśli nie, to prawdopodobnie interfejs jest zbyt duży.

Wnioski

Przede wszystkim należy pamiętać, że interfejsu nie tworzy się dlatego, że czasami jest to uznawane za profesjonalizm – tworzenie nieprzemyślanych interfejsów jest prawie tak samo nieprofesjonalne co nietworzenie ich wcale.

Interfejs jest sposobem na odwrócenie zależności (patrz zasada DIP) i to klient (klasa) korzystający z danego interfejsu powinien ten interfejs definiować. Mimo, że klient definiuje interfejs to w jednym interfejsie powinny znaleźć się metody ściśle ze sobą powiązane (nie tak jak w przypadku super klasy DataManager lub Light).

Jeśli uważasz treść za wartościową to podziel się nią z innymi. Dziękuję.

Mateusz Chrustny

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *