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.
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).