Gdy pojedynczy serwis zaczyna jednocześnie obsługiwać płatności, zapis do bazy i wysyłkę wiadomości, każda kolejna zmiana może uruchamiać lawinę poprawek. Właśnie wtedy SOLID w IT przestaje być teorią z rozmowy rekrutacyjnej, a staje się praktycznym sposobem porządkowania kodu, projektowania architektury i ograniczania ryzyka. Pokażę, jak rozumieć pięć zasad, gdzie pomagają najbardziej i kiedy ich stosowanie zaczyna tworzyć niepotrzebną złożoność.
Pięć zasad, które pomagają budować kod gotowy na zmiany
- SOLID wspiera czytelność, testowalność i łatwiejsze rozwijanie aplikacji.
- Jedna odpowiedzialność oznacza, że klasa powinna mieć jeden główny powód do zmiany.
- Małe interfejsy i abstrakcje ograniczają zależności między modułami.
- Wstrzykiwanie zależności ułatwia testowanie oraz wymianę implementacji.
- Nie każda klasa potrzebuje interfejsu, a zasady SOLID nie zastępują zdrowego rozsądku.

Dlaczego SOLID ma znaczenie w aplikacjach webowych
Najprościej mówiąc, SOLID to pięć zasad projektowania obiektowego, które pomagają ograniczyć sprzężenie, czyli liczbę bezpośrednich zależności między elementami systemu. Im mniej modułów musi znać szczegóły innych modułów, tym łatwiej zmienić bazę danych, dostawcę płatności czy sposób wysyłania powiadomień.
Nie chodzi jednak o tworzenie maksymalnie wielu klas i interfejsów. Dobrze zaprojektowany kod powinien być łatwy do zmiany, ale nie przeładowany abstrakcjami, których nikt jeszcze nie potrzebuje. W małym skrypcie jednorazowym rozbudowana architektura może tylko spowolnić pracę. W rozwijanym sklepie internetowym, panelu administracyjnym albo API różnica staje się odczuwalna już po kilku miesiącach.
| Zasada | Główne pytanie | Praktyczny efekt |
|---|---|---|
| Single Responsibility | Czy ta klasa ma jeden powód do zmiany? | Mniejsze klasy i prostsze testy |
| Open/Closed | Czy nowe zachowanie dodam bez naruszania stabilnego kodu? | Bezpieczniejsze rozszerzanie funkcji |
| Liskov Substitution | Czy implementację można zastąpić inną bez łamania działania programu? | Przewidywalne polimorfizm i integracje |
| Interface Segregation | Czy klient zależy tylko od metod, których potrzebuje? | Mniej pustych metod i przypadkowych zależności |
| Dependency Inversion | Czy logika biznesowa zależy od abstrakcji, a nie od szczegółu technicznego? | Łatwiejsze testowanie i wymiana komponentów |
W praktyce nie traktuję tych zasad jak listy kontrolnej do odhaczania przy każdej metodzie. Są raczej zestawem pytań, które pomagają znaleźć miejsca najbardziej podatne na zmiany i błędy.
Single Responsibility porządkuje odpowiedzialność
Zasada pojedynczej odpowiedzialności mówi, że klasa powinna mieć jeden główny powód do zmiany. Nie oznacza to, że może wykonywać tylko jedną instrukcję. Chodzi o spójność odpowiedzialności, a nie o mechaniczne dzielenie kodu na najmniejsze możliwe fragmenty.
Przykładem problematycznej klasy jest OrderService, która oblicza cenę zamówienia, zapisuje dane do bazy, generuje fakturę i wysyła e-mail. Każda zmiana w podatkach, formacie dokumentu lub dostawcy poczty wymusza modyfikację tego samego miejsca. Z czasem klasa staje się trudna do testowania, bo aby sprawdzić kalkulację ceny, trzeba przygotować także bazę danych i system wysyłki.
Lepszy podział może wyglądać tak:
- OrderCalculator odpowiada za obliczenia biznesowe.
- OrderRepository zapisuje i odczytuje zamówienia.
- InvoiceGenerator tworzy dokument sprzedaży.
- NotificationSender wysyła komunikaty do klienta.
Takie rozdzielenie nie oznacza automatycznie dobrej architektury. Jeśli każda z tych klas ma po kilka metod, a przepływ danych pozostaje prosty, podział jest sensowny. Jeżeli natomiast powstaje kilkanaście sztucznych obiektów przekazujących sobie po jednej wartości, kod staje się bardziej formalny niż użyteczny.
Najlepszy test SRP jest praktyczny. Zadaję sobie pytanie, czy zmianę sposobu wysyłki e-maila mogę wykonać bez dotykania kalkulatora zamówienia. Jeśli odpowiedź brzmi „nie”, odpowiedzialności prawdopodobnie zostały połączone zbyt mocno.
Open/Closed pozwala rozszerzać działanie bez lawiny zmian
Zasada otwarte-zamknięte mówi, że moduł powinien być otwarty na rozszerzanie, ale zamknięty na ciągłe modyfikowanie stabilnego kodu. Nie jest to zakaz poprawiania istniejących klas. Chodzi o to, aby nowe warianty funkcji nie wymagały rozbudowywania jednego warunkowego potwora.
Typowy przykład pojawia się przy naliczaniu rabatów. Początkowo kod zawiera warunek dla klienta standardowego i firmowego. Później dochodzą promocje sezonowe, kupony, program lojalnościowy oraz rabat dla nowych użytkowników. Kolejne instrukcje if mogą sprawić, że jedna metoda zna wszystkie reguły biznesowe i staje się ryzykowna w modyfikacji.
Rozsądniejszym rozwiązaniem jest wydzielenie strategii rabatowych. Każda strategia implementuje tę samą operację, na przykład obliczenie obniżki, a serwis zamówień korzysta z wybranej implementacji. Dodanie nowej promocji polega wtedy na utworzeniu kolejnej klasy, a nie na przebudowie całej logiki.
Ta zasada dobrze współpracuje ze wzorcem strategii, który pozwala wymieniać algorytmy bez zmiany kodu korzystającego z nich. Trzeba jednak zachować umiar. Jeśli aplikacja ma tylko jeden sposób naliczania ceny i nie ma realnego sygnału, że pojawią się kolejne, tworzenie pięciu abstrakcji na zapas będzie przedwczesnym projektowaniem.
W mojej praktyce OCP najbardziej pomaga tam, gdzie zmienność jest przewidywalna. Dotyczy to na przykład metod płatności, formatów eksportu, reguł rabatowych i integracji z zewnętrznymi usługami. Nie ma sensu budować rozszerzalnego mechanizmu dla fragmentu, który od dwóch lat pozostaje niezmienny.
Liskov Substitution chroni zachowanie klas i implementacji
Zasada podstawienia Liskov mówi, że obiekt typu pochodnego powinien dać się użyć w miejscu obiektu bazowego bez łamania oczekiwań kodu. Sama zgodność typów nie wystarcza. Ważne jest również zachowanie metod, obsługa błędów i warunki, które klient może zakładać.
Wyobraźmy sobie interfejs PaymentGateway z metodą płatności. Jedna implementacja zwraca identyfikator transakcji, druga rzuca wyjątek, gdy płatność się nie udała, a trzecia po cichu zwraca pustą wartość i uznaje operację za zakończoną. Wszystkie mogą technicznie implementować ten sam interfejs, ale nie są równoważnymi zamiennikami.
Naruszenie LSP często wychodzi dopiero w testach integracyjnych. Kod działa z implementacją domyślną, lecz po podmianie na atrapę testową albo innego dostawcę zaczyna ignorować błędy, zwracać niepoprawne dane lub wymagać specjalnych wyjątków.
Przed utworzeniem hierarchii klas sprawdzam trzy rzeczy:
- czy implementacja spełnia te same warunki wejściowe,
- czy zwraca wynik zgodny z kontraktem,
- czy błędy są sygnalizowane w przewidywalny sposób.
Jeśli jedna implementacja nie potrafi spełnić kontraktu, lepszym rozwiązaniem bywa mniejszy i bardziej precyzyjny interfejs niż wymuszanie dziedziczenia. To prowadzi bezpośrednio do kolejnej zasady.
Interface Segregation ogranicza niepotrzebne zależności
Zasada segregacji interfejsów sugeruje, aby klient nie był zmuszany do zależności od metod, których nie używa. Jeden szeroki interfejs często wygląda wygodnie na początku, ale szybko zaczyna wiązać ze sobą niezależne obszary aplikacji.
Załóżmy, że interfejs ReportService zawiera generowanie PDF, eksport CSV, wysyłkę e-maila i archiwizację dokumentu. Klasa odpowiedzialna wyłącznie za eksport CSV musi znać cały kontrakt, nawet jeśli trzy pozostałe metody są jej obojętne. W niektórych językach kończy się to pustymi implementacjami albo wyjątkami typu „ta operacja nie jest obsługiwana”.
Lepszy podział obejmuje osobne kontrakty, takie jak ReportExporter, ReportMailer i ReportArchiver. Dzięki temu moduł korzysta tylko z tego, czego rzeczywiście potrzebuje, a test zastępczy jest mały i czytelny.
Małe interfejsy nie są jednak wartością samą w sobie. Jeśli dwa komponenty zawsze zmieniają się razem i mają wspólnego właściciela biznesowego, sztuczne rozbijanie ich na wiele kontraktów może utrudnić orientację. Najlepiej dzielić interfejsy według potrzeb klienta, a nie według liczby metod.
Dependency Inversion przenosi ciężar z technologii na logikę
Zasada odwrócenia zależności mówi, że logika wysokiego poziomu nie powinna zależeć bezpośrednio od szczegółów technicznych. Obie warstwy powinny zależeć od abstrakcji, a szczegóły powinny być do nich dopasowane.
W aplikacji webowej kontroler lub serwis zamówień nie powinien sam tworzyć klienta konkretnej bramki płatności, otwierać połączenia z bazą i wybierać biblioteki do wysyłki wiadomości. Zamiast tego korzysta z kontraktu, na przykład PaymentGateway, a konkretna implementacja jest dostarczana z zewnątrz.
Najczęściej wykorzystuje się do tego dependency injection, czyli wstrzykiwanie zależności. Komponent otrzymuje potrzebne obiekty w konstruktorze lub innym mechanizmie konfiguracji, zamiast tworzyć je we własnym zakresie.
Daje to trzy konkretne korzyści:
- test może użyć prostej implementacji pamięciowej zamiast prawdziwej bramki płatności,
- zmiana dostawcy usługi nie wymaga przebudowy logiki biznesowej,
- zależności klasy są widoczne od razu, bez szukania ich w środku metod.
W tym miejscu łatwo przesadzić. Interfejs dla każdej klasy, nawet dla prostego obiektu bez planowanej wymiany, często tylko zwiększa liczbę plików. Tworzę abstrakcję wtedy, gdy istnieje granica biznesowa, techniczna albo testowa, a nie dlatego, że wzorzec wymaga kolejnego interfejsu.
Jak przełożyć zasady SOLID na architekturę projektu
Pięć zasad działa najlepiej, gdy wspiera je sensowny podział aplikacji. W typowym API można oddzielić warstwę prezentacji, logikę aplikacji, domenę i infrastrukturę. Kontroler przyjmuje żądanie, serwis koordynuje przypadek użycia, a repozytorium lub klient zewnętrznej usługi zajmuje się szczegółem technicznym.
Nie jest konieczne wdrażanie pełnej architektury heksagonalnej w każdej aplikacji. Jej główna idea jest jednak bardzo praktyczna: rdzeń systemu nie powinien być zakładnikiem frameworka, bazy danych ani jednego dostawcy API. Dzięki portom i adapterom szczegóły można wymieniać bez naruszania reguł biznesowych.
| Problem w projekcie | Możliwa reakcja zgodna z SOLID |
|---|---|
| Kontroler zawiera walidację, obliczenia i zapis do bazy | Rozdzielenie odpowiedzialności między warstwę wejścia, przypadek użycia i repozytorium |
| Jedna klasa zna kilku dostawców zewnętrznych | Wprowadzenie małych kontraktów i wstrzykiwanie implementacji |
| Każda nowa opcja wymaga edycji dużego warunku | Strategie lub osobne handlery dla zmiennych zachowań |
| Test wymaga uruchomionej bazy i zewnętrznego API | Oddzielenie logiki biznesowej od infrastruktury |
W aplikacjach webowych szczególnie dobrze sprawdza się podejście polegające na projektowaniu wokół przypadków użycia. Zamiast organizować cały kod wyłącznie według typów plików, można myśleć kategoriami „utwórz zamówienie”, „anuluj subskrypcję” albo „wygeneruj raport”. Taki układ ułatwia znalezienie logiki i pokazuje, które zależności naprawdę są potrzebne.
Najczęstsze błędy przy stosowaniu SOLID
Pierwszym błędem jest traktowanie zasad jak sztywnych przepisów. Kod może spełniać formalne reguły, a mimo to być trudny do zrozumienia. Dziesięć interfejsów, trzy fabryki i kilka warstw pośrednich nie poprawi projektu, jeśli problem można rozwiązać jedną spójną klasą.
Drugim błędem jest przedwczesna abstrakcja. Programista przewiduje pięć przyszłych implementacji, choć obecnie istnieje jedna i nie ma żadnego sygnału, że pojawią się kolejne. Taki kod płaci cenę za hipotetyczną elastyczność, która może nigdy nie być potrzebna.
Trzeci problem to mylenie SRP z zasadą „jedna metoda na klasę”. Klasa może zawierać kilka operacji, jeśli należą do tego samego obszaru odpowiedzialności. Liczy się spójność zmian, a nie liczba metod lub plików.
Najbezpieczniej wdrażać SOLID stopniowo:
- znajdź kod, który często powoduje błędy albo wymaga zmian w wielu miejscach,
- zidentyfikuj konkretną odpowiedzialność lub zależność tworzącą problem,
- wydziel najmniejszą potrzebną abstrakcję,
- dodaj test chroniący zachowanie,
- sprawdź, czy nowa struktura faktycznie uprościła kolejną zmianę.
Jeżeli po refaktoryzacji kod jest dłuższy, ale łatwiej go testować i modyfikować, to może być dobry kompromis. Jeżeli jest tylko dłuższy i bardziej rozproszony, prawdopodobnie zasada została zastosowana mechanicznie.
Jak rozpoznać, że architektura jest wystarczająco dobra
Nie istnieje jeden wynik, który potwierdzi poprawne użycie SOLID. Najlepszym sprawdzianem jest zwykła praca nad funkcją. Dodanie nowej reguły biznesowej nie powinno wymagać rozumienia całej aplikacji, a test jednostkowy nie powinien uruchamiać połowy infrastruktury.
Przed zaakceptowaniem projektu sprawdzam, czy mogę odpowiedzieć na kilka prostych pytań. Czy wiem, gdzie zmienić sposób zapisu danych? Czy potrafię zastąpić usługę zewnętrzną atrapą? Czy jedna zmiana biznesowa dotyka kilku niezależnych modułów? Czy nazwy klas opisują ich rolę, a nie techniczne szczegóły?
Jeśli odpowiedzi są jasne, prawdopodobnie architektura spełnia swój cel. Dobry kod nie musi wyglądać efektownie. Ma przede wszystkim pozwalać zespołowi bezpiecznie rozwijać produkt, rozumieć konsekwencje zmian i szybko wykrywać błędy.
Od pięciu liter do spokojniejszego rozwoju aplikacji
SOLID pomaga uporządkować sposób myślenia o kodzie. SRP pilnuje odpowiedzialności, OCP ogranicza ryzykowne modyfikacje, LSP sprawdza poprawność zamienników, ISP zmniejsza niepotrzebne kontrakty, a DIP oddziela logikę biznesową od technologicznych szczegółów.
Największą korzyść przynosi stosowanie tych zasad tam, gdzie aplikacja naprawdę się zmienia. Zacząłbym od jednego problematycznego modułu, dodał testy i dopiero potem rozszerzał refaktoryzację. Wtedy SOLID nie staje się ozdobnikiem architektury, lecz praktycznym narzędziem, które pomaga pisać kod czytelniejszy, testowalny i odporny na rozsądnie przewidywane zmiany.