google-site-verification: google61fe8ba583a51912.html
State Pattern: Dynamic Behavior Change through State Transitions

The State Pattern is a behavioral design pattern that allows an object to alter its behavior when its internal state changes. The object will appear to change its class, providing clean state-specific behavior without complex conditional logic.


What is the State Pattern?

The State Pattern encapsulates state-specific behavior in separate state objects and delegates behavior to the current state. This makes state transitions explicit and behavior changes clean and organized.

Key Benefits:

  • Eliminates complex conditionals - No large switch statements or if-else chains
  • Clean state transitions - Each state knows what states it can transition to
  • Open/Closed Principle - Easy to add new states without modifying existing code
  • Single Responsibility - Each state class handles one state's behavior
  • Explicit state modeling - States become first-class objects

Structure

Context ──────> State Interface ↑ ↑ | | Current State Concrete States - StateA - StateB - StateC

Implementation Examples

Example 1: Vending Machine State Management

import java.util.*; // State interface interface VendingMachineState { void insertMoney(double amount); void selectProduct(String productCode); void dispenseProduct(); void cancel(); void refill(); } // Concrete States class ReadyState implements VendingMachineState { private final VendingMachine machine; public ReadyState(VendingMachine machine) { this.machine = machine; } @Override public void insertMoney(double amount) { machine.addMoney(amount); System.out.printf("Inserted $%.2f. Total: $%.2f%n", amount, machine.getCurrentMoney()); machine.setState(new HasMoneyState(machine)); } @Override public void selectProduct(String productCode) { System.out.println("Please insert money first"); } @Override public void dispenseProduct() { System.out.println("Please insert money and select a product first"); } @Override public void cancel() { System.out.println("No transaction to cancel"); } @Override public void refill() { System.out.println("Machine refilled"); } @Override public String toString() { return "READY"; } } class HasMoneyState implements VendingMachineState { private final VendingMachine machine; public HasMoneyState(VendingMachine machine) { this.machine = machine; } @Override public void insertMoney(double amount) { machine.addMoney(amount); System.out.printf("Inserted $%.2f. Total: $%.2f%n", amount, machine.getCurrentMoney()); } @Override public void selectProduct(String productCode) { Product product = machine.getProduct(productCode); if (product == null) { System.out.println("Invalid product code: " + productCode); return; } if (product.getStock() <= 0) { System.out.println("Product out of stock: " + product.getName()); return; } if (machine.getCurrentMoney() < product.getPrice()) { System.out.printf("Insufficient funds. Need $%.2f, have $%.2f%n", product.getPrice(), machine.getCurrentMoney()); return; } machine.setSelectedProduct(product); machine.setState(new ProductSelectedState(machine)); System.out.println("Selected: " + product.getName()); } @Override public void dispenseProduct() { System.out.println("Please select a product first"); } @Override public void cancel() { double refund = machine.getCurrentMoney(); machine.setCurrentMoney(0); machine.setState(new ReadyState(machine)); System.out.printf("Transaction cancelled. Refunded: $%.2f%n", refund); } @Override public void refill() { System.out.println("Cannot refill while transaction in progress"); } @Override public String toString() { return "HAS_MONEY"; } } class ProductSelectedState implements VendingMachineState { private final VendingMachine machine; public ProductSelectedState(VendingMachine machine) { this.machine = machine; } @Override public void insertMoney(double amount) { System.out.println("Product already selected. Please complete or cancel current transaction"); } @Override public void selectProduct(String productCode) { System.out.println("Product already selected. Please complete or cancel current transaction"); } @Override public void dispenseProduct() { Product product = machine.getSelectedProduct(); double change = machine.getCurrentMoney() - product.getPrice(); product.reduceStock(1); machine.setCurrentMoney(0); machine.setSelectedProduct(null); System.out.printf("Dispensing: %s%n", product.getName()); if (change > 0) { System.out.printf("Returning change: $%.2f%n", change); } machine.setState(new ReadyState(machine)); System.out.println("Thank you for your purchase!"); } @Override public void cancel() { Product product = machine.getSelectedProduct(); double refund = machine.getCurrentMoney(); machine.setCurrentMoney(0); machine.setSelectedProduct(null); machine.setState(new ReadyState(machine)); System.out.printf("Transaction cancelled. Refunded: $%.2f%n", refund); } @Override public void refill() { System.out.println("Cannot refill while transaction in progress"); } @Override public String toString() { return "PRODUCT_SELECTED"; } } class OutOfServiceState implements VendingMachineState { private final VendingMachine machine; public OutOfServiceState(VendingMachine machine) { this.machine = machine; } @Override public void insertMoney(double amount) { System.out.println("Machine is out of service. Please try again later"); } @Override public void selectProduct(String productCode) { System.out.println("Machine is out of service. Please try again later"); } @Override public void dispenseProduct() { System.out.println("Machine is out of service. Please try again later"); } @Override public void cancel() { System.out.println("Machine is out of service"); } @Override public void refill() { machine.setState(new ReadyState(machine)); System.out.println("Machine refilled and back in service"); } @Override public String toString() { return "OUT_OF_SERVICE"; } } // Context class class VendingMachine { private VendingMachineState state; private double currentMoney; private Product selectedProduct; private final Map<String, Product> products; public VendingMachine() { this.products = new HashMap<>(); this.state = new ReadyState(this); initializeProducts(); } private void initializeProducts() { products.put("A1", new Product("A1", "Coke", 1.50, 10)); products.put("A2", new Product("A2", "Pepsi", 1.50, 8)); products.put("B1", new Product("B1", "Chips", 1.00, 15)); products.put("B2", new Product("B2", "Chocolate", 1.25, 12)); } // Delegate methods to current state public void insertMoney(double amount) { state.insertMoney(amount); } public void selectProduct(String productCode) { state.selectProduct(productCode); } public void dispenseProduct() { state.dispenseProduct(); } public void cancel() { state.cancel(); } public void refill() { state.refill(); } public void setOutOfService() { this.state = new OutOfServiceState(this); System.out.println("Machine set to out of service"); } // Getters and setters public void setState(VendingMachineState state) { System.out.printf("State changed: %s → %s%n", this.state, state); this.state = state; } public VendingMachineState getState() { return state; } public double getCurrentMoney() { return currentMoney; } public void setCurrentMoney(double money) { this.currentMoney = money; } public void addMoney(double amount) { this.currentMoney += amount; } public Product getSelectedProduct() { return selectedProduct; } public void setSelectedProduct(Product product) { this.selectedProduct = product; } public Product getProduct(String code) { return products.get(code); } public Collection<Product> getProducts() { return products.values(); } } // Product class class Product { private final String code; private final String name; private final double price; private int stock; public Product(String code, String name, double price, int stock) { this.code = code; this.name = name; this.price = price; this.stock = stock; } public void reduceStock(int quantity) { this.stock -= quantity; } // Getters public String getCode() { return code; } public String getName() { return name; } public double getPrice() { return price; } public int getStock() { return stock; } } // Demo public class VendingMachineDemo { public static void main(String[] args) { VendingMachine machine = new VendingMachine(); System.out.println("=== Vending Machine Demo ===\n"); // Display available products System.out.println("Available Products:"); machine.getProducts().forEach(p -> System.out.printf(" %s: %s - $%.2f (Stock: %d)%n", p.getCode(), p.getName(), p.getPrice(), p.getStock())); System.out.println(); // Test normal workflow System.out.println("1. Normal purchase workflow:"); machine.insertMoney(2.00); machine.selectProduct("A1"); machine.dispenseProduct(); System.out.println("\n2. Insufficient funds:"); machine.insertMoney(1.00); machine.selectProduct("B1"); // Needs $1.00, has $1.00 machine.selectProduct("A1"); // Needs $1.50, has $1.00 machine.cancel(); System.out.println("\n3. Multiple money insertion:"); machine.insertMoney(1.00); machine.insertMoney(0.50); machine.selectProduct("A1"); machine.dispenseProduct(); System.out.println("\n4. Out of service test:"); machine.setOutOfService(); machine.insertMoney(1.00); machine.refill(); // Back in service System.out.println("\n5. Invalid operations:"); machine.selectProduct("A1"); // No money inserted machine.dispenseProduct(); // No product selected } }

