Start Kontakt

Zasada SOLID (3/4)

W poprzednich dwóch artykułach analizowaliśmy trzy zasady SOLID: Single Responsibility Principle, Open/Closed Principle oraz Liskov Substitution Principle (zasadę podstawienia Liskov). Nadszedł czas na omówienie kolejnej, czyli zasady segregacji interfejsów.

Interface Segregation Principle

Klasy klienckie nie powinny być zmuszane do implementacji funkcjonalności, których nie wykorzystują. Interfejsy nie mogą być zbyt obszerne i ogólne, lecz powinny być mniejsze i bardziej specjalizowane. W razie konieczności klasa powinna implementować kilka specjalizowanych interfejsów, a nie jeden duży, z którego i tak wykorzysta niewiele metod.

Powiedzmy, że tworzymy symulator środków transportu. Utworzyliśmy jedną dużą klasę w pełni abstrakcyjną (interfejs), na podstawie której będziemy implementować poszczególne typy środków transportu:

class Transport
{
public:
    virtual int getSpeed(void) = 0; // prędkość
    virtual int getAltitude(void) = 0; // wysokość
    virtual int getClimbSpeed(void) = 0; // prędkość wznoszenia
    virtual unsigned int getDisplacement(void) = 0; // wyporność
    virtual unsigned int getDraught(void) = 0; // zanurzenie
    virtual unsigned int getHorizSpinningRotors(void) = 0; // liczba wirników obracających się w poziomie
    virtual unsigned int getWheels(void) = 0; // liczba kół
    virtual bool isSteeringWheelOnLeftSide(void) = 0; // czy kierownica po lewej stronie?
}; 

A oto przykładowe implementacje:

class Airplane : public Transport
{
    virtual int getSpeed(void) override
    {  
        int speed{};
        // wyznaczenie prędkości
        return speed;
    }
    virtual int getAltitude(void) override
    {
        int altitude{};
        // wyznaczenie wysokości
        return altitude;
    }
    // ... itd.

    // getDisplacement, getDraught, getWheels, isSteeringWheelOnLeftSide - nie zaimplementowano!
};

class Truck : public Transport
{
    virtual int getSpeed(void) override
    {
        int speed{};
        // wyznaczenie prędkości
        return speed;
    }
    virtual unsigned int getWheels(void) override
    {
        unsigned int wheels{};
        // wyznaczenie liczby kół
        return wheels;
    }
    // ... itd.

    // getAltitude, getClimbSpeed, getDisplacement, getDraught - nie zaimplementowano!

};

class Ship : public Transport
{
    virtual int getSpeed(void) override
    {
        int speed{};
        // wyznaczenie prędkości
        return speed;
    }
    virtual unsigned int getDisplacement(void) override
    {
        unsigned int displacement{};
        // wyznaczenie wyporności
        return displacement;
    }
    // ... itd.

    // getAltitude, getClimbSpeed, getWheels, isSteeringWheelOnLeftSide - nie zaimplementowano!

};

class Amphibian : public Transport
{
    virtual int getSpeed(void) override
    {
        int speed{};
        // wyznaczenie prędkości
        return speed;
    }
    virtual unsigned int getDisplacement(void) override
    {
        unsigned int displacement{};
        // wyznaczenie wyporności
        return displacement;
    }
    virtual unsigned int getWheels(void) override
    {
        unsigned int wheels{};
        // wyznaczenie liczby kół
        return wheels;
    }
    // ... itd.

    // getAltitude, getClimbSpeed - nie zaimplementowano!
};

Jak widać, każda klasa pochodna zawiera pewne metody, których nie może implementować, ponieważ nie miałoby to sensu. Na przykład Ship nie może zaimplementować metody getWheels, a Truck metody getAltitude. Co z tym zrobić?

Utwórzmy więcej interfejsów, które będą bardziej specjalizowane. Użyjmy ich następnie do implementacji środków transportu:

class GeneralTransport
{
    virtual int getSpeed(void) = 0; // prędkość
};

class AirTransport
{
    virtual int getAltitude(void) = 0; // wysokość
    virtual int getClimbSpeed(void) = 0; // prędkość wznoszenia
};


class WaterTransport
{
    virtual unsigned int getDisplacement(void) = 0; // wyporność
    virtual unsigned int getDraught(void) = 0; // zanurzenie
};

class LandTransport
{
    virtual unsigned int getWheels(void) = 0; // liczba kół
    virtual bool isSteeringWheelOnLeftSide(void) = 0; // czy kierownica po lewej stronie?
};

A oto nowe implementacje:

class Airplane : public GeneralTransport, AirTransport
{
    virtual int getSpeed(void) override
    {  
        int speed{};
        // wyznaczenie prędkości
        return speed;
    }
    virtual int getAltitude(void) override
    {
        int altitude{};
        // wyznaczenie wysokości
        return altitude;
    }
    // ... itd.
};

class Truck : public GeneralTransport, LandTransport
{
    virtual int getSpeed(void) override
    {
        int speed{};
        // wyznaczenie prędkości
        return speed;
    }
    virtual unsigned int getWheels(void) override
    {
        unsigned int wheels{};
        // wyznaczenie liczby kół
        return wheels;
    }
    // ... itd.
};

class Ship : public GeneralTransport, WaterTransport
{
    virtual int getSpeed(void) override
    {
        int speed{};
        // wyznaczenie prędkości
        return speed;
    }
    virtual unsigned int getDisplacement(void) override
    {
        unsigned int displacement{};
        // wyznaczenie wyporności
        return displacement;
    }
    // ... itd.
};

class Amphibian : public GeneralTransport, LandTransport, WaterTransport
{
    virtual int getSpeed(void) override
    {
        int speed{};
        // wyznaczenie prędkości
        return speed;
    }
    virtual unsigned int getDisplacement(void) override
    {
        unsigned int displacement{};
        // wyznaczenie wyporności
        return displacement;
    }
    virtual unsigned int getWheels(void) override
    {
        unsigned int wheels{};
        // wyznaczenie liczby kół
        return wheels;
    }
    // ... itd.
};

Uzyskaliśmy znaczną poprawę kodu. Dzięki specjalizowanym, prostszym interfejsom poszczególne klasy implementują wszystkie niezbędne metody i nic ponad to. Pamiętajmy jednak, że przesada jest niezdrowa - im więcej interfejsów, tym bardziej kod staje się skomplikowany. Jak zwykle trzeba się kierować rozsądkiem, unikać skrajności i zachowywać złoty środek.

W następnym, a zarazem ostatnim artykule o SOLID zajmiemy się zasadą Dependency Inversion Principle (odwrócenia zależności).