
Zasady SOLID
2025/04/04
Wprowadzenie
Zasady regulujące sposób budowania kodu w paradygmacie obiektowym. Wyróżniamy pięć zasad, które zdaniem Roberta Martina ,,pozwalają na projektowanie bardziej zrozumiałych i łatwiejszych w utrzymaniu programów”. W tym artykule pokrótce opowiem o każdej z tych zasad i zobrazuje na przykładzie – dostarczania przesyłek różnymi środkami transportu.
Zasada pojedynczej odpowiedzialności
Zasada pojedynczej odpowiedzialności (Single responsibility principle, SRP) zwraca uwagę na to, że klasa powinna mieć jeden i tylko jeden powód do zmiany, czyli powinna odpowiadać za jedną funkcjonalność. Dzięki temu klasa staje się czytelna i uporządkowana.
Na poniższym przykładzie, każda klasa implementująca interfejs odpowiada za jedną rzecz, czyli dostarczenie przesyłki.
interface DeliveryInterface
{
public function deliver(): string;
}
class LandShipment implements DeliveryInterface
{
public function deliver(): string
{
return "Delivering by land.";
}
}
class AirShipment implements DeliveryInterface
{
public function deliver(): string
{
return "Delivering by air.";
}
}
Zasada SRP nie byłaby spełniona, gdy w klasach pojawiłaby się np. metoda odpowiedzialna za kalkulację kosztów transportu. Należałoby wtedy utworzyć oddzielną klasę ShipmentCostCalculator, która odpowiadałaby za obliczanie kosztu przesyłki w zależności od odległości, wagi przesyłki i środka transportu.
Zasada otwarte/zamknięte
Zasada otwarte/zamknięte (Open/closed principle, OCP) zwraca uwagę na to, że klasa powinna być otwarta na rozszerzanie, ale zamknięta na modyfikację istniejącego kodu. Dodawanie nowych funkcjonalności i ingerencja w istniejący kod zawsze niesie ze sobą ryzyko popełnienia błędu.
Rozszerzenie klasy powinno być realizowane poprzez dodanie nowej klasy. Przydaje się do tego wzorzec Strategia. który pozwala na wymianę klas, nie wpływając w żaden sposób na istniejący kod.
Poniższy przykład pokazuje różne rodzaje dostaw (lądowa, powietrzna, wodna), które realizowane są jako różne strategie. Możemy łatwo rozszerzyć serwis dostaw, dodając nowe środki transportu bez zmian w istniejącej implementacji.
interface DeliveryInterface
{
public function deliver(): string;
}
class LandShipment implements DeliveryInterface
{
public function deliver(): string
{
return "Delivering by land.";
}
}
class AirShipment implements DeliveryInterface
{
public function deliver(): string
{
return "Delivering by air.";
}
}
class WaterShipment implements DeliveryInterface
{
public function deliver(): string
{
return "Delivering by water.";
}
}
class ShipmentService
{
private DeliveryInterface $delivery;
public function __construct(DeliveryInterface $delivery)
{
$this->delivery = $delivery;
}
public function ship(): string
{
return $this->delivery->deliver();
}
}
$landDelivery = new ShipmentService(new LandShipment());
echo $landDelivery->ship(); // Delivering by land.
$airDelivery = new ShipmentService(new AirShipment());
echo $airDelivery->ship(); // Delivering by air.
Obiekt klasy ShipmentService najpierw przyjął pierwszą przesyłkę, która została dostarczona drogą lądową. Następnie przyjął drugą przesyłkę, którą dostarczono drogą powietrzną. W ten sposób realizujemy pewną strategię, w zależności od rodzaju przesyłki, serwis realizuje dostawę w odpowiedni sposób. Nic nie stoi na przeszkodzie, aby poszerzyć wachlarz dostaw.
Zasada podstawienia Liskov
Zasada podstawienia Liskov (Liskov substitution principle, LSP) zwraca uwagę na to, że obiekt klasy pochodnej (dziedziczącej po klasie bazowej lub implementującej interfejs) powinnien być zamiennikiem dla obiektu tejże klasy bazowej lub interfejsu bez zmiany logiki działania programu.
interface DeliveryInterface
{
public function deliver(): string;
}
class BikeShipment implements DeliveryInterface
{
public function deliver(): string
{
return "Delivering by bike.";
}
}
class ShipmentService
{
private DeliveryInterface $delivery;
public function __construct(DeliveryInterface $delivery)
{
$this->delivery = $delivery;
}
public function ship(): string
{
return $this->delivery->deliver();
}
}
$bikeDelivery = new ShipmentService(new BikeShipment());
echo $bikeDelivery->ship(); // Delivering by bike.
Nowa klasa BikeShipment działa w ten sam sposób jak poprzednie i można ją podmienić zamiast obiektu klasy bazowej (w naszym przypadku interfejsu), w dalszym ciągu poprawnie realizując serwis dostaw.
Zasada segregacji interfejsów
Zasada segregacji interfejsów (Interface segregation principle, ISP) zwraca uwagę na to, że lepiej posiadać więcej chudszych interfejsów, niż jeden gruby. Obszerny interfejs wypchany metodami do zaimplementowania przez klasy pochodne powoduje czasami implementacje tychże metod na siłę.
Rozwiązaniem jest tworzenie bardziej specyficznych interfejsów z niezbędnymi metodami i danymi wejściowymi.
Przykład złego podejścia – interfejs posiadający metody, które klasy implementujące nie będą w 100% realizować. Klasa BikeShipment nie będzie wykorzystywać metody refuel() i jej implementacja musiałaby np. rzucić wyjątek ,,Roweru się nie tankuje”.
interface DeliveryInterface
{
public function deliver(): string;
public function refuel(): string;
}
Przykład dobrego podejścia – utworzenie kilku mniejszych interfejsów, które będą w pełni wykorzystywane przez konkretne klasy.
interface DeliveryInterface
{
public function deliver(): string;
}
interface FuelPoweredInterface
{
public function refuel(): string;
}
class WaterShipment implements DeliveryInterface, FuelPoweredInterface
{
public function deliver(): string
{
return "Delivering by water.";
}
public function refuel(): string
{
return "Refueling the ship.";
}
}
Zasada odwrócenia zależności
Zasada odwórcenia zależności (Dependency inversion principle, DIP) zwraca uwagę na to, że moduły wyższego poziomu nie powinny zależeć od modułów niższego poziomu – obie grupy powinny zależeć od abstrakcji.
Przykład złego podejścia – serwis zależy od implementacji konkretnego środka transportu.
class ShipmentService
{
private LandShipment $landShipment;
public function __construct()
{
$this->landShipment = new LandShipment();
}
public function ship(): string
{
return $this->landShipment->deliver();
}
}
Przykład dobrego podejścia – serwis zależy od interfejsu, czyli abstracji. Możemy podmienić środek transportu bez zmiany kodu klasy ShipmentService.
class ShipmentService
{
private DeliveryInterface $delivery;
public function __construct(DeliveryInterface $delivery)
{
$this->delivery = $delivery;
}
public function ship(): string
{
return $this->delivery->deliver();
}
}