State Pattern - Allowing Object Behavior to Change When Internal State Changes
Learning Objectives: By the end of this lesson, you will be able to:
- Understand the State pattern and how it differs from large conditional statements
- Implement state machines with clean state transitions and encapsulated behavior
- Apply State pattern to workflows, game AI, and protocol implementations
- Design flexible state systems that support dynamic state addition and modification
- Handle complex state transitions with guards, actions, and event-driven changes
1. Welcome and Introduction (5 minutes)
Icebreaker Activity
Think about a vending machine. When it’s in “waiting for selection” state, pressing a product button shows the price. When it’s in “payment received” state, the same button dispenses the product. The same input produces different behavior based on the machine’s internal state. How would you model this without having massive if-else statements checking the current state?
Lesson Overview
- Duration: 120 minutes
- Prerequisites: Object-oriented programming, basic understanding of state machines, polymorphism
- Materials Needed: Development environment with Java, Python, or JavaScript support
2. Learning Objectives and Session Outline (5 minutes)
What We’ll Cover Today:
- State Machine Fundamentals - Understanding states, transitions, and events
- State Pattern Structure - Context, state interface, and concrete states
- State Transitions - How states change and who controls the transitions
- Real-world Applications - Workflow engines, game AI, connection protocols
- Advanced Techniques - Hierarchical states, state history, guards and actions
- Performance Considerations - State caching, transition optimization, memory management
Success Criteria:
By the end of this session, you should be able to design and implement state machines using the State pattern for complex behavioral scenarios.
3. Foundation Knowledge (15-20 minutes)
Core Concepts
What is the State Pattern?
The State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class by delegating behavior to different state objects that represent different states.
Real-World Analogy: Think of a person’s mood affecting their behavior. When someone is happy, they might laugh at jokes and be sociable. When they’re tired, the same jokes might be met with indifference. The person is the same, but their behavior changes based on their internal state (mood).
Why This Matters: Instead of having large switch statements or nested if-else blocks that check the current state, you encapsulate state-specific behavior in separate classes. This makes code more maintainable, extensible, and follows the Open/Closed Principle.
Key Components
- Context: The object whose behavior changes based on state
- State Interface: Defines methods that all concrete states must implement
- Concrete States: Implement state-specific behavior and handle state transitions
- Client: Uses the context object without knowing about state changes
State Pattern vs Strategy Pattern
- Strategy: Chooses algorithm to use; context controls which strategy
- State: Behavior changes automatically based on internal state; states often control transitions
Key Terminology
- State Machine: System that can be in one of a finite number of states
- State Transition: Change from one state to another
- Event: Trigger that may cause a state transition
- Guard Condition: Boolean condition that determines if transition can occur
- Action: Code executed when entering/exiting state or during transition
- State History: Remembering previous states for potential restoration
4. Detailed Explanation and Examples (25-30 minutes)
Understanding State Pattern Structure
The State pattern is essential when:
- An object’s behavior depends on its state and must change at runtime
- You have large conditional statements based on state that are hard to maintain
- State transitions are complex and need to be managed systematically
- You want to avoid duplicating state transition logic across multiple methods
- Different states have significantly different behavior for the same operations
Practical Applications
State pattern is commonly used for:
- Workflow Systems: Document approval, order processing, ticket systems
- Game Development: Character AI, game states (menu, playing, paused), NPC behavior
- Network Protocols: TCP connections, HTTP sessions, communication protocols
- User Interface: Form validation, wizard steps, application modes
- Media Players: Playing, paused, stopped, buffering states
- Order Processing: Pending, confirmed, shipped, delivered, cancelled states
Common Misconceptions
-
Misconception: State pattern is only for simple state machines Reality: Can handle complex hierarchical and concurrent state machines
-
Misconception: States should never reference each other Reality: States often need to trigger transitions to other states
5. Live Coding Demonstration (20-25 minutes)
Example 1: Document Workflow System
// State interface
interface DocumentState {
void edit(Document document, String content);
void submit(Document document);
void approve(Document document);
void reject(Document document, String reason);
void publish(Document document);
String getStatusDescription();
boolean canTransitionTo(Class<? extends DocumentState> targetState);
}
// Context class
class Document {
private String title;
private String content;
private String author;
private DocumentState currentState;
private List<String> comments;
private Date lastModified;
public Document(String title, String author) {
this.title = title;
this.author = author;
this.content = "";
this.comments = new ArrayList<>();
this.lastModified = new Date();
this.currentState = new DraftState();
System.out.println("Document created: " + title + " by " + author);
System.out.println("Status: " + currentState.getStatusDescription());
}
// Delegate operations to current state
public void edit(String content) {
currentState.edit(this, content);
}
public void submit() {
currentState.submit(this);
}
public void approve() {
currentState.approve(this);
}
public void reject(String reason) {
currentState.reject(this, reason);
}
public void publish() {
currentState.publish(this);
}
// State management
public void setState(DocumentState newState) {
if (currentState.canTransitionTo(newState.getClass())) {
System.out.println("State transition: " + currentState.getClass().getSimpleName() +
" -> " + newState.getClass().getSimpleName());
this.currentState = newState;
this.lastModified = new Date();
} else {
throw new IllegalStateException("Cannot transition from " +
currentState.getClass().getSimpleName() + " to " +
newState.getClass().getSimpleName());
}
}
public String getStatus() {
return currentState.getStatusDescription();
}
public void addComment(String comment) {
comments.add(new Date() + ": " + comment);
}
// Getters and setters
public String getTitle() { return title; }
public String getContent() { return content; }
public void setContent(String content) {
this.content = content;
this.lastModified = new Date();
}
public String getAuthor() { return author; }
public List<String> getComments() { return new ArrayList<>(comments); }
public Date getLastModified() { return lastModified; }
}
// Concrete States
class DraftState implements DocumentState {
@Override
public void edit(Document document, String content) {
document.setContent(content);
System.out.println("Document edited in draft state");
}
@Override
public void submit(Document document) {
if (document.getContent().trim().isEmpty()) {
System.out.println("Cannot submit empty document");
return;
}
document.setState(new UnderReviewState());
document.addComment("Document submitted for review");
}
@Override
public void approve(Document document) {
System.out.println("Cannot approve document in draft state");
}
@Override
public void reject(Document document, String reason) {
System.out.println("Cannot reject document in draft state");
}
@Override
public void publish(Document document) {
System.out.println("Cannot publish document in draft state");
}
@Override
public String getStatusDescription() {
return "Draft - Being authored";
}
@Override
public boolean canTransitionTo(Class<? extends DocumentState> targetState) {
return targetState == UnderReviewState.class;
}
}
class UnderReviewState implements DocumentState {
@Override
public void edit(Document document, String content) {
System.out.println("Cannot edit document while under review");
}
@Override
public void submit(Document document) {
System.out.println("Document is already under review");
}
@Override
public void approve(Document document) {
document.setState(new ApprovedState());
document.addComment("Document approved");
}
@Override
public void reject(Document document, String reason) {
document.setState(new RejectedState());
document.addComment("Document rejected: " + reason);
}
@Override
public void publish(Document document) {
System.out.println("Cannot publish document that is under review");
}
@Override
public String getStatusDescription() {
return "Under Review - Awaiting approval";
}
@Override
public boolean canTransitionTo(Class<? extends DocumentState> targetState) {
return targetState == ApprovedState.class || targetState == RejectedState.class;
}
}
class ApprovedState implements DocumentState {
@Override
public void edit(Document document, String content) {
System.out.println("Cannot edit approved document");
}
@Override
public void submit(Document document) {
System.out.println("Document is already approved");
}
@Override
public void approve(Document document) {
System.out.println("Document is already approved");
}
@Override
public void reject(Document document, String reason) {
document.setState(new RejectedState());
document.addComment("Approved document rejected: " + reason);
}
@Override
public void publish(Document document) {
document.setState(new PublishedState());
document.addComment("Document published");
}
@Override
public String getStatusDescription() {
return "Approved - Ready for publication";
}
@Override
public boolean canTransitionTo(Class<? extends DocumentState> targetState) {
return targetState == PublishedState.class || targetState == RejectedState.class;
}
}
class RejectedState implements DocumentState {
@Override
public void edit(Document document, String content) {
// Allow editing to address rejection reasons
document.setContent(content);
document.setState(new DraftState());
document.addComment("Document edited after rejection, returned to draft");
}
@Override
public void submit(Document document) {
document.setState(new UnderReviewState());
document.addComment("Rejected document resubmitted for review");
}
@Override
public void approve(Document document) {
System.out.println("Cannot approve rejected document directly");
}
@Override
public void reject(Document document, String reason) {
document.addComment("Additional rejection reason: " + reason);
}
@Override
public void publish(Document document) {
System.out.println("Cannot publish rejected document");
}
@Override
public String getStatusDescription() {
return "Rejected - Needs revision";
}
@Override
public boolean canTransitionTo(Class<? extends DocumentState> targetState) {
return targetState == DraftState.class || targetState == UnderReviewState.class;
}
}
class PublishedState implements DocumentState {
@Override
public void edit(Document document, String content) {
System.out.println("Cannot edit published document");
}
@Override
public void submit(Document document) {
System.out.println("Document is already published");
}
@Override
public void approve(Document document) {
System.out.println("Document is already published");
}
@Override
public void reject(Document document, String reason) {
System.out.println("Cannot reject published document");
}
@Override
public void publish(Document document) {
System.out.println("Document is already published");
}
@Override
public String getStatusDescription() {
return "Published - Live and public";
}
@Override
public boolean canTransitionTo(Class<? extends DocumentState> targetState) {
return false; // Published is final state
}
}
Example 2: Media Player State Machine
// State interface for media player
interface PlayerState {
void play(MediaPlayer player);
void pause(MediaPlayer player);
void stop(MediaPlayer player);
void next(MediaPlayer player);
void previous(MediaPlayer player);
String getDisplayStatus();
}
// Context - Media Player
class MediaPlayer {
private PlayerState currentState;
private List<String> playlist;
private int currentTrackIndex;
private String currentTrack;
private long currentPosition;
private long totalDuration;
public MediaPlayer() {
this.playlist = new ArrayList<>();
this.currentTrackIndex = -1;
this.currentPosition = 0;
this.totalDuration = 0;
this.currentState = new StoppedState();
}
// Delegate to current state
public void play() { currentState.play(this); }
public void pause() { currentState.pause(this); }
public void stop() { currentState.stop(this); }
public void next() { currentState.next(this); }
public void previous() { currentState.previous(this); }
public void setState(PlayerState newState) {
this.currentState = newState;
System.out.println("Player state: " + newState.getDisplayStatus());
}
public void addToPlaylist(String track, long duration) {
playlist.add(track);
System.out.println("Added to playlist: " + track);
if (currentTrackIndex == -1 && !playlist.isEmpty()) {
currentTrackIndex = 0;
loadTrack(0);
}
}
public void loadTrack(int index) {
if (index >= 0 && index < playlist.size()) {
currentTrackIndex = index;
currentTrack = playlist.get(index);
currentPosition = 0;
totalDuration = (index + 1) * 60000; // Simulate track durations
System.out.println("Loaded track: " + currentTrack);
}
}
public boolean hasNextTrack() {
return currentTrackIndex < playlist.size() - 1;
}
public boolean hasPreviousTrack() {
return currentTrackIndex > 0;
}
public void moveToNextTrack() {
if (hasNextTrack()) {
loadTrack(currentTrackIndex + 1);
}
}
public void moveToPreviousTrack() {
if (hasPreviousTrack()) {
loadTrack(currentTrackIndex - 1);
}
}
public void simulatePlayback() {
if (currentState instanceof PlayingState) {
currentPosition += 1000; // Advance 1 second
if (currentPosition >= totalDuration) {
System.out.println("Track ended");
if (hasNextTrack()) {
moveToNextTrack();
setState(new PlayingState());
} else {
setState(new StoppedState());
}
}
}
}
// Getters
public String getCurrentTrack() { return currentTrack; }
public long getCurrentPosition() { return currentPosition; }
public long getTotalDuration() { return totalDuration; }
public String getStatus() { return currentState.getDisplayStatus(); }
public boolean isEmpty() { return playlist.isEmpty(); }
}
// Concrete States
class StoppedState implements PlayerState {
@Override
public void play(MediaPlayer player) {
if (player.isEmpty()) {
System.out.println("No tracks in playlist");
return;
}
if (player.getCurrentTrack() == null) {
player.loadTrack(0);
}
player.setState(new PlayingState());
System.out.println("Starting playback: " + player.getCurrentTrack());
}
@Override
public void pause(MediaPlayer player) {
System.out.println("Cannot pause when stopped");
}
@Override
public void stop(MediaPlayer player) {
System.out.println("Already stopped");
}
@Override
public void next(MediaPlayer player) {
if (player.hasNextTrack()) {
player.moveToNextTrack();
System.out.println("Skipped to next track: " + player.getCurrentTrack());
} else {
System.out.println("No next track available");
}
}
@Override
public void previous(MediaPlayer player) {
if (player.hasPreviousTrack()) {
player.moveToPreviousTrack();
System.out.println("Skipped to previous track: " + player.getCurrentTrack());
} else {
System.out.println("No previous track available");
}
}
@Override
public String getDisplayStatus() {
return "⏹️ Stopped";
}
}
class PlayingState implements PlayerState {
@Override
public void play(MediaPlayer player) {
System.out.println("Already playing");
}
@Override
public void pause(MediaPlayer player) {
player.setState(new PausedState());
System.out.println("Playback paused");
}
@Override
public void stop(MediaPlayer player) {
player.setState(new StoppedState());
System.out.println("Playback stopped");
}
@Override
public void next(MediaPlayer player) {
if (player.hasNextTrack()) {
player.moveToNextTrack();
System.out.println("Playing next track: " + player.getCurrentTrack());
} else {
player.setState(new StoppedState());
System.out.println("End of playlist reached");
}
}
@Override
public void previous(MediaPlayer player) {
if (player.hasPreviousTrack()) {
player.moveToPreviousTrack();
System.out.println("Playing previous track: " + player.getCurrentTrack());
} else {
System.out.println("Already at first track");
}
}
@Override
public String getDisplayStatus() {
return "▶️ Playing";
}
}
class PausedState implements PlayerState {
@Override
public void play(MediaPlayer player) {
player.setState(new PlayingState());
System.out.println("Resuming playback: " + player.getCurrentTrack());
}
@Override
public void pause(MediaPlayer player) {
System.out.println("Already paused");
}
@Override
public void stop(MediaPlayer player) {
player.setState(new StoppedState());
System.out.println("Playback stopped");
}
@Override
public void next(MediaPlayer player) {
if (player.hasNextTrack()) {
player.moveToNextTrack();
player.setState(new PlayingState());
System.out.println("Playing next track: " + player.getCurrentTrack());
} else {
System.out.println("No next track available");
}
}
@Override
public void previous(MediaPlayer player) {
if (player.hasPreviousTrack()) {
player.moveToPreviousTrack();
player.setState(new PlayingState());
System.out.println("Playing previous track: " + player.getCurrentTrack());
} else {
System.out.println("No previous track available");
}
}
@Override
public String getDisplayStatus() {
return "⏸️ Paused";
}
}
Example 3: TCP Connection State Machine
// State interface for TCP connection
interface TCPState {
void open(TCPConnection connection);
void close(TCPConnection connection);
void acknowledge(TCPConnection connection);
void send(TCPConnection connection, String data);
String getConnectionStatus();
}
// Context - TCP Connection
class TCPConnection {
private TCPState currentState;
private String localAddress;
private String remoteAddress;
private Queue<String> sendBuffer;
private Queue<String> receiveBuffer;
private long connectionStartTime;
public TCPConnection(String localAddress, String remoteAddress) {
this.localAddress = localAddress;
this.remoteAddress = remoteAddress;
this.sendBuffer = new LinkedList<>();
this.receiveBuffer = new LinkedList<>();
this.currentState = new ClosedState();
System.out.println("TCP Connection created: " + localAddress + " -> " + remoteAddress);
}
// Delegate operations to current state
public void open() { currentState.open(this); }
public void close() { currentState.close(this); }
public void acknowledge() { currentState.acknowledge(this); }
public void send(String data) { currentState.send(this, data); }
public void setState(TCPState newState) {
System.out.println("TCP State transition: " + currentState.getClass().getSimpleName() +
" -> " + newState.getClass().getSimpleName());
this.currentState = newState;
if (newState instanceof EstablishedState) {
connectionStartTime = System.currentTimeMillis();
}
}
public void addToSendBuffer(String data) {
sendBuffer.offer(data);
System.out.println("Added to send buffer: " + data);
}
public void addToReceiveBuffer(String data) {
receiveBuffer.offer(data);
System.out.println("Received data: " + data);
}
public String pollFromSendBuffer() {
return sendBuffer.poll();
}
public void processSendBuffer() {
while (!sendBuffer.isEmpty()) {
String data = sendBuffer.poll();
System.out.println("Transmitting: " + data + " from " + localAddress + " to " + remoteAddress);
}
}
public String getStatus() {
return currentState.getConnectionStatus();
}
public long getConnectionDuration() {
if (currentState instanceof EstablishedState) {
return System.currentTimeMillis() - connectionStartTime;
}
return 0;
}
// Getters
public String getLocalAddress() { return localAddress; }
public String getRemoteAddress() { return remoteAddress; }
public int getSendBufferSize() { return sendBuffer.size(); }
public int getReceiveBufferSize() { return receiveBuffer.size(); }
}
// Concrete States
class ClosedState implements TCPState {
@Override
public void open(TCPConnection connection) {
connection.setState(new ListenState());
System.out.println("Connection opening...");
}
@Override
public void close(TCPConnection connection) {
System.out.println("Connection is already closed");
}
@Override
public void acknowledge(TCPConnection connection) {
System.out.println("Cannot acknowledge in closed state");
}
@Override
public void send(TCPConnection connection, String data) {
System.out.println("Cannot send data: connection is closed");
}
@Override
public String getConnectionStatus() {
return "CLOSED";
}
}
class ListenState implements TCPState {
@Override
public void open(TCPConnection connection) {
System.out.println("Connection is already opening");
}
@Override
public void close(TCPConnection connection) {
connection.setState(new ClosedState());
System.out.println("Connection closed");
}
@Override
public void acknowledge(TCPConnection connection) {
connection.setState(new EstablishedState());
System.out.println("Connection established");
connection.processSendBuffer(); // Process any buffered data
}
@Override
public void send(TCPConnection connection, String data) {
System.out.println("Connection not established yet, buffering data");
connection.addToSendBuffer(data);
}
@Override
public String getConnectionStatus() {
return "LISTEN";
}
}
class EstablishedState implements TCPState {
@Override
public void open(TCPConnection connection) {
System.out.println("Connection is already established");
}
@Override
public void close(TCPConnection connection) {
connection.setState(new ClosingState());
System.out.println("Initiating connection close...");
}
@Override
public void acknowledge(TCPConnection connection) {
System.out.println("Acknowledgment received");
}
@Override
public void send(TCPConnection connection, String data) {
System.out.println("Sending data: " + data + " from " +
connection.getLocalAddress() + " to " + connection.getRemoteAddress());
// In real implementation, would actually transmit the data
}
@Override
public String getConnectionStatus() {
return "ESTABLISHED";
}
}
class ClosingState implements TCPState {
@Override
public void open(TCPConnection connection) {
System.out.println("Cannot open connection while closing");
}
@Override
public void close(TCPConnection connection) {
System.out.println("Connection is already closing");
}
@Override
public void acknowledge(TCPConnection connection) {
connection.setState(new ClosedState());
System.out.println("Connection fully closed");
}
@Override
public void send(TCPConnection connection, String data) {
System.out.println("Cannot send data: connection is closing");
}
@Override
public String getConnectionStatus() {
return "CLOSING";
}
}
Example 4: Usage Demonstration
public class StatePatternDemo {
public static void demonstrateDocumentWorkflow() {
System.out.println("=== Document Workflow State Machine ===");
Document doc = new Document("API Documentation", "John Doe");
// Try operations in different states
doc.edit("Initial API documentation content");
doc.submit();
System.out.println("Current status: " + doc.getStatus());
doc.approve();
doc.publish();
System.out.println("Final status: " + doc.getStatus());
// Try invalid operations
doc.edit("Cannot edit published document");
}
public static void demonstrateMediaPlayer() {
System.out.println("\n=== Media Player State Machine ===");
MediaPlayer player = new MediaPlayer();
// Add some tracks
player.addToPlaylist("Song 1", 180000);
player.addToPlaylist("Song 2", 200000);
player.addToPlaylist("Song 3", 165000);
// Demonstrate state transitions
player.play();
player.pause();
player.play();
player.next();
player.stop();
System.out.println("Player status: " + player.getStatus());
}
public static void demonstrateTCPConnection() {
System.out.println("\n=== TCP Connection State Machine ===");
TCPConnection connection = new TCPConnection("192.168.1.10", "192.168.1.20");
// Try to send data before connection is established
connection.send("Hello, World!");
// Establish connection
connection.open();
connection.acknowledge();
// Send data when connected
connection.send("Data packet 1");
connection.send("Data packet 2");
// Close connection
connection.close();
connection.acknowledge();
System.out.println("Final status: " + connection.getStatus());
}
public static void main(String[] args) {
demonstrateDocumentWorkflow();
demonstrateMediaPlayer();
demonstrateTCPConnection();
}
}
6. Hands-On Practice (30-40 minutes)
Exercise 1: Basic Practice (10 minutes)
Objective: Implement a simple traffic light state machine
Instructions:
- Create a TrafficLight class with states: Red, Yellow, Green
- Implement automatic transitions: Red -> Green -> Yellow -> Red
- Add manual override capability for emergency situations
- Include timing information for each state
Starter Code:
interface TrafficLightState {
void next(TrafficLight light);
void emergency(TrafficLight light);
String getColor();
int getDuration(); // seconds
}
class TrafficLight {
// TODO: Implement context with state management
}
Exercise 2: Intermediate Challenge (15 minutes)
Objective: Build an order processing state machine
Requirements:
- States: Pending, Confirmed, Paid, Shipped, Delivered, Cancelled
- Support different transition paths based on payment type
- Include validation rules for state transitions
- Add email notifications for state changes
- Handle cancellation from various states
Hints:
- Consider which states allow cancellation
- Think about partial payments and refunds
- Design notification system that works with state changes
Exercise 3: Creative Application (15 minutes)
Objective: Design a game character AI state machine
Challenge: Create an AI character with states:
- Idle, Patrolling, Chasing, Attacking, Fleeing, Searching
- Implement line-of-sight detection affecting state transitions
- Add health and energy levels that influence behavior
- Include decision-making based on player proximity and character health
Extension Ideas:
- Add personality traits that modify state behavior
- Implement memory system for remembering player locations
- Create hierarchical states (Combat vs Non-Combat main states)
7. Common Pitfalls and Troubleshooting (10 minutes)
Frequent Mistakes
-
Problem: States become too tightly coupled Cause: States directly referencing and creating other states Solution: Use state factory or let context manage transitions
-
Problem: Massive state classes with too much responsibility Cause: Putting too much logic in individual state classes Solution: Break down complex states or use state hierarchy
-
Problem: Inconsistent state transition validation Cause: Different states having different rules for same transitions Solution: Centralize transition validation or use state machine frameworks
Debugging Tips
- Log all state transitions with timestamps for debugging
- Implement state validation methods to check for invalid states
- Use visual state diagrams to understand complex transition flows
- Add debugging methods to inspect current state and available transitions
8. Best Practices and Professional Tips (5 minutes)
Industry Standards
- Keep states cohesive - each state should have a single, clear responsibility
- Make transitions explicit - clearly define what triggers each state change
- Handle invalid operations gracefully - don’t throw exceptions for expected invalid operations
- Document state diagrams - visual representations help team understanding
Design Principles
- Encapsulate state-specific behavior - avoid conditionals in context class
- Use enums for simple state machines - when behavior doesn’t vary much between states
- Consider state machine frameworks - for complex systems with many states
- Plan for extensibility - new states should be easy to add
Performance Considerations
- Cache state objects - if states are stateless, reuse instances
- Optimize transition checks - use maps or switch statements for fast lookups
- Consider state change frequency - optimize for common transitions
- Monitor state machine performance - track transition times and bottlenecks
Next Steps in Your Learning Journey
- Study Command pattern for undoable state transitions
- Learn Observer pattern for state change notifications
- Explore State machine frameworks like Spring State Machine or Akka
- Investigate Hierarchical state machines for complex systems
9. Summary and Review (5 minutes)
Key Takeaways
- State pattern encapsulates state-specific behavior in separate classes
- Eliminates complex conditional logic based on state
- Makes state transitions explicit and manageable
- Perfect for systems where behavior changes dramatically based on state
Quick Review Questions
- How does State pattern differ from Strategy pattern?
- When would you choose State pattern over simple if-else statements?
- How do you handle state transitions - in the state classes or the context?
For Instructors
Teaching Tips
- Start with simple state machines (traffic light, on/off switch) before complex ones
- Draw state transition diagrams on whiteboard to visualize the flow
- Show both automatic and manual state transitions
- Emphasize the elimination of conditional logic as the main benefit
- Use real-world examples students can relate to (vending machines, elevators)
Common Student Questions
- “Who should control state transitions?” - Discuss trade-offs between state-controlled vs context-controlled
- “How many states is too many?” - Talk about state explosion and hierarchical solutions
- “When to use State vs Strategy?” - Emphasize automatic vs manual selection
Assessment Ideas
- Design challenge: Create state machine for specific domain (ATM, game, workflow)
- Refactoring exercise: Convert complex conditional logic to state pattern
- Performance analysis: Compare state pattern vs switch statement approaches
Adaptation Notes
- Beginner focus: Start with simple binary states before complex multi-state systems
- Advanced students: Discuss state machine frameworks, hierarchical states, concurrent states
- Language-specific: Show language-specific optimizations (enums, pattern matching, etc.)
- Time management: TCP connection example can be simplified or extended based on available time