Zasady SOLID #4 – Interface Segregation Principle

Wyobraźmy sobie taką sytuację – kupujemy w sklepie produkt, który potrzebujemy. Niestety trafiamy na sprzedawcę, speca od „marketingu”, który w przypływie szczodrości obdarowuje nas dodatkowo katalogiem wszystkich produktów firmy, próbkami innych produktów, brelokiem z logo firmy i masą innych niepotrzebnych nam rzeczy. Dzisiejszy wpis pokaże nam w jaki sposób radzić sobie z niepotrzebnymi rzeczami w programistycznym świecie.

Wprowadzenie

Zasada SOLID nr 4 nosi nazwę Zasada Segregacji Interfejsów (ang. Interface Segregation Principle). Treść tej zasady jest następująca: klienci nie powinni zależeć od interfejsów, których nie używają. Można też spotkać się z taką treścią: wiele dedykowanych interfejsów jest lepsze niż jeden ogólny.
Czasem zdarza się, że klasa, która implementuje pewien interfejs, zostaje zmuszona do zaimplementowania metod, z których klient nie korzysta. Jeżeli w naszych programach pojawi się taka sytuacja, to znak, że ten interfejs powinien zostać rozbity na dwa lub więcej mniejszych interfejsów.

Objaśnienie zasady

Z dzisiejszym przykładem niezgodności z Zasadą Segregacji Interfejsów spotkałem się osobiście. W celu montażu zmywarki do naczyń byłem zmuszony zakupić nasadkę na klucz z końcówką typu Torx (jest to nasadka do śrub w kształcie sześcioramiennej gwiazdy). Niestety w sklepie narzędziowym,do którego się udałem, nie było możliwości zakupu pojedynczej nasadki. Występowała ona tylko w zestawie narzędziowym składającym się z wielu nasadek różnych kształtów i rozmiarów, grzechotek, kluczy płaskich i innych narzędzi, których w ogóle nie potrzebowałem.
Ta skrzynka narzędziowa może posłużyć jako przykład interfejsu, który zmusza użytkownika do korzystania z rzeczy których nie potrzebuje.
Przykładem zgodności z ISP może być np. konfigurator PC dostępny w niektórych internetowych sklepach komputerowych. Zamiast kupować gotowy PC, konfigurator umożliwia samodzielny wybór tych konkretnych komponentów które chcemy posiadać w swoim PC. Jeżeli potrzebujemy, możemy także dobrać do swojego komputera urządzenia dodatkowe np. monitor, klawiaturę, mysz, ale nie jesteśmy zmuszeni do ich zakupu, jeżeli ich nie potrzebujemy.

Przykład

Tym razem za przykład niech posłuży oprogramowanie sklepu internetowego z oponami samochodowymi.
public interface IProduct {

    String getName();
    String getPrice();
    String getDiameter();
    String getWidth();
    String getProfile();
    String getSeason();
}

public class Tire implements IProduct {

    private String name;
    private String price;
    private String diameter;
    private String width;
    private String profile;
    private String season;

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getPrice() {
        return price;
    }

    @Override
    public String getDiameter() {
        return diameter;
    }

    @Override
    public String getWidth() {
        return width;
    }

    @Override
    public String getProfile() {
        return profile;
    }

    @Override
    public String getSeason() {
        return season;
    }
} 
Interfejs IProduct ma zadeklarowane metody, jakie będzie implementowała klasa Tire, której obiekty to poszczególne opony w sklepie. Jak na razie kod jest poprawny – interfejs umożliwia pobieranie pojedynczych informacji o każdej z opon.

Kod niezgodny z ISP

Po pewnym czasie postanowiliśmy rozszerzyć asortyment sklepu o felgi. Musimy dodać więcej metod do interfejsu, gdyż felga posiada nieco inne parametry. Spójrzmy na kod poniżej:
public interface IProduct {

    String getName();
    String getPrice();
    String getDiameter();
    String getWidth();
    String getProfile();
    String getSeason();

    String getMaterial();
    String getColor();
}

public class Tire implements IProduct {

    private String name;
    private String price;
    private String diameter;
    private String width;
    private String profile;
    private String season;

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getPrice() {
        return price;
    }

    @Override
    public String getDiameter() {
        return diameter;
    }

    @Override
    public String getWidth() {
        return width;
    }

    @Override
    public String getProfile() {
        return profile;
    }

    @Override
    public String getSeason() {
        return season;
    }

    @Override
    public String getMaterial() {
        return null;
    }

    @Override
    public String getColor() {
        return null;
    }
}

public class Rim implements IProduct {

    private String name;
    private String price;
    private String diameter;
    private String material;
    private String color;

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getPrice() {
        return price;
    }

    @Override
    public String getDiameter() {
        return diameter;
    }

    @Override
    public String getWidth() {
        return null;
    }

    @Override
    public String getProfile() {
        return null;
    }

    @Override
    public String getSeason() {
        return null;
    }

    @Override
    public String getMaterial() {
        return material;
    }

    @Override
    public String getColor() {
        return color;
    }
} 
Po rozszerzeniu interfejsu pojawił się pewien problem. Klasy Tire i Rim implementujące interfejs IProduct muszą zaimplementować wszystkie jego metody. Jednak klasa Tire nie korzysta z wszystkich metod, gdyż niektóre z nich są zarezerwowane tylko dla obiektów klasy Rim. Atrybuty color i material są zarezerwowane dla klasy Rim, więc klasa Tire nie potrzebuje getMaterial() i getColor(). I analogicznie, klasa Rim nie potrzebuje metod getSeason(), getWidth() i getProfile(). Obchodzimy ten problem poprzez zwrócenie wartości null dla niektórych metod.
W ten sposób złamaliśmy Zasadę Segregacji Interfejsów, gdyż obiekty klas implementujących ten interfejs nie korzystają z wszystkich jego metod. Dodatkowo złamaliśmy też Zasadę Podstawienia Liskov, gdyż metody niepotrzebne w danej klasie zwracają null, co jest równoznaczne ze zmianą zachowania.

Kod zgodny z ISP

public interface IProduct {

    String getName();
    String getPrice();
    String getDiameter();
}

public interface ITire {

    String getWidth();
    String getProfile();
    String getSeason();
}

public interface IRim {

    String getMaterial();
    String getColor();
}

public class Tire implements IProduct, ITire {

    private String name;
    private String price;
    private String diameter;
    private String width;
    private String profile;
    private String season;

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getPrice() {
        return price;
    }

    @Override
    public String getDiameter() {
        return diameter;
    }


    @Override
    public String getWidth() {
        return width;
    }

    @Override
    public String getProfile() {
        return profile;
    }

    @Override
    public String getSeason() {
        return season;
    }
}

public class Rim implements IProduct, IRim {

    private String name;
    private String price;
    private String diameter;
    private String material;
    private String color;

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getPrice() {
        return price;
    }

    @Override
    public String getDiameter() {
        return diameter;
    }

    @Override
    public String getMaterial() {
        return material;
    }

    @Override
    public String getColor() {
        return color;
    }
} 
W tym przypadku z jednego dużego interfejsu zrobiliśmy trzy mniejsze. IProduct posiada metody wspólne dla każdego produktu w sklepie, ITire posiada tylko te metody, które dotyczą opon, a IRim te, które dotyczą felg. W ten sposób zachowaliśmy Zasadę Segregacji Interfejsów, gdyż każda z klas implementuje tylko te interfejsy, z których chce skorzystać.

Podsumowanie

Starajmy się unikać bałaganu i niedbałości w pisaniu oprogramowania. Tworzenie dużych interfejsów z wieloma metodami może spowodować konieczność zaimplementowania niektórych metod w klasie tylko po to, aby program mógł się skompilować. Przypadkowa próba wywołania takiej metody może spowodować zupełnie nieoczekiwane rezultaty. Dlatego warto poświęcić nieco czasu, aby interfejsy były możliwie jak najmniejsze, a obiekty korzystały tylko z tego, czego naprawdę potrzebują.

Znajdź mnie również na:

arrow