Output:

=== Vending Machine Demo === Available Products: A1: Coke - $1.50 (Stock: 10) A2: Pepsi - $1.50 (Stock: 8) B1: Chips - $1.00 (Stock: 15) B2: Chocolate - $1.25 (Stock: 12) 1. Normal purchase workflow: State changed: READY → HAS_MONEY Inserted $2.00. Total: $2.00 State changed: HAS_MONEY → PRODUCT_SELECTED Selected: Coke Dispensing: Coke Returning change: $0.50 State changed: PRODUCT_SELECTED → READY Thank you for your purchase! 2. Insufficient funds: State changed: READY → HAS_MONEY Inserted $1.00. Total: $1.00 Selected: Chips Insufficient funds. Need $1.50, have $1.00 State changed: HAS_MONEY → READY Transaction cancelled. Refunded: $1.00 3. Multiple money insertion: State changed: READY → HAS_MONEY Inserted $1.00. Total: $1.00 Inserted $0.50. Total: $1.50 State changed: HAS_MONEY → PRODUCT_SELECTED Selected: Coke Dispensing: Coke State changed: PRODUCT_SELECTED → READY Thank you for your purchase! 4. Out of service test: Machine set to out of service Machine is out of service. Please try again later State changed: OUT_OF_SERVICE → READY Machine refilled and back in service 5. Invalid operations: Please insert money first Please insert money and select a product first

Example 2: Document Workflow Management

import java.util.*; // State interface interface DocumentState { void edit(String content); void review(boolean approved); void publish(); void archive(); String getStatus(); } // Concrete States class DraftState implements DocumentState { private final Document document; public DraftState(Document document) { this.document = document; } @Override public void edit(String content) { document.setContent(content); System.out.println("Document edited in draft state"); } @Override public void review(boolean approved) { if (approved) { document.setState(new ReviewedState(document)); System.out.println("Document approved, moved to reviewed state"); } else { System.out.println("Document rejected, remains in draft"); } } @Override public void publish() { System.out.println("Cannot publish document in draft state. Please review first."); } @Override public void archive() { document.setState(new ArchivedState(document)); System.out.println("Document archived from draft"); } @Override public String getStatus() { return "DRAFT"; } } class ReviewedState implements DocumentState { private final Document document; public ReviewedState(Document document) { this.document = document; } @Override public void edit(String content) { System.out.println("Cannot edit document in reviewed state. Move back to draft first."); } @Override public void review(boolean approved) { if (approved) { System.out.println("Document already reviewed"); } else { document.setState(new DraftState(document)); System.out.println("Document sent back to draft for revisions"); } } @Override public void publish() { document.setState(new PublishedState(document)); System.out.println("Document published successfully"); } @Override public void archive() { document.setState(new ArchivedState(document)); System.out.println("Document archived from reviewed state"); } @Override public String getStatus() { return "REVIEWED"; } } class PublishedState implements DocumentState { private final Document document; public PublishedState(Document document) { this.document = document; } @Override public void edit(String content) { System.out.println("Cannot edit published document. Create new version."); } @Override public void review(boolean approved) { System.out.println("Document already published. Review not applicable."); } @Override public void publish() { System.out.println("Document already published"); } @Override public void archive() { document.setState(new ArchivedState(document)); System.out.println("Published document archived"); } @Override public String getStatus() { return "PUBLISHED"; } } class ArchivedState implements DocumentState { private final Document document; public ArchivedState(Document document) { this.document = document; } @Override public void edit(String content) { document.setState(new DraftState(document)); document.setContent(content); System.out.println("Document unarchived and edited"); } @Override public void review(boolean approved) { System.out.println("Cannot review archived document"); } @Override public void publish() { System.out.println("Cannot publish archived document"); } @Override public void archive() { System.out.println("Document already archived"); } @Override public String getStatus() { return "ARCHIVED"; } } // Context class class Document { private DocumentState state; private String content; private final String title; private final String author; private final Date createdDate; private Date modifiedDate; public Document(String title, String author, String content) { this.title = title; this.author = author; this.content = content; this.createdDate = new Date(); this.modifiedDate = new Date(); this.state = new DraftState(this); } // Delegate methods to current state public void edit(String content) { state.edit(content); this.modifiedDate = new Date(); } public void review(boolean approved) { state.review(approved); this.modifiedDate = new Date(); } public void publish() { state.publish(); this.modifiedDate = new Date(); } public void archive() { state.archive(); this.modifiedDate = new Date(); } public void restore() { if (state instanceof ArchivedState) { state.edit(this.content); // This will move to draft state } } // Getters and setters public void setState(DocumentState state) { System.out.printf("Document state changed: %s → %s%n", this.state.getStatus(), state.getStatus()); this.state = state; } public DocumentState getState() { return state; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getTitle() { return title; } public String getAuthor() { return author; } public Date getCreatedDate() { return createdDate; } public Date getModifiedDate() { return modifiedDate; } public void printStatus() { System.out.printf("Document: %s | Status: %s | Author: %s%n", title, state.getStatus(), author); System.out.printf("Created: %s | Modified: %s%n", createdDate, modifiedDate); System.out.println("---"); } } // Demo public class DocumentWorkflowDemo { public static void main(String[] args) throws InterruptedException { System.out.println("=== Document Workflow Demo ===\n"); Document document = new Document("Project Proposal", "John Doe", "Initial proposal content"); document.printStatus(); // Normal workflow System.out.println("1. Editing in draft:"); document.edit("Updated proposal content with more details"); System.out.println("\n2. First review (rejected):"); document.review(false); document.printStatus(); System.out.println("3. Editing after rejection:"); document.edit("Revised content based on feedback"); System.out.println("\n4. Second review (approved):"); document.review(true); document.printStatus(); System.out.println("5. Publishing:"); document.publish(); document.printStatus(); System.out.println("6. Archiving:"); document.archive(); document.printStatus(); System.out.println("7. Restoring from archive:"); document.restore(); document.printStatus(); // Test invalid operations System.out.println("8. Testing invalid operations:"); document.publish(); // Should fail - in draft state document.review(true); document.publish(); document.edit("Trying to edit published doc"); // Should fail } }

