How do I implement the observer pattern in EK9?

← Design Patterns and Idioms · Ref: Q262

EK9 implements the observer pattern using abstract functions as listener types and lists of function delegates as subscriber collections. This avoids the interface boilerplate of Java's Observer/Observable.

ABSTRACT FUNCTION AS LISTENER

Define the listener contract as an abstract function:

  Listener() as abstract
    -> event as String

This is the subscription interface. Any function matching this signature can be a listener.

EVENT EMITTER CLASS

Store listeners in a list and iterate to notify:

  EventEmitter
    listeners as List of Listener
    subscribe()
      -> listener as Listener
      listeners += listener
    emit()
      -> event as String
      for listener in listeners
        listener(event)

DYNAMIC FUNCTION SUBSCRIBERS

Create inline listeners with dynamic functions:

  printListener <- () is Listener as function
    Stdout().println(event)

The event parameter comes from the Listener abstract function signature.

CAPTURE FOR CONTEXT

Use explicit capture to add context to listeners:

  prefix <- "[AUDIT]"
  auditListener <- (prefix) is Listener as function
    Stdout().println(prefix + ": " + event)

The prefix is captured by value at creation time.

ADDING LISTENERS

Subscribe without modifying the emitter:

  emitter.subscribe(printListener)
  emitter.subscribe(auditListener)
  emitter.emit("user-login")

New listeners can be added at any time.

CONSUMER VS ACCEPTOR

Listeners that only read are consumers (pure). Listeners that cause side effects (printing, logging) are acceptors (impure). The abstract function's purity annotation controls this.

See Q54 for consumer and acceptor function types. See Q55 for function delegates. See Q115 for dynamic classes with capture. See Q214 for strategy pattern using similar function callbacks.

Example

defines module qa.patterns.observer

  defines function

    Listener() as abstract
      -> event as String

  defines class

    EventEmitter
      listeners as List of Listener: List() of Listener

      subscribe()
        -> listener as Listener
        listeners += listener

      emit()
        -> event as String
        for listener in listeners
          listener(event)

      subscriberCount() as pure
        <- rtn as Integer: length listeners

      default operator ?

  defines program

    ObserverPatternDemo()
      stdout <- Stdout()

      emitter <- EventEmitter()

      // === DYNAMIC FUNCTION LISTENERS ===

      printListener <- () is Listener as function
        Stdout().println(`Event received: ${event}`)

      // Listener with captured context
      prefix <- "[AUDIT]"
      auditListener <- (prefix) is Listener as function
        Stdout().println(`${prefix} ${event}`)

      // === SUBSCRIBE ===

      emitter.subscribe(printListener)
      emitter.subscribe(auditListener)

      stdout.println(`Subscribers: ${emitter.subscriberCount()}`)

      // === EMIT EVENTS ===

      emitter.emit("user-login")
      emitter.emit("data-saved")

      // === ADD MORE LISTENERS LATER ===

      countListener <- () is Listener as function
        Stdout().println(`Counted: ${event}`)

      emitter.subscribe(countListener)
      emitter.emit("final-event")

Common mistakes

E08180 — Class fields must be initialized inline. Declaring a field without an initializer triggers E08180. Always provide an initializer, such as 'List() of Listener' for an empty list. See ek9 -h E08180 for details.

Incorrect:

listeners as List of Listener

Correct:

listeners as List of Listener: List() of Listener

E50001 — Renaming the variable means later references to 'emitter' become unresolved, triggering E50001. Variable names must be consistent throughout the scope. See ek9 -h E50001 for details.

Incorrect:

emitterXYZ <- EventEmitter()

Correct:

emitter <- EventEmitter()
Other ways to ask this
  • How do I use events and listeners in EK9?
  • How do I implement publish-subscribe in EK9?
  • How do function delegates work as event listeners?

Coming from another language?

Java: Observer/Observable (deprecated), listeners via interfaces, anonymous inner classes, lambda expressions since Java 8, EventListener pattern. Python: no built-in observer, callback functions or third-party libraries. JavaScript: EventEmitter (Node.js), addEventListener (DOM), callback-heavy. Rust: no built-in observer, channels or callback closures. Go: channels for pub/sub, callback functions. EK9: abstract functions as listener types, List of Listener for subscriber management, dynamic functions with capture for inline listeners.

Keywords: pattern, idiom, design, migrate, emit, delegate, publish, event, observer, listener, subscribe, callback, notify