Chain of Responsibility pattern

TL;DR: Chain of Responsibility to wzorzec który pozwala przekazać żądanie przez łańcuch obsługi. Każdy element może obsłużyć żądanie lub przekazać dalej. Idealny do walidacji, logowania, autoryzacji i systemów workflow. W Javie implementujesz go przez abstrakcyjną klasę Handler z metodą process().

Dlaczego Chain of Responsibility jest ważny

W każdej aplikacji mamy procesy wymagające wieloetapowej obsługi – od walidacji formularzy po autoryzację użytkowników. Chain of Responsibility eliminuje sztywne powiązania między nadawcą żądania a jego obsługą. Zamiast jednej wielkiej klasy z if-else, masz elastyczny łańcuch gdzie każdy element ma jedną odpowiedzialność.

Chain of Responsibility należy do wzorców behawioralnych i jest fundamentem dla middleware w aplikacjach web oraz systemów pipeline processing.

Co się nauczysz

  • Jak działa wzorzec Chain of Responsibility i kiedy go stosować
  • Implementacja abstrakcyjnej klasy Handler i konkretnych handlerów
  • Tworzenie łańcucha obsługi żądań w praktyce
  • Systemy walidacji i autoryzacji używające tego wzorca
  • Zarządzanie przepływem żądań i obsługa błędów

Wymagania wstępne

Powinieneś znać: podstawy programowania obiektowego w Javie, dziedziczenie, klasy abstrakcyjne i polimorfizm. Przydatna znajomość wzorców projektowych.

Czym jest wzorzec Chain of Responsibility

Chain of Responsibility to wzorzec projektowy który pozwala przekazać żądanie wzdłuż łańcucha potencjalnych obsługujących. Każdy element łańcucha decyduje czy obsłużyć żądanie czy przekazać je dalej.

Handler – abstrakcyjna klasa/interfejs definiujący wspólny interfejs
ConcreteHandler – konkretna implementacja obsługująca specyficzne żądania

Client – wysyła żądanie do pierwszego elementu łańcucha
Wyobraź sobie firmę z hierarchią zatwierdzania wydatków. Pracownik składa wniosek, który przechodzi przez kierownika, dyrektora, aż do prezesa. Każdy poziom może zatwierdzić wniosek lub przekazać wyżej, w zależności od kwoty.

Podstawowa implementacja w Javie

Zacznijmy od prostego przykładu – systemu obsługi wsparcia technicznego:

// Klasa reprezentująca żądanie
class SupportRequest {
    private String type;
    private String description;
    private int priority; // 1=low, 2=medium, 3=high
    
    public SupportRequest(String type, String description, int priority) {
        this.type = type;
        this.description = description;
        this.priority = priority;
    }
    
    // Gettery
    public String getType() { return type; }
    public String getDescription() { return description; }
    public int getPriority() { return priority; }
}

// Abstrakcyjna klasa Handler
abstract class SupportHandler {
    protected SupportHandler nextHandler;
    
    public void setNext(SupportHandler handler) {
        this.nextHandler = handler;
    }
    
    public abstract void handleRequest(SupportRequest request);
    
    protected void passToNext(SupportRequest request) {
        if (nextHandler != null) {
            nextHandler.handleRequest(request);
        } else {
            System.out.println("No handler available for: " + request.getDescription());
        }
    }
}

Konkretne implementacje handlerów

Teraz stwórzmy konkretne handlery dla różnych poziomów wsparcia:

// Handler dla podstawowych problemów
class Level1SupportHandler extends SupportHandler {
    @Override
    public void handleRequest(SupportRequest request) {
        if (request.getPriority() == 1 && request.getType().equals("basic")) {
            System.out.println("Level 1 Support: Handling basic request - " 
                             + request.getDescription());
        } else {
            System.out.println("Level 1 Support: Escalating to Level 2");
            passToNext(request);
        }
    }
}

// Handler dla średnio zaawansowanych problemów  
class Level2SupportHandler extends SupportHandler {
    @Override
    public void handleRequest(SupportRequest request) {
        if (request.getPriority() <= 2 && 
           (request.getType().equals("technical") || request.getType().equals("basic"))) {
            System.out.println("Level 2 Support: Handling technical request - " 
                             + request.getDescription());
        } else {
            System.out.println("Level 2 Support: Escalating to Level 3");
            passToNext(request);
        }
    }
}

// Handler dla krytycznych problemów
class Level3SupportHandler extends SupportHandler {
    @Override
    public void handleRequest(SupportRequest request) {
        if (request.getPriority() == 3) {
            System.out.println("Level 3 Support: Handling critical request - " 
                             + request.getDescription());
        } else {
            System.out.println("Level 3 Support: Handling any remaining request - " 
                             + request.getDescription());
        }
        // Level 3 nie przekazuje dalej - to ostatni poziom
    }
}
Pro tip: Zawsze miej jeden handler który obsłuży wszystkie nieprzechwycone żądania. Zapobiega to sytuacji gdzie żądanie "przepada" w łańcuchu.

Budowanie i użycie łańcucha

Zobaczmy jak połączyć handlery w działający łańcuch:

public class ChainOfResponsibilityDemo {
    public static void main(String[] args) {
        // Tworzenie handlerów
        SupportHandler level1 = new Level1SupportHandler();
        SupportHandler level2 = new Level2SupportHandler();
        SupportHandler level3 = new Level3SupportHandler();
        
        // Budowanie łańcucha
        level1.setNext(level2);
        level2.setNext(level3);
        
        // Testowe żądania
        SupportRequest[] requests = {
            new SupportRequest("basic", "Password reset", 1),
            new SupportRequest("technical", "Database connection issues", 2),
            new SupportRequest("critical", "Server crashed", 3),
            new SupportRequest("unknown", "Strange error", 2)
        };
        
        // Przetwarzanie żądań
        for (SupportRequest request : requests) {
            System.out.println("\n--- Processing new request ---");
            level1.handleRequest(request);
        }
    }
}

