State Pattern w Finite State Machines – Praktyczny Przewodnik

TL;DR: State Pattern to wzorzec projektowy idealny do implementacji finite state machines. Pozwala obiektowi zmieniać zachowanie w zależności od wewnętrznego stanu. W Java implementujemy go przez interface State, konkretne klasy stanów i Context. Kluczowe: enkapsulacja logiki stanów, łatwość dodawania nowych stanów, eliminacja długich if-else.

Dlaczego State Pattern jest kluczowy w finite state machines?

W rozwoju aplikacji enterprise często spotykamy się z obiektami, które muszą zmieniać zachowanie w zależności od swojego stanu. Zamówienia w e-commerce przechodzą przez stany: „nowe”, „płatność”, „wysłane”, „dostarczone”. Bez State Pattern kod szybko zamienia się w spaghetti z if-else sprawdzającymi każdy możliwy stan i przejście.

State Pattern rozwiązuje ten problem poprzez enkapsulację logiki każdego stanu w osobnej klasie. To oznacza czytelniejszy kod, łatwiejsze testowanie i możliwość dodawania nowych stanów bez modyfikacji istniejącego kodu.

Co się nauczysz:

  • Jak implementować State Pattern w Java 8/11
  • Kiedy stosować finite state machines w aplikacjach
  • Różnicę między State Pattern a Strategy Pattern
  • Jak unikać typowych pułapek przy implementacji stanów
  • Best practices dla enterprise state machines
Wymagania wstępne: Znajomość podstaw Java, interfejsów, dziedziczenia oraz wzorców Strategy i Template Method. Doświadczenie z aplikacjami biznesowymi będzie pomocne.

Czym jest State Pattern?

State Pattern – wzorzec behawioralny pozwalający obiektowi zmieniać zachowanie poprzez zmianę wewnętrznego stanu. Obiekt wydaje się zmieniać swoją klasę.

State Pattern składa się z trzech głównych elementów:

**Context** – klasa, której zachowanie zależy od stanu. Przechowuje referencję do aktualnego stanu i deleguje do niego wywołania.

**State Interface** – interfejs definiujący metody, które każdy konkretny stan musi implementować.

**Concrete States** – konkretne implementacje stanów, każda enkapsuluje zachowanie dla danego stanu.

Implementacja State Pattern w Java

Zobaczmy praktyczną implementację na przykładzie systemu przetwarzania zamówień:

// Interface State - definiuje operacje dla każdego stanu
public interface OrderState {
    void processPayment(OrderContext context);
    void shipOrder(OrderContext context);
    void cancelOrder(OrderContext context);
    String getStatusName();
}

// Context - zamówienie które zmienia zachowanie w zależności od stanu
public class OrderContext {
    private OrderState currentState;
    private String orderId;
    private BigDecimal amount;
    
    public OrderContext(String orderId, BigDecimal amount) {
        this.orderId = orderId;
        this.amount = amount;
        this.currentState = new NewOrderState(); // stan początkowy
    }
    
    public void setState(OrderState state) {
        System.out.println("Order " + orderId + " changed state to: " + 
                          state.getStatusName());
        this.currentState = state;
    }
    
    // Delegacja wywołań do aktualnego stanu
    public void processPayment() {
        currentState.processPayment(this);
    }
    
    public void shipOrder() {
        currentState.shipOrder(this);
    }
    
    public void cancelOrder() {
        currentState.cancelOrder(this);
    }
    
    // getters
    public String getOrderId() { return orderId; }
    public BigDecimal getAmount() { return amount; }
    public String getCurrentStatus() { return currentState.getStatusName(); }
}

Teraz implementujemy konkretne stany:

// Stan: Nowe zamówienie
public class NewOrderState implements OrderState {
    
    @Override
    public void processPayment(OrderContext context) {
        System.out.println("Processing payment for order: " + context.getOrderId());
        // Logika przetwarzania płatności
        PaymentService.processPayment(context.getAmount());
        
        // Przejście do następnego stanu
        context.setState(new PaidOrderState());
    }
    
    @Override
    public void shipOrder(OrderContext context) {
        System.out.println("Cannot ship unpaid order: " + context.getOrderId());
    }
    
    @Override
    public void cancelOrder(OrderContext context) {
        System.out.println("Canceling new order: " + context.getOrderId());
        context.setState(new CancelledOrderState());
    }
    
    @Override
    public String getStatusName() {
        return "NEW";
    }
}

// Stan: Zamówienie opłacone
public class PaidOrderState implements OrderState {
    
    @Override
    public void processPayment(OrderContext context) {
        System.out.println("Order already paid: " + context.getOrderId());
    }
    
    @Override
    public void shipOrder(OrderContext context) {
        System.out.println("Shipping order: " + context.getOrderId());
        // Logika wysyłki
        ShippingService.shipOrder(context.getOrderId());
        
        context.setState(new ShippedOrderState());
    }
    
    @Override
    public void cancelOrder(OrderContext context) {
        System.out.println("Refunding and canceling paid order: " + context.getOrderId());
        PaymentService.refund(context.getAmount());
        context.setState(new CancelledOrderState());
    }
    
    @Override
    public String getStatusName() {
        return "PAID";
    }
}

// Stan: Zamówienie wysłane
public class ShippedOrderState implements OrderState {
    
    @Override
    public void processPayment(OrderContext context) {
        System.out.println("Order already paid and shipped: " + context.getOrderId());
    }
    
    @Override
    public void shipOrder(OrderContext context) {
        System.out.println("Order already shipped: " + context.getOrderId());
    }
    
    @Override
    public void cancelOrder(OrderContext context) {
        System.out.println("Cannot cancel shipped order: " + context.getOrderId());
    }
    
    @Override
    public String getStatusName() {
        return "SHIPPED";
    }
}
Pro tip: W środowisku Spring Boot możesz wykorzystać `@Component` i dependency injection dla stanów. Każdy stan jako singleton bean może mieć wstrzyknięte serwisy biznesowe.

Użycie State Machine

public class StatePatternDemo {
    public static void main(String[] args) {
        // Tworzenie nowego zamówienia
        OrderContext order = new OrderContext("ORD-001", new BigDecimal("299.99"));
        
        System.out.println("Initial state: " + order.getCurrentStatus());
        
        // Próba wysyłki bez płatności
        order.shipOrder(); // Cannot ship unpaid order
        
        // Przetwarzanie płatności
        order.processPayment(); // Processing payment, state changes to PAID
        
        // Teraz można wysłać
        order.shipOrder(); // Shipping order, state changes to SHIPPED
        
        // Próba anulowania po wysyłce
        order.cancelOrder(); // Cannot cancel shipped order
        
        System.out.println("Final state: " + order.getCurrentStatus());
    }
}

State Pattern vs Strategy Pattern

AspektState PatternStrategy Pattern
CelZmiana zachowania w zależności od stanuWymienne algorytmy
Kiedy się zmieniaAutomatycznie podczas działaniaUstawiane z zewnątrz
StanowośćStany znają o sobie nawzajemStrategie są niezależne
PrzykładStan zamówienia, stan dokumentuAlgorytm sortowania, kompresji
Pułapka: Nie mieszaj State Pattern ze Strategy Pattern. W State obiekty stanu mogą zmieniać stan Context, w Strategy – strategie są bezstanowe i wymienne.

Zaawansowane techniki

State Factory dla skalowalności

public class OrderStateFactory {
    private static final Map states = new HashMap<>();
    
    static {
        states.put("NEW", new NewOrderState());
        states.put("PAID", new PaidOrderState());
        states.put("SHIPPED", new ShippedOrderState());
        states.put("CANCELLED", new CancelledOrderState());
    }
    
    public static OrderState getState(String stateName) {
        OrderState state = states.get(stateName);
        if (state == null) {
            throw new IllegalArgumentException("Unknown state: " + stateName);
        }
        return state;
    }
    
    public static Set getAvailableStates() {
        return states.keySet();
    }
}

Persystencja stanu

// JPA Entity dla persystencji stanu
@Entity
@Table(name = "orders")
public class OrderEntity {
    @Id
    private String orderId;
    
    @Column(name = "current_state")
    private String currentStateName;
    
    @Column(name = "amount")
    private BigDecimal amount;
    
    // Odtwarzanie state machine z bazy
    public OrderContext toStateMachine() {
        OrderContext context = new OrderContext(orderId, amount);
        context.setState(OrderStateFactory.getState(currentStateName));
        return context;
    }
    
    // getters/setters
}
W aplikacjach enterprise często kombinujemy State Pattern z JPA/Hibernate dla persystencji stanu i Spring dla dependency injection w stanach.

Testing State Machines

@Test
public void shouldTransitionFromNewToPaidWhenPaymentProcessed() {
    // Given
    OrderContext order = new OrderContext("TEST-001", new BigDecimal("100.00"));
    assertEquals("NEW", order.getCurrentStatus());
    
    // When
    order.processPayment();
    
    // Then
    assertEquals("PAID", order.getCurrentStatus());
}

@Test
public void shouldNotAllowShippingUnpaidOrder() {
    // Given
    OrderContext order = new OrderContext("TEST-002", new BigDecimal("200.00"));
    
    // When
    order.shipOrder();
    
    // Then
    assertEquals("NEW", order.getCurrentStatus()); // Stan się nie zmienił
}

@Test
public void shouldFollowCompleteWorkflow() {
    // Given
    OrderContext order = new OrderContext("TEST-003", new BigDecimal("300.00"));
    
    // When & Then
    assertEquals("NEW", order.getCurrentStatus());
    
    order.processPayment();
    assertEquals("PAID", order.getCurrentStatus());
    
    order.shipOrder();
    assertEquals("SHIPPED", order.getCurrentStatus());
}
Kiedy użyć State Pattern zamiast prostych if-else?

Gdy masz więcej niż 3-4 stany, złożone przejścia między stanami, lub gdy logika stanu przekracza 20-30 linii kodu. State Pattern również świetnie sprawdza się gdy planujesz częste dodawanie nowych stanów.

Czy stany powinny być singletonami?

Tak, jeśli stany nie przechowują danych specyficznych dla instancji. Stany bezstanowe mogą być współdzielone między wieloma Context obiektami. Użyj State Factory lub Spring beans.

Jak radzić sobie z wieloma Context w tym samym stanie?

Upewnij się, że stany są thread-safe. Unikaj mutowania stanu w klasach State. Wszystkie dane specyficzne dla instancji trzymaj w Context, nie w State.

Jak logować przejścia stanów?

Dodaj logging w metodzie setState() w Context lub stwórz State Transition Logger który będzie obserwował zmiany. W Spring Boot użyj @EventListener.

Czy można mieć hierarchię stanów?

Tak, możesz tworzyć abstract base state classes dla stanów o podobnym zachowaniu. Na przykład AbstractPaidState dla wszystkich stanów po płatności.

Jak testować przejścia stanów?

Testuj każde przejście osobno, sprawdzaj czy nieprawidłowe przejścia są blokowane, i testuj complete workflows. Użyj parametrized tests dla podobnych przejść.

Co z performance przy dużej liczbie stanów?

State Pattern ma minimalny overhead. Kluczowe to cache’owanie instancji stanów w State Factory i unikanie tworzenia nowych obiektów przy każdym przejściu.

🚀 Zadanie dla Ciebie

Zaimplementuj state machine dla dokumentu który przechodzi przez stany: DRAFT → REVIEW → APPROVED → PUBLISHED. Każdy stan ma różne operacje: edit(), submit(), approve(), publish(), reject(). Dodaj walidację przejść i persistence z użyciem JPA. Bonus: dodaj email notifications przy zmianie stanu.

Przydatne zasoby:

Czy masz doświadczenie z implementacją state machines w swoich projektach? Jakie napotkałeś wyzwania przy zarządzaniu złożonymi stanami? Podziel się w komentarzach!

Zostaw komentarz

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

Przewijanie do góry