SOLID to zbiór zasad, które pomagają programistom tworzyć elastyczny, skalowalny i łatwy do utrzymania kod. Każda z zasad ma swoje specyficzne cele i zastosowania. Ich zastosowanie w połączeniu może przynieść znaczne korzyści w tworzeniu jakościowego kodu.
Poniżej ku pamięci, czym jest SOLID? Według Wikipedii, mnemonik ten został zaproponowany przez Roberta C. Martina. Znanego w branży pod pseudonimem 'Uncle Bob’, który jest twórcą m.in. kultowej książki 'Czysty Kod’. Opisuje pięć podstawowych założeń programowania obiektowego:
- Single Responsibility Principle (SRP) – Zasada pojedynczej odpowiedzialności.
- Open/Closed Principle (OCP) – Zasada otwarte-zamknięte.
- Liskov Substitution Principle (LSP) – Zasada podstawienia Liskov.
- Interface Segregation Principle (ISP) – Zasada segregacji interfejsów.
- Dependency Inversion Principle (DIP) – Zasada odwrócenia zależności.
[SOLID] SINGLE RESPONSIBILITY PRINCIPLE
:: zasada pojedynczej odpowiedzialności
Zasada pojedynczej odpowiedzialności (SRP) w SOLID mówi o tym, że klasa powinna mieć tylko jedną odpowiedzialność. Dodatkowo tylko jeden powód powinien móc zmienić tę klasę.
Jednym z przykładów zastosowania tej zasady może być klasa Logger
, która odpowiada za logowanie informacji w aplikacji. Według zasady SRP, klasa Logger
powinna być odpowiedzialna tylko za logowanie i nie powinna zawierać dodatkowej funkcjonalności, która nie jest związana z jej podstawową odpowiedzialnością.
Poniżej przedstawiony jest przykładowy kod klasy Logger
z zastosowaniem zasady SRP:
class Logger { public function __construct(private $logFile) { } public function log($message) { $formattedMessage = $this->formatMessage($message); $this->writeToFile($formattedMessage); } private function formatMessage($message) { return date('Y-m-d H:i:s') . ' - ' . $message; } private function writeToFile($message) { file_put_contents($this->logFile, $message . "n", FILE_APPEND); } }
W powyższym kodzie klasa Logger
ma tylko jedną odpowiedzialność – logowanie informacji. Konstruktor klasy przyjmuje nazwę pliku, do którego będą zapisywane logi. Metoda log
przyjmuje wiadomość do zalogowania, formatuje ją, a następnie zapisuje do pliku logu. Wszystkie inne operacje związane z logowaniem, takie jak obsługa wyjątków czy wypisywanie logów na ekran, nie są realizowane w klasie Logger
.
Dzięki zastosowaniu zasady SRP klasa Logger
jest łatwa w utrzymaniu i rozszerzaniu. Jeśli w przyszłości pojawią się dodatkowe wymagania związane z logowaniem, takie jak zapisywanie logów w bazie danych, to możemy stworzyć nową klasę, która będzie realizować te wymagania, nie modyfikując istniejącego kodu klasy Logger
.
[SOLID] OPEN/ CLOSED PRINCIPLE
:: zasada otwarte-zamknięte
Zasada Open/Closed (OCP) w SOLID mówi o tym, że klasy powinny być otwarte na rozszerzenie, ale zamknięte na modyfikację. Innymi słowy, kod powinien być łatwy do rozszerzenia bez wprowadzania zmian w istniejącym kodzie.
Jednym z najczęściej stosowanych sposobów na spełnienie tej zasady jest użycie wzorca projektowego Strategia. Ten wzorzec pozwala zdefiniować rodzinę algorytmów, które są wymienne, ale nie modyfikują istniejącego kodu.
Przykładowo, możemy stworzyć abstrakcyjną klasę PaymentMethod
:
abstract class PaymentMethod { abstract public function pay($amount); } // Następnie, możemy stworzyć konkretne klasy implementujące tę klasę abstrakcyjną: class CreditCardPayment extends PaymentMethod { public function pay($amount) { // logika płatności kartą kredytową } } class PayPalPayment extends PaymentMethod { public function pay($amount) { // logika płatności PayPal } }
Załóżmy, że w przyszłości pojawią się nowe metody płatności, np. płatność przy użyciu kryptowalut. Zamiast modyfikować istniejące klasy, możemy dodać nową klasę implementującą PaymentMethod
:
class CryptoPayment extends PaymentMethod { public function pay($amount) { // logika płatności kryptowalutami } }
W ten sposób spełniamy zasadę OCP, ponieważ dodaliśmy nową funkcjonalność bez modyfikowania istniejącego kodu. Możemy łatwo dodać nowe metody płatności, a nasz istniejący kod pozostanie bez zmian.
[SOLID] LISKOV SUBSTITUTION PRINCIPLE
:: zasada podstawienia Liskov
Zasada Liskov mówi o tym, że obiekt typu potomnego może być używany w miejscu obiektu typu nadrzędnego bez zmiany poprawności programu.
Oto przykład w PHP, który ilustruje zasadę Liskov:
// rodzic class Car { public function startEngine() { echo "Car engine started"; } } // potomek class ElectricCar extends Car { public function startEngine() { throw new Exception("Electric cars don't have engines"); } public function startMotor() { return "Electric car motor started"; } } // funkcja, która przyjmuje obiekt typu Car function driveCar(Car $car) { $car->startEngine(); } // Tworzymy obiekty typu Car i ElectricCar $car = new Car(); $electricCar = new ElectricCar(); // Uruchamiamy funkcję driveCar z oboma obiektami driveCar($car); // "Car engine started" driveCar($electricCar); // rzuci wyjątkiem "Electric cars don't have engines"
W tym przykładzie, zasada Liskov jest spełniona, ponieważ obiekt typu ElectricCar, będący obiektem typu potomnego, może być używany w funkcji driveCar()
, która wymaga obiektu typu Car (typu nadrzędnego), bez zmiany poprawności programu. Jednakże, metoda startEngine()
w klasie ElectricCar rzuca wyjątkiem zamiast uruchamiać silnik, ponieważ samochody elektryczne nie mają silników spalinowych.
[SOLID] INTERFACE SEGREGATION PRINCIPLE
:: zasada segregacji interfejsów
Zasada Interface Segregation Principle (ISP) w SOLID mówi o tym, że interfejsy powinny być cienkie i specyficzne dla potrzeb danego klienta.
Przykładem zastosowania zasady ISP w PHP może być aplikacja, która obsługuje różne rodzaje płatności. Przykładowo takie jak płatność kartą kredytową, płatność przelewem, płatność za pobraniem itp. Każdy rodzaj płatności może wymagać różnych danych wejściowych, takich jak numer karty, nazwa właściciela, data ważności, kwota płatności itp.
Zamiast tworzyć jeden interfejs, który zawierałby wszystkie metody potrzebne dla każdej formy płatności. Można zastosować wiele cieńszych interfejsów specyficznych dla danego typu płatności. Na przykład:
interface CreditCardPaymentInterface { public function makePayment($cardNumber, $expiryDate, $amount); } interface BankTransferPaymentInterface { public function makePayment($accountNumber, $sortCode, $amount); } interface CashOnDeliveryPaymentInterface { public function makePayment($amount); }
Każdy interfejs posiada jedną metodę makePayment
, która jest specyficzna dla danego rodzaju płatności i wymaga tylko niezbędnych danych wejściowych. Dzięki temu każdy klient, który korzysta z tych interfejsów, wie dokładnie, jakie dane są wymagane i jakie operacje musi wykonać, aby dokonać płatności.
Dzięki zastosowaniu zasady ISP, interfejsy są bardziej specyficzne i elastyczne, a klient nie jest zmuszony implementować metod, których nie potrzebuje. Prowadzi to do łatwiejszej modyfikacji kodu i jego utrzymania.
[SOLID] DEPENDENCY INVERSION PRINCIPLE
:: zasada odwrócenia zależności
Zasada Dependency Inversion Principle (DIP) w SOLID mówi o tym, że moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Oba rodzaje modułów powinny zależeć od abstrakcji.
Przykładem zastosowania zasady DIP w PHP może być aplikacja, która wymaga połączenia z bazą danych w celu pobrania danych z niej. Zamiast bezpośredniego łączenia się z bazą danych z poziomu kodu, można zastosować wzorzec projektowy Repository.
Repository jest interfejsem, który definiuje operacje związane z pobieraniem danych, np. findById
, findAll
, findByCriteria
itp. Wszystkie metody w interfejsie zwracają abstrakcyjne dane, takie jak obiekty biznesowe, zamiast bezpośrednio zwracać wynik zapytania do bazy danych.
interface UserRepository { public function findById($id); public function findAll(); public function findByCriteria(array $criteria); } class MySQLUserRepository implements UserRepository { public function __construct(private PDO $db) { } public function findById($id) { // logika pobierająca użytkownika z bazy danych } public function findAll() { // logika pobierająca wszystkich użytkowników z bazy danych } public function findByCriteria(array $criteria) { // logika pobierająca użytkowników spełniających określone kryteria z bazy danych } }
Dzięki zastosowaniu zasady DIP, moduł wysokiego poziomu (np. serwis biznesowy) nie zależy bezpośrednio od modułu niskiego poziomu (np. bazy danych). Zamiast tego, moduł wysokiego poziomu zależy od abstrakcji (np. interfejsu UserRepository
). W tym przypadku, serwis biznesowy korzysta z abstrakcyjnych operacji, takich jak findById
, findAll
, findByCriteria
, które są definiowane w interfejsie UserRepository
. Implementacja operacji znajduje się w module niskiego poziomu (np. klasie MySQLUserRepository
), która implementuje interfejs UserRepository
.
Dzięki temu, gdy zmieni się źródło danych (np. z MySQL na inny typ bazy danych), nie trzeba modyfikować modułu wysokiego poziomu, a jedynie moduł niskiego poziomu (np. klasę MySQLUserRepository
).
Świetny blog! Dziękuję za dzielenie się wiedzą. 🙂
Dziękuję
[…] zabawy obiektami polecam zapoznać się i rozpocząć przygodę z SOLIDem. Dzięki temu Twój kod od samego początku będzie zorientowany na prawidłowe konstrukcje i da Ci […]
[…] substytucji Liskov jest jednym z pięciu zasad SOLID (więcej informacji o pozostałych dobrych praktykach programowania SOLID, znajdziesz tutaj). Mówi ona o tym, że w klasie dziedziczącej powinno być możliwe zastąpienie obiektu klasy […]
[…] (przeczytaj o SOLID w moim osobnym artykule) – SOLID to skrót od pięciu zasad projektowania oprogramowania: Single Responsibility […]
[…] problem wielodziedziczenia w PHP. Oczywiście robiąc to rozsądnie, biorąc pod uwagę zasady SOLID, czy zachowania kompozycji ponad […]
[…] że mamy klasę User, która ma odpowiedzialność za zarządzanie danymi użytkownika (zgodnie z SRP SOLID). Aby zapewnić wysoką spójność, klasa powinna zajmować się tylko operacjami związanymi z […]
Dobrze mordo, wszystko fajnie