Mediator Pattern w Spring – Praktyczne Zastosowanie w Aplikacjach

TL;DR: Mediator Pattern to wzorzec projektowy który centralizuje logikę komunikacji między obiektami, eliminując bezpośrednie zależności. W Spring implementujemy go przez ApplicationEventPublisher lub dedykowane klasy mediatorów, co czyni kod bardziej testowalnym i loosely coupled.

Dlaczego Mediator Pattern jest ważny?

W zespołach programistycznych często spotykamy się z problemem „spaghetti code” – sytuacją gdzie klasy są ściśle powiązane i zmiany w jednej z nich wymuszają modyfikacje w wielu innych. Mediator Pattern rozwiązuje ten problem poprzez wprowadzenie centralnego „pośrednika” który zarządza komunikacją między komponentami.

W kontekście biznesowym oznacza to łatwiejsze wprowadzanie zmian, szybsze testowanie i mniejsze ryzyko wprowadzenia bugów przy rozwijaniu aplikacji.

Co się nauczysz:

  • Czym jest Mediator Pattern i kiedy go stosować
  • Jak implementować wzorzec w Spring Boot z użyciem ApplicationEventPublisher
  • Praktyczne zastosowania w systemach e-commerce i CRM
  • Testowanie aplikacji z wzorcem Mediator
  • Różnice między Command Pattern a Mediator Pattern

Wymagania wstępne:

  • Znajomość Spring Boot i Dependency Injection
  • Podstawowa znajomość wzorców projektowych
  • Doświadczenie z testowaniem jednostkowym
  • Zrozumienie koncepcji loose coupling

Czym jest Mediator Pattern?

Mediator Pattern definiuje sposób w jaki zbiór obiektów współpracuje ze sobą. Zamiast bezpośredniego odwoływania się do innych obiektów, klasy komunikują się poprzez wspólny interfejs – mediator.

Mediator Pattern – wzorzec behawioralny który enkapsuluje sposób komunikacji między grupą obiektów, promując loose coupling poprzez eliminację bezpośrednich referencji między nimi.
Analogia: Mediator działa jak dyspozytornia lotów na lotnisku. Samoloty nie komunikują się bezpośrednio między sobą, ale przez kontrolę ruchu lotniczego, która koordynuje wszystkie operacje.

Struktura wzorca

Klasyczna implementacja Mediator Pattern składa się z:

  • Mediator – interfejs definiujący kontakt między komponentami
  • ConcreteMediator – implementacja logiki komunikacji
  • Colleague – klasy które komunikują się przez mediatora

Implementacja w Spring Boot

Spring Framework oferuje kilka sposobów implementacji Mediator Pattern. Najpopularniejsze to wykorzystanie ApplicationEventPublisher lub stworzenie dedykowanych klas mediatorów.

Podejście 1: ApplicationEventPublisher

Spring Boot posiada wbudowany mechanizm event-driven programming, który idealnie nadaje się do implementacji wzorca Mediator.

// Event reprezentujący zamówienie
public class OrderCreatedEvent {
    private final Long orderId;
    private final String customerEmail;
    private final BigDecimal amount;
    
    public OrderCreatedEvent(Long orderId, String customerEmail, BigDecimal amount) {
        this.orderId = orderId;
        this.customerEmail = customerEmail;
        this.amount = amount;
    }
    
    // gettery...
}

@Service
public class OrderService {
    private final ApplicationEventPublisher eventPublisher;
    private final OrderRepository orderRepository;
    
    public OrderService(ApplicationEventPublisher eventPublisher, OrderRepository orderRepository) {
        this.eventPublisher = eventPublisher;
        this.orderRepository = orderRepository;
    }
    
    public void createOrder(OrderRequest request) {
        Order order = new Order(request.getCustomerEmail(), request.getAmount());
        orderRepository.save(order);
        
        // Publikujemy event - mediator w akcji
        eventPublisher.publishEvent(
            new OrderCreatedEvent(order.getId(), order.getCustomerEmail(), order.getAmount())
        );
    }
}

Teraz różne komponenty mogą reagować na event bez bezpośredniej zależności od OrderService:

@Component
public class EmailNotificationService {
    
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        // Wysyłamy email potwierdzający
        sendOrderConfirmationEmail(event.getCustomerEmail(), event.getOrderId());
    }
}

@Component
public class InventoryService {
    
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        // Aktualizujemy stan magazynu
        updateInventoryAfterOrder(event.getOrderId());
    }
}

