Skip to content
Code Kata
Go back

Kata: Observer Pattern

Suggest edit

Table of contents

Open Table of contents

Problem Statement

Implement an EventEmitter (observer pattern) that supports:

Concepts

The observer pattern defines a one-to-many dependency: when one object (the subject) changes state, all dependents (observers) are notified. The event emitter is its most common implementation.

TypeScript: Uses generics to type event maps. See TypeScript Core Concepts.

PHP: SPL provides SplSubject and SplObserver interfaces. See PHP Core Concepts.

Boilerplate

TypeScript

// event-emitter.ts
type Listener = (...args: any[]) => void;

class EventEmitter {
  private events: Map<string, Listener[]> = new Map();

  on(event: string, callback: Listener): void { /* TODO */ }
  off(event: string, callback: Listener): void { /* TODO */ }
  emit(event: string, ...args: any[]): void { /* TODO */ }
  once(event: string, callback: Listener): void { /* TODO */ }
}

Hints

Hint 1: on

Get or create the listener array for this event, then push the callback.

Hint 2: once

Wrap the callback in a function that calls off after the first invocation, then register the wrapper with on.

Solution

TypeScript

type Listener = (...args: any[]) => void;

class EventEmitter {
  private events: Map<string, Listener[]> = new Map();

  on(event: string, callback: Listener): void {
    if (!this.events.has(event)) {
      this.events.set(event, []);
    }
    this.events.get(event)!.push(callback);
  }

  off(event: string, callback: Listener): void {
    const listeners = this.events.get(event);
    if (!listeners) return;

    const index = listeners.indexOf(callback);
    if (index !== -1) {
      listeners.splice(index, 1);
    }
  }

  emit(event: string, ...args: any[]): void {
    const listeners = this.events.get(event);
    if (!listeners) return;

    for (const listener of [...listeners]) {
      listener(...args);
    }
  }

  once(event: string, callback: Listener): void {
    const wrapper: Listener = (...args) => {
      this.off(event, wrapper);
      callback(...args);
    };
    this.on(event, wrapper);
  }
}

Python

from collections import defaultdict
from typing import Any, Callable

Listener = Callable[..., Any]

class EventEmitter:
    def __init__(self) -> None:
        self._events: defaultdict[str, list[Listener]] = defaultdict(list)

    def on(self, event: str, callback: Listener) -> None:
        self._events[event].append(callback)

    def off(self, event: str, callback: Listener) -> None:
        listeners = self._events[event]
        if callback in listeners:
            listeners.remove(callback)

    def emit(self, event: str, *args: Any) -> None:
        for listener in list(self._events[event]):
            listener(*args)

    def once(self, event: str, callback: Listener) -> None:
        def wrapper(*args: Any) -> None:
            self.off(event, wrapper)
            callback(*args)
        self.on(event, wrapper)

PHP

class EventEmitter {
    /** @var array<string, array<callable>> */
    private array $events = [];

    public function on(string $event, callable $callback): void {
        $this->events[$event][] = $callback;
    }

    public function off(string $event, callable $callback): void {
        if (!isset($this->events[$event])) return;

        $this->events[$event] = array_values(
            array_filter(
                $this->events[$event],
                fn($listener) => $listener !== $callback
            )
        );
    }

    public function emit(string $event, mixed ...$args): void {
        foreach ($this->events[$event] ?? [] as $listener) {
            $listener(...$args);
        }
    }

    public function once(string $event, callable $callback): void {
        $wrapper = null;
        $wrapper = function (mixed ...$args) use ($event, $callback, &$wrapper): void {
            $this->off($event, $wrapper);
            $callback(...$args);
        };
        $this->on($event, $wrapper);
    }
}

Complexity Analysis

OperationTimeSpace
OnO(1)O(1)
OffO(n)O(1)
EmitO(n)O(n)
OnceO(1)O(1)

Where n = number of listeners for that event. The emit operation creates a copy of the listeners array to safely handle off calls during emission.

Cross-Language Notes

Node.js: The built-in EventEmitter class follows this exact pattern. It’s the foundation of Node’s event-driven architecture.

PHP: SPL provides SplSubject/SplObserver interfaces for a more formal observer pattern with attach(), detach(), and notify() methods. See PHP 8.x Updates for how attributes and enums can enhance this pattern.

Python: The asyncio module extends this pattern with async event loops. For typed events, consider the blinker library.

C#: C# has first-class language support for the observer pattern through event and delegate keywords, making it arguably the most elegant implementation.


Suggest edit
Share this post on:

Previous Post
Kata: Strategy Pattern
Next Post
Kata: Graph Representation and Traversal