Uwaga: Kolejność handlerów w łańcuchu ma kluczowe znaczenie. Umieszczaj bardziej specyficzne handlery przed ogólnymi, aby uniknąć niepotrzebnej eskalacji.

Zaawansowany przykład - walidacja użytkownika

Praktyczny przykład z walidacją i autoryzacją:

// Klasa użytkownika
class User {
    private String username;
    private String password;
    private String role;
    private boolean isActive;
    
    public User(String username, String password, String role, boolean isActive) {
        this.username = username;
        this.password = password;
        this.role = role;
        this.isActive = isActive;
    }
    
    // Gettery...
    public String getUsername() { return username; }
    public String getPassword() { return password; }
    public String getRole() { return role; }
    public boolean isActive() { return isActive; }
}

// Handler walidacji
class ValidationHandler extends SupportHandler {
    @Override
    public void handleRequest(SupportRequest request) {
        // W rzeczywistości byłby to bardziej złożony request z danymi użytkownika
        System.out.println("ValidationHandler: Validating request format");
        
        if (request.getDescription().length() < 5) {
            System.out.println("Validation failed: Description too short");
            return;
        }
        
        System.out.println("Validation passed, forwarding...");
        passToNext(request);
    }
}

// Handler autoryzacji
class AuthorizationHandler extends SupportHandler {
    @Override
    public void handleRequest(SupportRequest request) {
        System.out.println("AuthorizationHandler: Checking user permissions");
        
        // Symulacja sprawdzenia uprawnień
        if (request.getPriority() > 2) {
            System.out.println("Authorization check: Admin privileges required");
        }
        
        System.out.println("Authorization passed, forwarding...");
        passToNext(request);
    }
}

Zalety i wady wzorca

ZaletyWady
Oddzielenie nadawcy od odbiorcyŻądanie może nie zostać obsłużone
Dynamiczne budowanie łańcuchaTrudne debugowanie długich łańcuchów
Single Responsibility PrincipleWydajność - wszystkie handlery są sprawdzane
Open/Closed PrincipleKolejność ma znaczenie
Typowy błąd: Tworzenie bardzo długich łańcuchów z wieloma warunkami. Każdy handler powinien mieć jasno określoną odpowiedzialność i proste warunki decyzyjne.

Kiedy używać Chain of Responsibility

Idealny do:

  • Systemów walidacji z wieloma etapami
  • Middleware w aplikacjach web
  • Workflow i systemy zatwierdzania
  • Parsowania i przetwarzania danych
  • Event handling w GUI
  • Logowania na różnych poziomach

Porównanie z innymi wzorcami

Strategy vs Chain of Responsibility: Strategy wybiera jeden algorytm do wykonania, Chain może wykonać wiele handlerów sekwencyjnie lub znaleźć właściwy handler.
Czy każdy handler musi przekazywać żądanie dalej?

Nie, handler może obsłużyć żądanie i zakończyć łańcuch. Może też obsłużyć częściowo i przekazać dalej do dodatkowego przetwarzania.

Jak zarządzać kolejnością handlerów?

Kolejność powinna być od najbardziej specyficznych do ogólnych. Możesz też użyć Builder pattern do łatwego budowania łańcucha lub konfigurować kolejność przez pliki konfiguracyjne.

Co gdy żaden handler nie obsłuży żądania?

Zawsze dodaj domyślny handler na końcu łańcucha lub implementuj mechanizm fallback. Możesz też rzucić wyjątek lub zalogować nieobsłużone żądanie.

Jak testować Chain of Responsibility?

Testuj każdy handler osobno z mock następnikiem, oraz całe łańcuchy z różnymi scenariuszami żądań. Sprawdź czy właściwy handler obsługuje właściwe żądania.

Czy można modyfikować łańcuch w runtime?

Tak! To jedna z zalet wzorca. Możesz dynamicznie dodawać, usuwać lub zmieniać kolejność handlerów w zależności od potrzeb aplikacji.

Jak Chain of Responsibility współpracuje z Spring?

Świetnie! Spring Interceptors używają tego wzorca. Możesz też wstrzykiwać handlery przez DI i budować łańcuchy automatycznie używając @Autowired i @Order.

Czy wzorzec wpływa na wydajność?

Może, jeśli łańcuch jest bardzo długi. Każdy handler wymaga sprawdzenia warunków. Optymalizuj przez umieszczanie najczęściej używanych handlerów na początku łańcucha.

Przydatne zasoby

🚀 Zadanie dla Ciebie

Stwórz system obsługi zamówień e-commerce który:

  • Waliduje dane zamówienia (adres, produkty, płatność)
  • Sprawdza dostępność produktów w magazynie
  • Aplikuje promocje i rabaty
  • Generuje fakturę
  • Wysyła potwierdzenie email

Każdy etap powinien być osobnym handlerem. Dodaj obsługę błędów - jeśli któryś etap się nie powiedzie, cały proces zostaje wstrzymany z odpowiednim komunikatem.

Chain of Responsibility to wzorzec który pomaga budować elastyczne systemy przetwarzania. Pamiętaj o prostych warunkach w handlerach i zawsze testuj różne ścieżki przepływu żądań. W jakich systemach spotkałeś podobne łańcuchy przetwarzania?

Zostaw komentarz

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

Przewijanie do góry