GoF Design Patterns: Behavioral Patterns (4) - Iterator, State | TypeScript Examples

Design PatternsGoFBehavioral PatternsIterator PatternState PatternObject-OrientedTypeScript
Read in about 5 min read
Published: 2025-07-05
Last modified: 2025-07-05
View count: 41

Summary

This is the fourth part of the Behavioral Patterns series in GoF Design Patterns. Learn the concepts and TypeScript examples of the Iterator pattern, which provides a way to access the elements of a collection sequentially without exposing its underlying representation, and the State pattern, which allows an object to alter its behavior when its internal state changes.

In the third part on Behavioral Patterns, we explored the Chain of Responsibility Pattern and the Memento Pattern, learning how to process requests with multiple objects and how to safely save an object's state.

In this post, we will learn an elegant way to handle collection data and a sophisticated method for changing an object's behavior based on its state.

  • 🚶‍♂️ Iterator Pattern: Provides a way to access the elements of a collection sequentially without exposing its underlying implementation.
  • 🚦 State Pattern: Allows an object to alter its behavior when its internal state changes. It appears as if the object's class has changed.

Let's explore how these patterns can reduce code complexity and improve maintainability!

🚶‍♂️ 7. Iterator Pattern

The Iterator pattern is a behavioral design pattern that lets you traverse elements of a collection without exposing its underlying representation (like an array, list, or tree).

The easiest analogy is the 'channel buttons on a TV remote'. We just press the 'next' button (▶) to move to the next channel. We don't need to know how the list of broadcast frequencies is stored or what data structure is used to manage it. The Iterator pattern, in this way, hides the complex internals of a collection and allows access to data through a simple interface like 'next()' and 'isDone()'.

In fact, this pattern is already deeply integrated into modern programming languages. JavaScript's for...of loop, array methods like map and filter, and Java's Iterator interface are all implementations of the Iterator pattern.

🏗️ Basic Structure

  • 📜 Iterator: The common interface for iterators. It usually defines a next() method to retrieve the next element and a hasNext() method (or includes a done property in the return value of next()) to check if the traversal is complete.
  • ConcreteIterator: A concrete class that implements the Iterator interface. It implements the traversal logic for a specific collection and keeps track of the current position in the traversal.
  • 📦 Aggregate: The common interface for collections, which has a createIterator() method.
  • ConcreteAggregate: A concrete collection class that implements the Aggregate interface. In its createIterator() method, it creates and returns an instance of a ConcreteIterator suitable for itself.

💻 Example: Implementing the JavaScript Iterable Protocol

In JavaScript/TypeScript, you can implement the Iterator pattern directly to use the for...of syntax. An object with a special method [Symbol.iterator] is called 'iterable'.

Let's create a simple WordCollection class that iterates through words in alphabetical order.

typescript
// Aggregate interface (for reference, explicit implementation is not needed in TS due to structural typing)
interface Iterable<T> {
  [Symbol.iterator](): Iterator<T>;
}

// Iterator interface (for reference)
interface Iterator<T> {
  next(): { value: T; done: boolean };
}

// ConcreteAggregate: The word collection
class WordCollection {
  private words: string[] = [];

  add(word: string): void {
    this.words.push(word);
  }

  // The method to create an iterator
  [Symbol.iterator](): Iterator<string> {
    let index = 0;
    const sortedWords = [...this.words].sort(); // Sort alphabetically

    // ConcreteIterator logic (implemented as an anonymous object)
    return {
      next: (): { value: string; done: boolean } => {
        if (index < sortedWords.length) {
          return {
            value: sortedWords[index++],
            done: false,
          };
        } else {
          return {
            value: undefined,
            done: true,
          };
        }
      },
    };
  }
}

// Client Code
const collection = new WordCollection();
collection.add("TypeScript");
collection.add("GoF");
collection.add("Design Patterns");
collection.add("Iterator");

console.log("Iterating through the word collection:");
// Since WordCollection follows the iterable protocol, for...of can be used
for (const word of collection) {
  console.log(word);
}
// Output:
// Design Patterns
// GoF
// Iterator
// TypeScript

The client doesn't need to know how the words are stored in the words array inside WordCollection or how the sorting happens. It can simply access each element of the collection one by one using a standard method like for...of. This is the separation of 'access' and 'implementation' that the Iterator pattern provides.

Key Takeaways for Iterator Pattern 🔑

  • 🚶‍♂️ Accesses elements of a collection sequentially without exposing its internal structure.
  • 🤝 Decouples the access method from the collection implementation, reducing coupling.
  • 🔄 Can provide multiple ways of traversing a single collection (e.g., forward and backward iterators).
  • 💡 Many features of modern languages, like for...of, map, and filter, are based on this pattern.

🚦 8. State Pattern

The State pattern is a behavioral design pattern that allows an object to change its behavior when its internal state changes. Using this pattern, the object appears to change its class. It allows you to clean up code filled with if/else or switch statements for state management by separating them into distinct classes representing the state.

Think of a 'traffic light' 🚦. A traffic light has states like 'Green', 'Yellow', and 'Red'. The behavior of the traffic light (changing to the next state) is completely different in each state. When it's green, it changes to yellow; when it's yellow, it changes to red; and when it's red, it changes to green. The State pattern encapsulates the behaviors associated with particular states into separate classes and simply switches the class responsible for the behavior when the state changes.

🏗️ Basic Structure

  • 🎭 Context: The main object that has a state. It holds a reference to a State object that represents its current state. When a client makes a request, it delegates the action to the current State object. It also provides a method like setState() to allow state objects to change the Context's state.
  • 📜 State: The common interface for all concrete state classes. It defines methods for the actions that the Context will delegate (e.g., handle()).
  • ConcreteState: A concrete class that implements the State interface. It implements the behavior for a specific state and, if necessary, decides and changes the next state of the Context.

💻 Example: Online Document Publishing Workflow

Let's create an online document object that has 'Draft' -> 'Moderation' -> 'Published' states.

First, let's define the DocumentState interface that all states will follow.

typescript
// State interface
interface DocumentState {
  publish(): void;
}

Next, let's create the Context object, the Document class, which will have a state.

typescript
// Context: The document object that has a state
class Document {
  private state: DocumentState;

  constructor() {
    // The initial state is 'Draft'
    this.state = new DraftState(this);
  }

  // Method for changing the state
  changeState(state: DocumentState): void {
    this.state = state;
  }

  // Delegate the action to the current state
  publish(): void {
    this.state.publish();
  }
}

Now, let's create the ConcreteState classes that define the behavior for each state.

typescript
// ConcreteState 1: Draft State
class DraftState implements DocumentState {
  constructor(private document: Document) {}

  publish(): void {
    console.log("Changing state from [Draft] -> [Moderation].");
    this.document.changeState(new ModerationState(this.document));
  }
}

// ConcreteState 2: Moderation State
class ModerationState implements DocumentState {
  constructor(private document: Document) {}

  publish(): void {
    console.log(
      "Changing state from [Moderation] -> [Published]. The article is now public!"
    );
    this.document.changeState(new PublishedState(this.document));
  }
}

// ConcreteState 3: Published State
class PublishedState implements DocumentState {
  constructor(private document: Document) {}

  publish(): void {
    console.log(
      "[Published] The article is already published. No action taken."
    );
    // No state change
  }
}

The client code only needs to call the publish() method of the Document object.

typescript
// Client Code
const myDocument = new Document();

console.log("--- 1. Attempting to publish from Draft state ---");
myDocument.publish(); // Draft -> Moderation

console.log("\n--- 2. Attempting to publish from Moderation state ---");
myDocument.publish(); // Moderation -> Published

console.log("\n--- 3. Attempting to publish from Published state ---");
myDocument.publish(); // Nothing happens

There are no conditional statements like if (state === 'draft') { ... } else if (state === 'moderation') { ... } inside the Document class. All state-related logic is delegated to the respective State classes. If a new 'Rejected' state is needed, we can simply create a RejectedState class and add it, thus adhering to the 'Open-Closed Principle (OCP)'.

Key Takeaways for State Pattern 🔑

  • 🚦 Encapsulates state-specific behavior into separate classes.
  • 🎭 Allows an object to change its behavior based on its internal state (as if its class changes).
  • 🧹 Is very effective for cleaning up state machine code filled with complex if/else or switch statements.
  • 💡 The responsibility for state transitions lies within the individual State classes, not the Context, making the rules clearer.

Over these four parts, we have covered eight key behavioral patterns. In the next series, we will wrap up by covering the remaining behavioral patterns!