SOLID w praktyce. Jak projektować kod łatwiejszy w rozwoju

Kod źródłowy w języku Java, pokazujący tworzenie i uruchamianie wątków. To przykład solidnego programowania, gdzie każdy element ma swoje zadanie.

Napisano przez

Alex Jabłoński

Opublikowano

26 wrz 2026

Spis treści

Gdy aplikacja zaczyna rosnąć, problemem rzadko jest sam brak nowych funkcji. Znacznie częściej przeszkadza kod, w którym jedna zmiana uruchamia lawinę poprawek w wielu miejscach. Za takim podejściem stoi solid programowanie, czyli świadome projektowanie modułów, zależności i interfejsów tak, aby system był łatwiejszy w utrzymaniu, testowaniu i rozwijaniu.

Pięć zasad pomaga ograniczyć koszt zmian w rozwijanej aplikacji

  • SRP ogranicza liczbę powodów, dla których moduł trzeba zmienić.
  • OCP ułatwia dodawanie funkcji bez modyfikowania stabilnego kodu.
  • LSP i ISP chronią przed niebezpiecznym dziedziczeniem oraz zbyt szerokimi interfejsami.
  • DIP oddziela logikę biznesową od bazy danych, frameworka i zewnętrznych usług.
  • Wzorce projektowe mają sens dopiero wtedy, gdy rozwiązują konkretny problem, a nie zdobią kod.

Zasady SOLID: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion. Klucz do solidnego programowania.

Dlaczego zasady SOLID mają znaczenie w praktyce

Zasady SOLID nie są frameworkiem ani checklistą, którą trzeba mechanicznie odhaczyć. To zestaw wskazówek pomagających zarządzać złożonością kodu, szczególnie gdy nad projektem pracuje kilka osób i wymagania regularnie się zmieniają.

Największą korzyść widać przy zmianach. Jeżeli dodanie nowego sposobu płatności wymaga edycji kontrolera, serwisu, encji, konfiguracji i kilku testów, prawdopodobnie zależności są zbyt ciasne. Dobrze zaprojektowany moduł pozwala zmienić jeden fragment bez niepotrzebnego naruszania reszty systemu.

W praktyce patrzę na SOLID przez dwa pojęcia. Spójność oznacza, że elementy modułu należą do siebie logicznie, a sprzężenie opisuje liczbę zależności między modułami. Dobry projekt zwiększa pierwszą wartość i ogranicza drugą, ale nie próbuje wyeliminować wszystkich zależności, bo bez nich aplikacja nie mogłaby działać.

To ważne także w małych aplikacjach webowych. Nie każda strona potrzebuje rozbudowanej architektury heksagonalnej, lecz nawet prosty endpoint z osobną walidacją, logiką biznesową i zapisem danych łatwiej testować niż pojedynczą funkcję wykonującą wszystkie te zadania naraz.

Pięć zasad i problemy, które pomagają rozwiązać

Zasada Najprostsze znaczenie Typowy sygnał ostrzegawczy
SRP Moduł powinien mieć jeden powód do zmiany. Jedna klasa waliduje, zapisuje dane i wysyła wiadomości.
OCP Element powinien dać się rozszerzać bez ciągłej edycji stabilnego kodu. Długi łańcuch if-else dla kolejnych wariantów.
LSP Zastąpienie typu bazowego podtypem nie powinno psuć działania programu. Podklasa rzuca wyjątek w metodzie, którą odziedziczyła.
ISP Klient nie powinien zależeć od metod, których nie używa. Interfejs ma kilkanaście metod, choć implementacja potrzebuje dwóch.
DIP Logika biznesowa powinna zależeć od abstrakcji, nie od szczegółów technicznych. Serwis biznesowy sam tworzy klienta HTTP albo połączenie z bazą.

SRP, czyli zasada pojedynczej odpowiedzialności, nie oznacza, że każda klasa ma mieć dokładnie jedną metodę. Chodzi o jeden obszar odpowiedzialności i jeden główny powód do zmiany. Klasa zamówienia może pilnować reguł zamówienia, ale generowanie PDF-a i wysyłkę e-maila lepiej przekazać innym komponentom.

class OrderService {
  constructor(
    private pricing: PricingService,
    private orders: OrderRepository,
    private notifier: NotificationService
  ) {}

  async place(order: Order) {
    const total = this.pricing.calculate(order);
    const saved = await this.orders.save({ ...order, total });
    await this.notifier.sendConfirmation(saved);
    return saved;
  }
}

OCP dobrze widać na przykładzie płatności lub naliczania rabatów. Zamiast dopisywać kolejne warunki do jednej funkcji, można wprowadzić osobne strategie implementujące wspólny kontrakt. Dzięki temu nowy wariant trafia do nowej klasy, a działający kod pozostaje stabilniejszy.

LSP przypomina, że dziedziczenie to zobowiązanie, a nie tylko wygodny sposób ponownego użycia kodu. Jeżeli metoda przyjmuje typ bazowy, każda jego implementacja powinna zachowywać oczekiwane reguły. Gdy podklasa zmienia znaczenie metod albo odmawia wykonania podstawowej operacji, lepszym rozwiązaniem bywa kompozycja lub osobny interfejs.

ISP chroni przed tak zwanymi tłustymi interfejsami. Moduł wysyłający powiadomienie SMS nie powinien implementować metod dotyczących raportów, eksportu i zarządzania użytkownikami tylko dlatego, że wszystkie umieszczono w jednym interfejsie. Mniejsze kontrakty są prostsze do testowania i łatwiej je zastąpić atrapą w teście.

DIP jest zasadą, która spina pozostałe reguły. Serwis obsługujący rejestrację użytkownika nie powinien wiedzieć, czy dane trafiają do PostgreSQL, pliku czy zewnętrznego API. Powinien korzystać z abstrakcji, a konkretną implementację można podłączyć przez wstrzykiwanie zależności.

Jak stosować SOLID bez budowania nadmiarowej architektury

Najrozsądniej zacząć od miejsca, które już sprawia problemy. Szukam klas z dużą liczbą zależności, metod zawierających wiele warunków oraz kodu, który trudno przetestować bez uruchamiania całej aplikacji. To zwykle lepszy punkt wyjścia niż przebudowa projektu tylko dlatego, że nowa architektura wygląda atrakcyjnie.

  1. Znajdź częstą zmianę. Może to być nowy operator płatności, sposób logowania albo format raportu.
  2. Wskaż szczegóły techniczne. Oddziel bazę danych, klienta HTTP, system plików i framework od logiki biznesowej.
  3. Wydziel mały kontrakt. Interfejs powinien opisywać potrzebę biznesową, a nie wszystkie możliwości konkretnej biblioteki.
  4. Przenieś decyzję w jedno miejsce. Wybór strategii, adaptera lub implementacji nie powinien być powielany w wielu klasach.
  5. Dodaj test po zmianie. Jeżeli refaktoryzacja nie poprawia testowalności albo czytelności, jej sens może być niewielki.

Przykładowo, zamiast uzależniać logikę zamówienia od konkretnego klienta Stripe, można zdefiniować kontrakt PaymentGateway. Implementacja Stripe będzie adapterem, a test może użyć prostego fałszywego obiektu. Zyskujemy niezależność od dostawcy, choć płacimy za nią dodatkowym kodem i koniecznością utrzymania abstrakcji.

interface PaymentGateway {
  charge(amount: number, currency: string): Promise;
}

class CheckoutService {
  constructor(private gateway: PaymentGateway) {}

  pay(amount: number) {
    return this.gateway.charge(amount, "PLN");
  }
}

Nie tworzę interfejsu dla każdej klasy tylko po to, aby projekt wyglądał bardziej profesjonalnie. Dla prostego komponentu używanego w jednym miejscu dodatkowa warstwa może utrudnić zrozumienie kodu. Abstrakcja powinna odpowiadać na realną potrzebę zmiany, testowania lub wymiany technologii.

Wzorce projektowe, które wspierają te zasady

SOLID opisuje kierunek projektowania, natomiast wzorce projektowe pokazują sprawdzone sposoby organizacji kodu. Nie są gotowymi klockami do użycia w każdej aplikacji. Ten sam wzorzec może pomóc w jednym module i niepotrzebnie skomplikować inny.

Strategy pasuje do OCP, gdy istnieje kilka wymiennych sposobów wykonania operacji. Przykładem może być obliczanie ceny dostawy dla paczkomatu, kuriera i odbioru osobistego. Każda strategia ma własne reguły, a serwis zamówienia nie musi znać szczegółów każdej z nich.

Adapter pomaga realizować DIP. Tłumaczy interfejs zewnętrznej biblioteki na kontrakt, którego potrzebuje aplikacja. Dzięki temu zmiana dostawcy płatności albo systemu wysyłki nie rozlewa się po całej warstwie biznesowej.

Decorator pozwala dodawać zachowanie bez modyfikowania podstawowej implementacji. Można nim opakować repozytorium logowaniem, cache’em albo pomiarem czasu. Trzeba jednak kontrolować liczbę dekoratorów, bo zbyt wiele warstw utrudnia śledzenie przepływu.

Factory centralizuje tworzenie obiektów, kiedy wybór implementacji zależy od konfiguracji lub danych wejściowych. Sama fabryka nie naprawi złego projektu, ale może ograniczyć sytuacje, w których kod biznesowy zna szczegóły konstrukcji wielu zależności.

W aplikacjach webowych zasady te często prowadzą do architektury warstwowej albo heksagonalnej. W pierwszej podejście rozdziela na przykład kontrolery, logikę aplikacyjną i dostęp do danych. W drugiej centrum stanowi domena, a komunikacja ze światem zewnętrznym odbywa się przez porty i adaptery. Żaden z tych modeli nie jest automatycznie lepszy. Wybór zależy od rozmiaru projektu, liczby integracji i tempa zmian.

Najczęstsze błędy i rozsądne kompromisy

Pierwszym błędem jest traktowanie SOLID jak zestawu sztywnych zakazów. Kod może naruszać pojedynczą zasadę, jeżeli dzięki temu pozostaje prostszy, a moduł jest mały i stabilny. Problem pojawia się wtedy, gdy wyjątek staje się stałym sposobem pracy i zaczyna zwiększać koszt każdej kolejnej zmiany.

Drugą pułapką jest tworzenie abstrakcji zbyt wcześnie. Interfejs IUserService, jedna implementacja i brak planowanej wymiany technologii często nie dają realnej korzyści. Najpierw obserwuję, co rzeczywiście się zmienia, a dopiero potem wydzielam granicę, która ma chronić stabilny kod.

Niebezpieczny bywa także kult małych klas. Rozbicie jednej prostej operacji na siedem obiektów nie musi oznaczać lepszego projektu. Jeżeli programista musi przeskakiwać między wieloma plikami, aby zrozumieć prostą regułę, czytelność mogła ucierpieć bardziej niż zyskała modularność.

Warto uważać również na pozorne OCP. Długi system fabryk, konfiguracji i rejestrów może wyglądać elastycznie, ale w praktyce każda nowa funkcja nadal wymaga zmian w kilku miejscach. Rozszerzalność ma sens wtedy, gdy nowy przypadek można dodać przewidywalnie i bez naruszania istniejących reguł.

Najlepszym sprawdzianem jest praca zespołu. Jeżeli nowa osoba potrafi znaleźć miejsce dla kolejnej funkcji, testy obejmują logikę bez uruchamiania całej infrastruktury, a zmiana integracji nie wymaga przepisywania domeny, projekt prawdopodobnie korzysta z SOLID w zdrowy sposób.

Od zasad do kodu, który nie przeszkadza w rozwoju

Najważniejsza lekcja jest prosta. SOLID nie ma sprawić, że kod będzie wyglądał idealnie, tylko że kolejna zmiana będzie przewidywalna. Zaczynam od odpowiedzialności, ograniczam niepotrzebne zależności, wybieram małe kontrakty i dopiero wtedy sięgam po wzorzec, który rozwiązuje konkretny problem.

Jeżeli aplikacja jest mała, wystarczy kilka czytelnych modułów i dobre testy. Gdy rośnie liczba funkcji, integracji oraz osób pracujących nad projektem, świadome granice zaczynają oszczędzać czas całego zespołu. To właśnie wtedy zasady projektowania przestają być teorią, a stają się praktycznym narzędziem utrzymania skalowalnego oprogramowania.

FAQ - Najczęstsze pytania

SRP oznacza, że moduł powinien mieć jeden główny powód do zmiany. Serwis zamówień może obliczać cenę, zapisywać zamówienie i zlecać wysyłkę potwierdzenia, ale generowanie PDF-a oraz wysyłkę e-maila warto przekazać osobnym komponentom.

Logika zamówienia powinna korzystać z abstrakcji, na przykład interfejsu PaymentGateway, zamiast bezpośrednio tworzyć klienta Stripe. Implementacja Stripe pełni wtedy rolę adaptera, a w testach można użyć prostego fałszywego obiektu.

Strategy sprawdza się, gdy operacja ma kilka wymiennych wariantów, na przykład różne sposoby naliczania rabatu lub ceny dostawy. Każdy wariant otrzymuje osobną implementację wspólnego kontraktu, dzięki czemu dodanie nowej strategii nie wymaga rozbudowy długiego łańcucha if-else.

Najpierw warto znaleźć częste zmiany, klasy z wieloma zależnościami i kod trudny do testowania. Następnie można oddzielić szczegóły techniczne, wydzielić mały kontrakt i dodać test. Interfejsu nie trzeba tworzyć dla każdej klasy, jeśli komponent jest prosty, stabilny i używany tylko w jednym miejscu.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi:

interfejsy dziedziczenie refaktoryzacja solid testowalność

Udostępnij artykuł

Alex Jabłoński

Alex Jabłoński

Nazywam się Alex Jabłoński i od 14 lat zajmuję się programowaniem webowym. Moja przygoda z tą dziedziną zaczęła się od prostych projektów, które stopniowo przerodziły się w pasję do tworzenia złożonych aplikacji internetowych. Interesuje mnie nie tylko kodowanie, ale także dzielenie się wiedzą, co skłoniło mnie do pisania artykułów, które pomagają innym zrozumieć zasady programowania. W swoich tekstach koncentruję się na podstawach, ale także na bardziej zaawansowanych zagadnieniach, starając się w przystępny sposób wyjaśnić trudne tematy. Przy pisaniu zwracam szczególną uwagę na aktualność informacji oraz ich zrozumiałość. Regularnie śledzę nowinki w branży, porównuję różne podejścia i staram się uprościć skomplikowane koncepcje, aby były dostępne dla każdego, niezależnie od poziomu zaawansowania. Moim celem jest dostarczenie wartościowych treści, które pomogą czytelnikom w ich drodze do kariery w programowaniu webowym.

Napisz komentarz