Example 3: TCP Connection State Management

import java.util.*; // State interface interface TCPState { void open(); void close(); void acknowledge(); void send(String data); } // Concrete States class TCPClosedState implements TCPState { private final TCPConnection connection; public TCPClosedState(TCPConnection connection) { this.connection = connection; } @Override public void open() { connection.setState(new TCPListenState(connection)); System.out.println("Connection opened, now in LISTEN state"); } @Override public void close() { System.out.println("Connection already closed"); } @Override public void acknowledge() { System.out.println("Cannot acknowledge - connection closed"); } @Override public void send(String data) { System.out.println("Cannot send data - connection closed"); } @Override public String toString() { return "CLOSED"; } } class TCPListenState implements TCPState { private final TCPConnection connection; public TCPListenState(TCPConnection connection) { this.connection = connection; } @Override public void open() { System.out.println("Connection already open"); } @Override public void close() { connection.setState(new TCPClosedState(connection)); System.out.println("Connection closed from LISTEN state"); } @Override public void acknowledge() { connection.setState(new TCPEstablishedState(connection)); System.out.println("Connection acknowledged, now ESTABLISHED"); } @Override public void send(String data) { System.out.println("Cannot send data - connection not established"); } @Override public String toString() { return "LISTEN"; } } class TCPEstablishedState implements TCPState { private final TCPConnection connection; public TCPEstablishedState(TCPConnection connection) { this.connection = connection; } @Override public void open() { System.out.println("Connection already established"); } @Override public void close() { connection.setState(new TCPClosedState(connection)); System.out.println("Connection closed from ESTABLISHED state"); } @Override public void acknowledge() { System.out.println("Connection already acknowledged"); } @Override public void send(String data) { System.out.println("Sending data: " + data); connection.addToSentData(data); } @Override public String toString() { return "ESTABLISHED"; } } // Context class class TCPConnection { private TCPState state; private final List<String> sentData; private final String remoteHost; private final int remotePort; public TCPConnection(String remoteHost, int remotePort) { this.remoteHost = remoteHost; this.remotePort = remotePort; this.sentData = new ArrayList<>(); this.state = new TCPClosedState(this); } // Delegate methods to current state public void open() { System.out.printf("Attempting to open connection to %s:%d%n", remoteHost, remotePort); state.open(); } public void close() { state.close(); } public void acknowledge() { state.acknowledge(); } public void send(String data) { state.send(data); } // Getters and setters public void setState(TCPState state) { System.out.printf("TCP State changed: %s → %s%n", this.state, state); this.state = state; } public TCPState getState() { return state; } public void addToSentData(String data) { sentData.add(data); } public List<String> getSentData() { return new ArrayList<>(sentData); } public String getRemoteHost() { return remoteHost; } public int getRemotePort() { return remotePort; } public void printStatus() { System.out.printf("Connection: %s:%d | State: %s | Data Sent: %d packets%n", remoteHost, remotePort, state, sentData.size()); } } // Demo public class TCPConnectionDemo { public static void main(String[] args) { System.out.println("=== TCP Connection State Demo ===\n"); TCPConnection connection = new TCPConnection("example.com", 8080); connection.printStatus(); System.out.println("\n1. Normal connection workflow:"); connection.open(); connection.acknowledge(); connection.send("Hello Server!"); connection.send("How are you?"); connection.close(); connection.printStatus(); System.out.println("\n2. Invalid operations:"); connection.send("This should fail"); // Closed connection connection.acknowledge(); // Closed connection System.out.println("\n3. Reopening connection:"); connection.open(); connection.send("Trying to send before established"); // Should fail connection.acknowledge(); connection.send("Now this should work"); connection.printStatus(); System.out.println("\nSent data: " + connection.getSentData()); } }

Advanced State Pattern

Example 4: State Pattern with State Transitions Map