@Component
public class AnalyticsService {
    
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        // Zapisujemy dane do analityki
        trackOrderCreated(event.getAmount());
    }
}

Pro tip: Używaj @Async na listenerach eventów, aby uniknąć blokowania głównego wątku aplikacji.

Podejście 2: Dedykowany Mediator

Dla bardziej złożonych przypadków możemy stworzyć dedykowaną klasę mediatora:

public interface OrderMediator {
    void processOrder(OrderRequest request);
}

@Service
public class OrderMediatorImpl implements OrderMediator {
    private final OrderService orderService;
    private final EmailService emailService;
    private final PaymentService paymentService;
    private final InventoryService inventoryService;
    
    public OrderMediatorImpl(OrderService orderService, EmailService emailService,
                            PaymentService paymentService, InventoryService inventoryService) {
        this.orderService = orderService;
        this.emailService = emailService;
        this.paymentService = paymentService;
        this.inventoryService = inventoryService;
    }
    
    @Override
    @Transactional
    public void processOrder(OrderRequest request) {
        try {
            // Krok 1: Sprawdź dostępność w magazynie
            inventoryService.checkAvailability(request.getProductId(), request.getQuantity());
            
            // Krok 2: Przetworz płatność
            PaymentResult payment = paymentService.processPayment(request.getPaymentDetails());
            
            // Krok 3: Utwórz zamówienie
            Order order = orderService.createOrder(request, payment.getTransactionId());
            
            // Krok 4: Wyślij email
            emailService.sendOrderConfirmation(order);
            
            // Krok 5: Zaktualizuj magazyn
            inventoryService.updateStock(request.getProductId(), request.getQuantity());
            
        } catch (Exception e) {
            // Obsłuż błędy w jednym miejscu
            handleOrderError(request, e);
        }
    }
}

Praktyczne zastosowania

System e-commerce

W systemach e-commerce Mediator Pattern świetnie sprawdza się przy obsłudze workflow zamówień:

@Service
public class ECommerceMediator {
    
    public void processCheckout(CheckoutRequest request) {
        // Koordynacja wielu usług bez bezpośrednich zależności
        validateCart(request.getCartId());
        applyDiscounts(request.getPromoCodes());
        processPayment(request.getPaymentData());
        createShipment(request.getShippingAddress());
        sendNotifications(request.getCustomerId());
    }
}

System CRM

W aplikacjach CRM mediator może koordynować działania związane z zarządzaniem klientami:

@Component
public class CustomerLifecycleMediator {
    
    @EventListener
    public void handleCustomerRegistered(CustomerRegisteredEvent event) {
        // Koordynacja akcji dla nowego klienta
        createCustomerProfile(event.getCustomerId());
        assignToSalesperson(event.getCustomerId());
        scheduleWelcomeEmail(event.getCustomerId());
        triggerOnboardingProcess(event.getCustomerId());
    }
}

Testowanie z Mediator Pattern

Jedną z największych korzyści wzorca Mediator jest łatwość testowania. Możemy testować komponenty w izolacji:

@ExtendWith(MockitoExtension.class)
class OrderMediatorTest {
    
    @Mock private OrderService orderService;
    @Mock private EmailService emailService;
    @Mock private PaymentService paymentService;
    @Mock private InventoryService inventoryService;
    
    @InjectMocks private OrderMediatorImpl orderMediator;
    
    @Test
    void shouldProcessOrderSuccessfully() {
        // Given
        OrderRequest request = new OrderRequest("product-123", 2, "customer@email.com");
        when(inventoryService.checkAvailability(anyString(), anyInt())).thenReturn(true);
        when(paymentService.processPayment(any())).thenReturn(new PaymentResult("tx-123"));
        
        // When
        orderMediator.processOrder(request);
        
        // Then
        verify(inventoryService).checkAvailability("product-123", 2);
        verify(paymentService).processPayment(any());
        verify(orderService).createOrder(eq(request), eq("tx-123"));
        verify(emailService).sendOrderConfirmation(any());
    }
}

Uwaga: Przy testowaniu event-driven mediatorów pamiętaj o używaniu @MockBean dla Spring Context lub @TestEventListener dla weryfikacji eventów.

Mediator vs Command Pattern

Często mylone są wzorce Mediator i Command. Oto kluczowe różnice:

AspektMediator PatternCommand Pattern
CelRedukuje coupling między obiektamiEnkapsuluje żądanie jako obiekt
StrukturaCentralny mediator + komponentyInvoker + Command + Receiver
UżycieKoordynacja wielu operacjiParametryzacja operacji
SpringApplicationEventPublisher@Component + interfaces

Najlepsze praktyki

Pro tip: Używaj Mediator Pattern gdy masz do czynienia z kompleksowymi workflow wymagającymi koordynacji wielu usług.

Kiedy używać Mediator Pattern:

  • Kompleksowe workflow: Proces składa się z wielu kroków wymagających koordynacji
  • Loose coupling: Chcesz uniknąć bezpośrednich zależności między komponentami
  • Centralna logika: Logika biznesowa ma być w jednym miejscu
  • Event-driven architecture: Aplikacja opiera się na reagowaniu na wydarzenia

Kiedy unikać wzorca:

  • Proste operacje: Gdy logika jest prosta i nie wymaga koordynacji
  • Performance critical: Dodatkowa warstwa może wpłynąć na wydajność
  • Małe aplikacje: Overhead może przewyższać korzyści
Pułapka: Unikaj tworzenia „God Mediator” – mediatora który robi za dużo. Lepiej mieć kilka wyspecjalizowanych mediatorów niż jeden uniwersalny.

Performance i monitoring

Przy implementacji Mediator Pattern w środowisku produkcyjnym warto monitorować:

@Service
public class MonitoredOrderMediator {
    private final MeterRegistry meterRegistry;
    
    @Timed(value = "order.processing.time", description = "Order processing time")
    public void processOrder(OrderRequest request) {
        Timer.Sample sample = Timer.start(meterRegistry);
        try {
            // Logika biznesowa
            doProcessOrder(request);
            meterRegistry.counter("order.processed.success").increment();
        } catch (Exception e) {
            meterRegistry.counter("order.processed.error").increment();
            throw e;
        } finally {
            sample.stop(Timer.builder("order.processing.time").register(meterRegistry));
        }
    }
}

Czy Mediator Pattern wpływa na wydajność aplikacji?

Mediator może wprowadzić minimalny overhead, ale korzyści z łatwiejszego testowania i maintainability przeważają nad niewielkim spadkiem wydajności. W aplikacjach enterprise różnica jest niezauważalna.

Czy mogę używać wielu mediatorów w jednej aplikacji?

Tak, to dobra praktyka. Lepiej mieć wyspecjalizowanych mediatorów dla różnych domen (OrderMediator, CustomerMediator, PaymentMediator) niż jeden uniwersalny.

Jak testować asynchroniczne eventy w Spring?

Używaj @TestEventListener lub ApplicationEventPublisher w testach. Możesz też użyć CountDownLatch dla synchronizacji testów asynchronicznych.

Czy ApplicationEventPublisher to jedyny sposób na Mediator w Spring?

Nie, możesz też tworzyć dedykowane klasy mediatorów lub używać bibliotek typu MediatR dla .NET (equivalenty dla Java jak Axon Framework).

Jak obsłużyć błędy w event-driven mediatorze?

Używaj @TransactionalEventListener z fazą AFTER_ROLLBACK lub implementuj retry mechanism z Spring Retry.

Czy Mediator Pattern może zastąpić Service Layer?

Nie, Mediator Pattern współpracuje z Service Layer. Mediator koordynuje wywołania między serwisami, ale nie zastępuje logiki biznesowej w serwisach.

Jak debugować problemy w aplikacji z Mediator Pattern?

Używaj logowania na poziomie DEBUG w mediatorze, Spring Boot Actuator do monitorowania eventów, oraz narzędzi typu Zipkin do distributed tracing.

Następne kroki:

Teraz gdy znasz Mediator Pattern, warto zgłębić:

Przydatne zasoby:

🚀 Zadanie dla Ciebie

Zaimplementuj system powiadomień dla bloga używając Mediator Pattern. Gdy użytkownik dodaje nowy post, system powinien automatycznie wysłać email do subskrybentów, zaktualizować RSS feed i wygenerować notyfikację push. Użyj Spring Events do koordynacji tych działań.

Mediator Pattern to potężne narzędzie do tworzenia loosely coupled aplikacji. W Spring Boot mamy do dyspozycji gotowe mechanizmy które ułatwiają implementację tego wzorca. Czy używasz już event-driven architecture w swoich projektach? Podziel się swoimi doświadczeniami w komentarzach!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry