Dodanie drugiego operatora płatności miało zająć godzinę, a kończy się poprawkami w kontrolerze, bazie i testach. Zasady SOLID zwykle ograniczają taki efekt, choć nie każda klasa potrzebuje własnego interfejsu i osobnej warstwy.

Projektuj pod zmiany, które naprawdę mogą nadejść
- SRP pomaga ustalić, kto powinien odpowiadać za daną zmianę.
- OCP i LSP chronią rozszerzenia przed naruszaniem działającego kodu.
- ISP i DIP ograniczają zależności do potrzebnych kontraktów.
- Prosta reguła: dodaj abstrakcję, gdy izoluje zmienny szczegół albo ułatwia test, a nie dla samej elegancji.
Pięć liter opisuje decyzje projektowe, nie checklistę
Gdy jedna klasa rozlicza płatność, zapisuje zamówienie i wysyła e-mail, każdy z tych obszarów może wymusić jej zmianę. SOLID porządkuje zależności między takimi elementami, ale nie mówi, że każda metoda musi trafić do osobnej klasy.
Robert C. Martin przedstawił zasady projektowania obiektowego w pracy Design Principles and Design Patterns. Pięć nazw pomaga nazwać konkretne problemy w kodzie:
| Skrót | Pełna nazwa | Pytanie, które warto zadać |
|---|---|---|
| S | Single Responsibility Principle | Czy jedna grupa interesariuszy może wymusić kilka niezależnych zmian w tej klasie? |
| O | Open/Closed Principle | Czy nowe zachowanie można dodać bez ryzykownego ruszania stabilnej logiki? |
| L | Liskov Substitution Principle | Czy implementacja zastępcza zachowuje obietnice kontraktu? |
| I | Interface Segregation Principle | Czy klient zależy tylko od operacji, których rzeczywiście używa? |
| D | Dependency Inversion Principle | Czy logika biznesowa zna szczegóły techniczne, których nie musi znać? |
W nazwach łatwo utknąć, a sens widać dopiero przy zmianie. Uważam, że najlepszym testem SOLID jest pytanie, ile niepowiązanych miejsc trzeba poprawić po zmianie wymagania.
Nowy operator płatności nie powinien przepisywać całego zamówienia
Załóżmy, że aplikacja obsługuje jednego operatora, a teraz ma przyjąć drugiego. Jeśli kod aplikacyjny rozpoznaje nazwy dostawców w wielu miejscach, nowa integracja rozleje się po całym systemie.
Handler zamówienia może koordynować proces, ale szczegóły płatności powinien przekazać kontraktowi, na przykład IPaymentGateway. Implementacje dla poszczególnych operatorów realizują ten sam kontrakt, a rejestracja zależności wybiera właściwą. DIP odwraca kierunek zależności: logika aplikacji zna abstrakcję, a adapter infrastruktury ją implementuje.
W tym układzie pięć zasad wspiera jedną zmianę na różne sposoby. Osobny serwis płatności ogranicza odpowiedzialność handlera (SRP), nową bramkę można dodać jako implementację (OCP), każda bramka powinna dotrzymywać tego samego kontraktu (LSP), wąski interfejs nie zmusza klienta do zależności od nieużywanych metod (ISP), a handler nie tworzy konkretnego klienta HTTP (DIP).
Kontrakt musi opisywać także zachowanie, nie samą sygnaturę metody. Jeśli jeden operator zwraca „zaakceptowano” dopiero po potwierdzeniu, a drugi oznacza tym statusem jedynie przyjęcie żądania, implementacje nie są wymienne, nawet gdy obie zwracają ten sam typ PaymentResult.
Interfejs pomaga, gdy izoluje zmianę albo upraszcza test
Każda abstrakcja ma koszt: dodatkowy typ, konfigurację i miejsce, w którym trzeba szukać przepływu. Wprowadzam ją wtedy, gdy oddziela zmienny szczegół od stabilnej logiki albo pozwala sprawdzić tę logikę bez uruchamiania infrastruktury.
| Sytuacja | Prawdopodobna przyczyna lub skutek | Co zrobić |
|---|---|---|
| Wiele miejsc tworzy klienta konkretnego operatora | Logika aplikacji zależy od infrastruktury | Wydziel kontrakt i adapter, a tworzenie klienta zostaw konfiguracji zależności |
| Klasa implementuje metody, których nie może obsłużyć | Interfejs łączy zbyt wiele ról | Podziel go według potrzeb klientów, na przykład osobno na odczyt i zapis |
| Nowa klasa pochodna zmienia sens wyniku lub zgłasza wyjątek przy poprawnym wywołaniu | Implementacja łamie oczekiwania kontraktu | Popraw kontrakt albo wybierz kompozycję zamiast dziedziczenia |
| Prosty endpoint dostaje kilka interfejsów i klas pośrednich | Abstrakcje wyprzedziły rzeczywistą potrzebę | Zostaw prostą implementację; wydziel granicę dopiero przy zmianie lub trudnym teście |
Przy jednym operatorze i braku testów wymagających podmiany bramki bezpośrednia zależność może być rozsądnym początkiem. Gdy pojawia się druga implementacja albo test procesu wymaga odcięcia sieci, granica zaczyna zwracać koszt swojego utrzymania.
Podobnie jest z OCP: nie trzeba przewidywać każdej przyszłej odmiany rabatu. Jeśli reguły rabatowe regularnie się zmieniają, osobne implementacje strategii pozwalają dodać nową regułę bez rozbudowy długiego łańcucha warunków. Jeśli reguła jest stała i krótka, dodatkowa hierarchia typów może tylko utrudnić czytanie.
Badanie kodu uczenia maszynowego wykazało poprawę rozumienia
Obawa przed nadmiarem warstw ma sens, ale równie słabe jest założenie, że zasady projektowe niczego nie wnoszą. W 2024 roku Raphael Cabral i współautorzy opisali w pracy Investigating the Impact of SOLID Design Principles on Machine Learning Code Understanding kontrolowany eksperyment z udziałem 100 analityków danych w trzech niezależnych próbach.
Uczestnicy analizowali rzeczywisty kod przemysłowego projektu ML w wersji pierwotnej albo przebudowanej z uwzględnieniem SOLID. Autorzy podali statystycznie istotne dowody na poprawę rozumienia kodu w grupie pracującej z wersją zgodną z tymi zasadami, ale streszczenie badania nie podaje wielkości efektu. Wyniku nie należy więc czytać jako obietnicy, że każdy refaktoring SOLID przyspieszy pracę; pokazuje on raczej, że porządek w granicach kodu może pomóc także w projektach ML.
To dobry argument za czytelną strukturą, nie za mechanicznym rozbijaniem klas. W eksperymencie porównano konkretne wersje kodu, a nie każdą możliwą interpretację SOLID.
Jeśli liczba warstw rośnie szybciej niż liczba powodów do zmian
W dyskusjach programistów wraca zastrzeżenie, że SOLID bywa używane jako uzasadnienie nadmiernego rozdrobnienia kodu. Zgadzam się z tym zarzutem wtedy, gdy zespół tworzy interfejsy bez drugiej implementacji, testu wymagającego izolacji czy odrębnej odpowiedzialności.
Przy przeglądzie zmiany stosuję prostą regułę: jeśli nowa klasa oddziela powód do zmiany lub pozwala sprawdzić istotne zachowanie bez zależności zewnętrznej, zostaw ją. Jeśli tylko przenosi wywołanie o jeden poziom niżej i nie upraszcza ani zmiany, ani testu, usuń ją lub poczekaj na rzeczywistą potrzebę.
Wróćmy do operatora płatności. Celem nie jest uzyskanie maksymalnej liczby interfejsów, lecz doprowadzenie do sytuacji, w której nowa integracja nie wymaga poprawiania logiki zamówienia, a testy mogą sprawdzić jej zachowanie bez prawdziwego połączenia z dostawcą. Tak rozumiane solidne programowanie mierzy jakość granic po koszcie następnej zmiany.
