Dlaczego Specification Pattern to Must-Have w DDD?
W Domain-Driven Design spotykamy się z coraz bardziej złożonymi regułami biznesowymi. Bez odpowiedniego wzorca, logika rozprasza się po całej aplikacji – w kontrolerach, serwisach, a nawet w modelach. Specification pattern rozwiązuje ten problem, tworząc dedykowane obiekty dla każdej reguły biznesowej. To nie tylko porządkuje kod, ale również czyni go testowalnym i ponownie używalnym.
Co się nauczysz:
- Jak implementować Specification pattern w Javie 8+ z wykorzystaniem funkcji lambda
- Tworzenie kompozytowych specyfikacji za pomocą operatorów AND, OR, NOT
- Wykorzystanie specyfikacji do walidacji obiektów domenowych
- Implementacja query specifications dla filtrowania danych
- Integracja z JPA i Criteria API dla zaawansowanych zapytań
- Best practices i anty-wzorce w używaniu specyfikacji
Czym jest Specification Pattern?
Specification pattern to wzorzec behawioralny, który enkapsuluje logikę biznesową w obiektach specyfikacji. Każda specyfikacja reprezentuje jedną regułę biznesową i może być kombinowana z innymi specyfikacjami.
Podstawowa implementacja w Javie
@FunctionalInterface public interface Specification{ boolean isSatisfiedBy(T candidate); default Specification and(Specification other) { return candidate -> this.isSatisfiedBy(candidate) && other.isSatisfiedBy(candidate); } default Specification or(Specification other) { return candidate -> this.isSatisfiedBy(candidate) || other.isSatisfiedBy(candidate); } default Specification not() { return candidate -> !this.isSatisfiedBy(candidate); } }
Implementacja Konkretnych Specyfikacji
Załóżmy, że mamy system e-commerce i chcemy zdefiniować reguły dla kwalifikowania klientów do różnych promocji:
// Model domenowy public class Customer { private String email; private LocalDate registrationDate; private BigDecimal totalSpent; private CustomerType type; private boolean isActive; // konstruktor, gettery, settery } // Konkretne specyfikacje public class ActiveCustomerSpecification implements Specification{ @Override public boolean isSatisfiedBy(Customer customer) { return customer.isActive(); } } public class LoyalCustomerSpecification implements Specification { private final int minimumMonths; private final BigDecimal minimumSpent; public LoyalCustomerSpecification(int minimumMonths, BigDecimal minimumSpent) { this.minimumMonths = minimumMonths; this.minimumSpent = minimumSpent; } @Override public boolean isSatisfiedBy(Customer customer) { LocalDate cutoffDate = LocalDate.now().minusMonths(minimumMonths); return customer.getRegistrationDate().isBefore(cutoffDate) && customer.getTotalSpent().compareTo(minimumSpent) >= 0; } } public class PremiumCustomerSpecification implements Specification { @Override public boolean isSatisfiedBy(Customer customer) { return customer.getType() == CustomerType.PREMIUM; } }
Kombinowanie Specyfikacji
Prawdziwa siła wzorca ujawnia się gdy kombinujemy specyfikacje:
public class CustomerPromotionService { // Specyfikacje jako stałe klasowe private static final SpecificationACTIVE = new ActiveCustomerSpecification(); private static final Specification LOYAL = new LoyalCustomerSpecification(12, new BigDecimal("1000")); private static final Specification PREMIUM = new PremiumCustomerSpecification(); public boolean isEligibleForVipPromotion(Customer customer) { Specification vipSpec = ACTIVE.and(LOYAL).and(PREMIUM); return vipSpec.isSatisfiedBy(customer); } public boolean isEligibleForLoyaltyDiscount(Customer customer) { Specification loyaltySpec = ACTIVE.and(LOYAL.or(PREMIUM)); return loyaltySpec.isSatisfiedBy(customer); } public List findEligibleCustomers(List customers) { Specification eligibilitySpec = ACTIVE.and(LOYAL.or(PREMIUM)); return customers.stream() .filter(eligibilitySpec::isSatisfiedBy) .collect(Collectors.toList()); } }
Specyfikacje z Parametrami
Często specyfikacje potrzebują parametrów konfiguracyjnych. Oto eleganckie rozwiązanie:
public class CustomerSpecifications { public static SpecificationspentMoreThan(BigDecimal amount) { return customer -> customer.getTotalSpent().compareTo(amount) > 0; } public static Specification registeredAfter(LocalDate date) { return customer -> customer.getRegistrationDate().isAfter(date); } public static Specification hasEmailDomain(String domain) { return customer -> customer.getEmail().endsWith("@" + domain); } public static Specification isOfType(CustomerType type) { return customer -> customer.getType() == type; } } // Użycie public class CustomerService { public List findHighValueRecentCustomers() { Specification highValueRecent = CustomerSpecifications.spentMoreThan(new BigDecimal("5000")) .and(CustomerSpecifications.registeredAfter(LocalDate.now().minusYears(1))); return customerRepository.findAll().stream() .filter(highValueRecent::isSatisfiedBy) .collect(Collectors.toList()); } }
Integracja z JPA i Criteria API
W aplikacjach enterprise często potrzebujemy przełożyć specyfikacje na zapytania bazodanowe:
// Rozszerzona specyfikacja dla JPA public interface JpaSpecificationextends Specification { Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder cb); } // Implementacja dla Customer public class ActiveCustomerJpaSpecification implements JpaSpecification { @Override public boolean isSatisfiedBy(Customer customer) { return customer.isActive(); } @Override public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder cb) { return cb.isTrue(root.get("isActive")); } } // Repository z wsparciem dla specyfikacji @Repository public class CustomerRepository { @PersistenceContext private EntityManager entityManager; public List findBySpecification(JpaSpecification specification) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(Customer.class); Root root = query.from(Customer.class); query.where(specification.toPredicate(root, query, cb)); return entityManager.createQuery(query).getResultList(); } }
Spring Data JPA Specifications
Spring Data JPA oferuje built-in wsparcie dla Specification pattern:
// Repository dziedziczące po JpaSpecificationExecutor public interface CustomerRepository extends JpaRepository, JpaSpecificationExecutor { } // Specyfikacje jako klasa utility public class CustomerSpecs { public static Specification isActive() { return (root, query, cb) -> cb.isTrue(root.get("isActive")); } public static Specification spentMoreThan(BigDecimal amount) { return (root, query, cb) -> cb.greaterThan(root.get("totalSpent"), amount); } public static Specification registeredAfter(LocalDate date) { return (root, query, cb) -> cb.greaterThan(root.get("registrationDate"), date); } } // Serwis używający specyfikacji @Service public class CustomerService { @Autowired private CustomerRepository customerRepository; public List findHighValueActiveCustomers(BigDecimal minimumSpent) { Specification spec = CustomerSpecs.isActive() .and(CustomerSpecs.spentMoreThan(minimumSpent)); return customerRepository.findAll(spec); } }
Testowanie Specyfikacji
Jedną z największych zalet Specification pattern jest testowalność:
public class CustomerSpecificationTest { private Customer activeCustomer; private Customer inactiveCustomer; private Customer loyalCustomer; @BeforeEach void setUp() { activeCustomer = new Customer("active@test.com", LocalDate.now().minusYears(2), new BigDecimal("2000"), CustomerType.REGULAR, true); inactiveCustomer = new Customer("inactive@test.com", LocalDate.now().minusMonths(6), new BigDecimal("500"), CustomerType.REGULAR, false); loyalCustomer = new Customer("loyal@test.com", LocalDate.now().minusYears(3), new BigDecimal("5000"), CustomerType.PREMIUM, true); } @Test void activeCustomerSpecification_shouldReturnTrueForActiveCustomer() { Specificationspec = new ActiveCustomerSpecification(); assertTrue(spec.isSatisfiedBy(activeCustomer)); assertFalse(spec.isSatisfiedBy(inactiveCustomer)); } @Test void combinedSpecification_shouldWorkCorrectly() { Specification activeAndLoyal = new ActiveCustomerSpecification() .and(new LoyalCustomerSpecification(12, new BigDecimal("1000"))); assertTrue(activeAndLoyal.isSatisfiedBy(loyalCustomer)); assertFalse(activeAndLoyal.isSatisfiedBy(inactiveCustomer)); assertFalse(activeAndLoyal.isSatisfiedBy(activeCustomer)); // not loyal enough } }
Najlepsze Praktyki i Anty-wzorce
- Jedna specyfikacja = jedna reguła biznesowa
- Używaj immutable objects dla specyfikacji
- Twórz factory methods dla specyfikacji z parametrami
- Kombinuj specyfikacje za pomocą operatorów logicznych
✅ Dobre praktyki | ❌ Anty-wzorce |
---|---|
Enkapsulacja złożonej logiki | Specyfikacje dla prostych getterów |
Testowalne reguły biznesowe | Specyfikacje z side effects |
Kombinowanie za pomocą AND/OR | Mutowalne specyfikacje |
Factory methods z parametrami | Specyfikacje ze stanem |
Gdy masz złożoną logikę biznesową, która może się zmieniać, jest używana w wielu miejscach, lub potrzebujesz kombinować różne reguły. Proste sprawdzenia typu if (customer.isActive()) nie wymagają specyfikacji.
Tak! Specyfikacje są idealne do walidacji obiektów domenowych. Możesz kombinować różne reguły walidacyjne i tworzyć złożone scenariusze sprawdzania poprawności danych.
W pamięci – minimalny wpływ. Dla dużych zbiorów danych lepiej używać JPA Specifications które tłumaczą się na zapytania SQL zamiast filtrować w Javie.
Tak, ale tylko jeśli są immutable i bezstanowe. Najlepiej tworzyć je jako statyczne stałe lub używać factory methods.
Dodaj metody toString() do specyfikacji opisujące ich logikę. Testuj każdą specyfikację osobno przed kombinowaniem.
🚀 Zadanie dla Ciebie
Stwórz system specyfikacji dla systemu zarządzania zamówieniami:
- Specyfikacja dla zamówień o wysokiej wartości (powyżej 1000 PLN)
- Specyfikacja dla zamówień pilnych (dostawa w ciągu 24h)
- Specyfikacja dla zamówień od klientów VIP
- Kombinacja specyfikacji dla zamówień wymagających special handling
Napisz testy jednostkowe i zintegruj z Spring Data JPA. Bonus: dodaj specyfikację dla zamówień w określonym przedziale czasowym.