Reguły biznesowe szybko puchną, gdy filtrujesz dane, sprawdzasz uprawnienia albo walidujesz złożone warunki w kilku miejscach aplikacji. Wtedy łatwo skończyć z długimi łańcuchami ifów, które trudno testować i jeszcze trudniej rozwijać. Jednym z narzędzi, które porządkuje ten chaos, jest specification pattern: wzorzec, w którym warunek staje się osobnym obiektem i można go bezpiecznie łączyć z innymi regułami.
Najważniejsze rzeczy do zapamiętania o wzorcu specyfikacji
- Oddziela regułę od obiektu sprawdzanego, więc logika nie rozlewa się po kontrolerach, serwisach i repozytoriach.
- Najbardziej pomaga tam, gdzie ta sama reguła jest używana w kilku miejscach: przy filtrowaniu, walidacji i operacjach masowych.
- Łączy się logicznie przez And, Or i Not, dzięki czemu z prostych warunków można budować czytelniejsze reguły złożone.
- Nie jest lekiem na wszystko - przy jednym prostym warunku zwykły if albo prywatna metoda będą lepsze.
- W DDD najlepiej działa blisko modelu domenowego, ale bez mieszania go z technicznymi detalami ORM.
Czym jest wzorzec specyfikacji i co porządkuje
W najprostszej wersji chodzi o to, żeby warunek biznesowy żył jako osobny obiekt. Zamiast trzymać logikę w stylu „jeśli klient ma status X, a zamówienie jest starsze niż Y, a rabat nie przekracza Z” w losowych miejscach systemu, wyciągam tę regułę do klasy albo modułu, który ma jedno zadanie: odpowiedzieć, czy dany obiekt spełnia określone kryteria.
To rozdzielenie brzmi skromnie, ale daje duży efekt. Ta sama specyfikacja może być użyta do filtrowania kolekcji, sprawdzania pojedynczego obiektu, a czasem także do budowania nowych danych, które mają spełnić konkretną regułę. W praktyce webowej najczęściej widzę trzy scenariusze: listy wyników, walidację reguł domenowych i decyzje typu „czy ten rekord może przejść dalej w procesie”.
Ja patrzę na ten wzorzec przede wszystkim jak na sposób opisania biznesu językiem kodu. Jeśli reguła ma nazwę, znaczenie i potencjał do ponownego użycia, lepiej, żeby była nazwana wprost, a nie ukryta w połowie metody. Dzięki temu kod staje się bardziej rozmowny dla zespołu, a mniej zależny od pamięci konkretnej osoby. To prowadzi naturalnie do pytania, jak taki obiekt zbudować, żeby nie przesadzić z abstrakcją.