import java.util.*; // Enhanced state with transition rules interface EnhancedState { void handle(); boolean canTransitionTo(String nextState); Set<String> getPossibleTransitions(); } class ConcreteStateA implements EnhancedState { private final StateContext context; private final Set<String> possibleTransitions = Set.of("STATE_B", "STATE_C"); public ConcreteStateA(StateContext context) { this.context = context; } @Override public void handle() { System.out.println("Handling in State A"); // Auto-transition based on business rules if (someCondition()) { context.transitionTo("STATE_B"); } } @Override public boolean canTransitionTo(String nextState) { return possibleTransitions.contains(nextState); } @Override public Set<String> getPossibleTransitions() { return possibleTransitions; } private boolean someCondition() { return Math.random() > 0.5; } @Override public String toString() { return "STATE_A"; } } // State context with transition management class StateContext { private EnhancedState currentState; private final Map<String, EnhancedState> states = new HashMap<>(); private final List<String> transitionHistory = new ArrayList<>(); public StateContext() { // Initialize states states.put("STATE_A", new ConcreteStateA(this)); // Add other states... currentState = states.get("STATE_A"); transitionHistory.add(currentState.toString()); } public void transitionTo(String stateName) { EnhancedState nextState = states.get(stateName); if (nextState != null && currentState.canTransitionTo(stateName)) { System.out.printf("Transition: %s → %s%n", currentState, nextState); currentState = nextState; transitionHistory.add(stateName); } else { System.out.printf("Invalid transition: %s → %s%n", currentState, stateName); } } public void handle() { currentState.handle(); } public void printTransitionHistory() { System.out.println("Transition History: " + transitionHistory); } }

Best Practices

  1. Use Enums for State Identification
 public enum DocumentStateType { DRAFT, REVIEWED, PUBLISHED, ARCHIVED }
  1. Make States Stateless When Possible
 // Reusable state instance class PublishedState implements DocumentState { private static final PublishedState INSTANCE = new PublishedState(); public static PublishedState getInstance() { return INSTANCE; } }
  1. Use State Pattern with Strategy Pattern
 // State defines what you can do, Strategy defines how you do it interface ProcessingStrategy { void process(String data); } class StateWithStrategy { private DocumentState state; private ProcessingStrategy strategy; // ... }
  1. Implement State Transition Validation
 public boolean isValidTransition(DocumentState current, DocumentState next) { Map<DocumentState, Set<DocumentState>> validTransitions = Map.of( DRAFT, Set.of(REVIEWED, ARCHIVED), REVIEWED, Set.of(DRAFT, PUBLISHED, ARCHIVED) // ... ); return validTransitions.get(current).contains(next); }

When to Use the State Pattern

Use State Pattern when:

  • An object's behavior depends on its state
  • You have complex conditional state checks
  • States have well-defined transitions
  • You need to add new states frequently
  • You want to avoid large conditional statements

Common Use Cases:

  • Workflow systems - Document approval processes
  • Game development - Character states (idle, walking, attacking)
  • UI components - Button states (enabled, disabled, loading)
  • Network protocols - Connection state management
  • Order processing - Order status workflows

Benefits and Trade-offs

Benefits:

  • Clean code - No complex conditionals
  • Easy extensibility - Add new states without modifying existing
  • Single Responsibility - Each state handles its own behavior
  • Explicit state transitions - Clear state flow

Trade-offs:

  • Increased number of classes - More classes to manage
  • Overkill for simple state machines - Use enums for simple cases
  • Runtime state transitions - May be less efficient than conditionals

Conclusion

The State Pattern is a powerful tool for managing object behavior that changes with state:

Key Benefits:

  • Eliminates Complex Conditionals - Replaces switch/case and if/else chains
  • Clean State Management - Each state is encapsulated in its own class
  • Easy Maintenance - States can be modified independently
  • Explicit Transitions - State flow is clear and maintainable

Implementation Approach:

  1. Identify States - List all possible states
  2. Define State Interface - Common operations for all states
  3. Create Concrete States - Implement state-specific behavior
  4. Delegate in Context - Context delegates to current state
  5. Manage Transitions - States or context handles transitions

Real-World Impact:

  • Workflow systems become more maintainable
  • Complex business rules are easier to manage
  • New states can be added with minimal risk
  • Code becomes more testable and readable

By using the State Pattern, you can create systems that are flexible, maintainable, and easy to understand, especially when dealing with complex state-dependent behavior.


Remember: The State Pattern shines when you have complex state-dependent behavior. For simpler cases with few states and simple transitions, consider using enums or simple conditionals to avoid unnecessary complexity.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper