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
Czym jest State Pattern?
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"; } }
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
Aspekt | State Pattern | Strategy Pattern |
---|---|---|
Cel | Zmiana zachowania w zależności od stanu | Wymienne algorytmy |
Kiedy się zmienia | Automatycznie podczas działania | Ustawiane z zewnątrz |
Stanowość | Stany znają o sobie nawzajem | Strategie są niezależne |
Przykład | Stan zamówienia, stan dokumentu | Algorytm sortowania, kompresji |
Zaawansowane techniki
State Factory dla skalowalności
public class OrderStateFactory { private static final Mapstates = 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 }
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()); }
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.
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.
Upewnij się, że stany są thread-safe. Unikaj mutowania stanu w klasach State. Wszystkie dane specyficzne dla instancji trzymaj w Context, nie w State.
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.
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.
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ść.
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!