Jak zbudować dobrą implementację w kodzie
Najprostszy wariant ma trzy elementy: interfejs specyfikacji, konkretne reguły i możliwość ich łączenia. W praktyce wystarcza mi kontrakt w stylu „czy ten obiekt spełnia warunek?”, a potem osobne klasy dla warunków takich jak aktywny klient, zamówienie powyżej progu albo produkt objęty promocją.
Interfejs i pojedyncza reguła
Kluczowe jest to, żeby pojedyncza specyfikacja była mała i jednoznaczna. Jedna klasa, jedna decyzja. Jeśli nazwa specyfikacji zaczyna brzmieć jak mini-raport, zwykle oznacza to, że rozbiłem ją za mało albo próbuję wcisnąć zbyt wiele odpowiedzialności w jedno miejsce.
interface Specification {
isSatisfiedBy(candidate: T): boolean
} Łączenie warunków
Największa wartość pojawia się wtedy, gdy mogę składać reguły bez duplikacji. Typowe operacje to And, Or i Not. Dzięki temu zamiast pisać trzy podobne ify w trzech różnych miejscach, tworzę jedną bazową regułę i łączę ją tam, gdzie naprawdę jest potrzebna. W dobrze uporządkowanym kodzie reguła „aktywny klient” może być użyta zarówno przy filtrze listy, jak i przy decyzji o przyznaniu rabatu.
Przeczytaj również: SOLID w aplikacjach webowych - Czy naprawdę go potrzebujesz?
Gdy reguła ma trafić do bazy
Jeżeli specyfikacja ma filtrować dane w bazie, a nie tylko w pamięci, warto myśleć o niej jak o wyrażeniu, które da się przetłumaczyć na zapytanie. To ważne, bo zwykłe uruchamianie logiki w pamięci na dużych zbiorach danych bywa kosztowne. W aplikacjach webowych najczęściej chcę mieć jednocześnie czytelny model domenowy i sensowną wydajność, więc wzorzec musi współpracować z warstwą zapytań, a nie ją zastępować.
Taki podział daje czysty kompromis: domena mówi, co znaczy spełnienie warunku, a warstwa danych decyduje, jak go wykonać. To prowadzi do kolejnego pytania: gdzie ten wzorzec daje realną przewagę, a gdzie jest tylko eleganckim dodatkiem bez zwrotu z inwestycji.
Gdzie ten wzorzec daje największą wartość
Najlepiej sprawdza się tam, gdzie warunki biznesowe są stabilne, ale używane w kilku miejscach naraz. Wtedy koszt wydzielenia specyfikacji szybko się zwraca, bo każda zmiana reguły trafia do jednego miejsca, zamiast rozlewać się po kodzie.
| Scenariusz | Dlaczego specyfikacja pomaga | Na co uważać |
|---|---|---|
| Filtrowanie ofert w panelu admina | Jedna reguła może obsłużyć listę, eksport i API bez kopiowania warunków. | Nie mieszaj zbyt wcześnie logiki UI z logiką domenową. |
| Walidacja istniejących encji | Możesz sprawdzać obiekty, które już żyją w systemie, bez tworzenia osobnych procedur. | Nie używaj tego do „ratowania” obiektów, które od początku są niepoprawnie zbudowane. |
| Reguły promocji i rabatów | Promocje lubią rosnąć, a specyfikacje dobrze znoszą składanie warunków. | Jeśli reguły zmieniają się co tydzień, pilnuj prostoty nazewnictwa. |
| Operacje masowe | Ten sam warunek może wskazać wiele rekordów do aktualizacji albo archiwizacji. | Przy dużych zbiorach zadbaj o tłumaczenie na zapytanie, nie tylko o ocenę w pamięci. |
W takich miejscach wzorzec przestaje być akademicki. Staje się sposobem na to, żeby biznesowa reguła nie była rozbita na dziesięć fragmentów, z których każdy trochę mówi to samo. A skoro już widać, kiedy pomaga, warto równie uczciwie powiedzieć, kiedy zwykłe rozwiązanie będzie lepsze.
Kiedy prostsze rozwiązanie będzie lepsze
Nie każdy warunek zasługuje na osobną klasę. Jeśli logika ma dwa proste sprawdzenia, żyje wyłącznie w jednym miejscu i raczej nie będzie się rozrastać, zwykły if albo prywatna metoda potrafią być bardziej czytelne niż cały zestaw specyfikacji. Ja traktuję to jako zdrowy odruch: najpierw prostota, potem wyodrębnienie.
| Sytuacja | Lepszy wybór | Dlaczego |
|---|---|---|
| Jednorazowy warunek w jednej metodzie | if / prywatna metoda | Dodanie wzorca tylko zwiększyłoby liczbę plików i pośredników. |
| Reguła biznesowa używana w 2-3 miejscach | Wzorzec specyfikacji | Powtarzalność zaczyna kosztować więcej niż sama abstrakcja. |
| Operacja koordynuje kilka kroków, ale nie opisuje jednej reguły | Domain service | Tu ważniejsza jest orkiestracja niż sam warunek logiczny. |
| Potrzebujesz tylko zapytania do danych | Query object | Jeśli chodzi wyłącznie o odpytywanie, nie zawsze trzeba budować pełną specyfikację domenową. |
Najczęstszy błąd, który widzę, to wdrażanie wzorca „na zapas”. Zamiast rozwiązywać realny problem, zespół dokłada poziom abstrakcji, który za miesiąc trzeba będzie tłumaczyć nowej osobie w projekcie. Dlatego przed decyzją zawsze pytam: czy ta reguła naprawdę ma żyć dłużej niż jedna metoda i czy ktoś jeszcze będzie jej używał? Jeśli tak, specyfikacja zaczyna mieć sens. Jeśli nie, nie warto jej sztucznie wywoływać z życia.
Jak połączyć go z DDD, repozytorium i CQRS
W architekturze opartej na domenie wzorzec specyfikacji najlepiej trzymać blisko języka biznesu. To znaczy: nie „czy rekord ma pole x”, tylko „czy klient jest aktywny”, „czy zamówienie kwalifikuje się do wysyłki”, „czy produkt może zostać opublikowany”. Taki język pomaga zespołowi myśleć o systemie przez pryzmat reguł, a nie samej struktury tabel.
Jednocześnie trzeba uważać, żeby specyfikacja nie zaczęła znać szczegółów ORM-u, indeksów albo technicznych sztuczek warstwy danych. To częsty skręt w złą stronę: zamiast czystej reguły domenowej powstaje obiekt, który bardziej przypomina zapytanie do konkretnej biblioteki niż biznesową decyzję. Jeśli potrzebuję użyć tej samej reguły w repozytorium, zwykle robię to przez wyrażenie albo adapter, a nie przez wpychanie całej logiki do warstwy persystencji.
Jest jeszcze ważna pułapka związana z zawsze poprawnym modelem domenowym. Nie używałbym specyfikacji do tworzenia nowego obiektu w stanie „prawie poprawnym”, tylko po to, żeby potem sprawdzić, czy przejdzie walidację. To kiepski kierunek, bo obiekt domenowy powinien od początku respektować swoje niezmienniki. Specyfikacje sprawdzają się lepiej przy już istniejących encjach, kolekcjach i procesach, które analizują stan systemu, a nie próbują usprawiedliwić niepoprawną konstrukcję. Gdy ten podział jest jasny, wzorzec zaczyna działać znacznie czyściej, ale wciąż można go łatwo zepsuć drobnymi błędami.
Najczęstsze błędy, które psują ten wzorzec
W praktyce nie przegrywa sam wzorzec, tylko sposób jego użycia. Najbardziej kosztowne błędy są zwykle dość przewidywalne.
- Za drobne specyfikacje - jeśli każda ma znaczenie jednego słowa bez kontekstu, kod robi się rozdrobniony i trudny do śledzenia.
- Za szerokie specyfikacje - kiedy jedna klasa zawiera pół logiki biznesowej, wracamy do punktu wyjścia, tylko pod inną nazwą.
- Sprzężenie z bazą danych - reguła domenowa nie powinna być zakładnikiem konkretnego ORM-u.
- Walidowanie nowych obiektów przez obejście niepoprawnego stanu - to zwykle sygnał, że problem trzeba rozwiązać w konstruktorze, fabryce albo na granicy wejścia.
- Nadmierne składanie - jeśli And/Or/Not tworzy konstrukcję trudniejszą niż sam biznesowy warunek, to znak, że abstrakcja się odkleja.
Ja trzymam się prostej zasady: jeśli nazwa specyfikacji nie daje się przeczytać bez cofania wzroku do kodu, to prawdopodobnie trzeba ją uprościć albo rozbić. I właśnie dlatego na koniec najbardziej przydaje się praktyczny plan wejścia, zamiast kolejnej teorii.
Jak zacząć bez przepisywania całej aplikacji
Nie zaczynałbym od refaktoryzacji wszystkiego naraz. Lepiej wybrać jeden warunek, który już teraz powtarza się w kilku miejscach, i wyciągnąć go do osobnego obiektu. To zwykle daje szybki efekt i od razu pokazuje, czy zespół faktycznie potrzebuje tego wzorca.
- Znajdź regułę, która pojawia się w co najmniej 2-3 miejscach.
- Nazwij ją językiem domeny, nie techniki.
- Napisz testy dla przypadków pozytywnych i negatywnych.
- Dopiero potem dodaj łączenie warunków, jeśli naprawdę jest potrzebne.
- Jeśli reguła ma działać na dużych zbiorach danych, zaplanuj tłumaczenie na zapytanie.
W dobrze prowadzonym projekcie taki krok wystarcza, żeby zobaczyć, czy wzorzec faktycznie porządkuje kod, czy tylko dokłada warstwę pośrednią. Jeśli wybierzesz pierwszą sensowną regułę zamiast największej i najbardziej skomplikowanej, zyskasz dużo lepszy sygnał niż po wielkiej, ryzykownej przebudowie całego modułu.