Implementing the Observer Pattern in Java for Event-Driven Systems

The Observer pattern is a foundational behavioral design pattern that enables loose coupling between components by defining a one-to-many dependency between objects: when one object (the subject) changes state, all its dependents (observers) are automatically notified and updated. This pattern underpins many event-handling systems in Java, including AWT/Swing listeners and modern reactive frameworks.

Core Components and Responsibilities

In a canonical implementation, the pattern consists of four key abstractions:

  • Subject: The observable entity maintaining a registry of observers and providing methods to attach, detach, and notify them.
  • ConcreteSubject: A concrete subclass that holds state and triggers notifications upon relevant state transitions.
  • Observer: A contract (typically an interface) declaring how observers react to updates—usually via a callback method.
  • ConcreteObserver: An implementing class that defines domain-specific logic executed during notification.

Refactored Java Implementation

Below is a modernized, type-safe implementation using generics and enhanced collections—avoiding raw types and unchecked casts found in legacy examples.

// Subject interface with generic payload support
public interface EventSource<T> {
    void register(EventListener<T> listener);
    void deregister(EventListener<T> listener);
    void broadcast(T event);
}

// Concrete subject managing numeric events
public class RandomNumberEmitter implements EventSource<Integer> {
    private final List<EventListener<Integer>> listeners = new CopyOnWriteArrayList<>();
    private final Random random = new Random();

    @Override
    public void register(EventListener<Integer> listener) {
        listeners.add(listener);
    }

    @Override
    public void deregister(EventListener<Integer> listener) {
        listeners.remove(listener);
    }

    @Override
    public void broadcast(Integer value) {
        listeners.forEach(listener -> listener.onEvent(value));
    }

    public void emitSequence(int count) {
        for (int i = 0; i < count; i++) {
            int next = random.nextInt(50);
            broadcast(next);
            sleepQuietly(1000);
        }
    }

    private void sleepQuietly(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

// Observer interface — renamed for clarity and aligned with Java conventions
public interface EventListener<T> {
    void onEvent(T data);
}

// Concrete observer rendering numbers as digits
public class NumericDisplay implements EventListener<Integer> {
    @Override
    public void onEvent(Integer number) {
        renderAsDigits(number);
    }

    private void renderAsDigits(Integer n) {
        System.out.printf("NumericDisplay: %d%n", n);
        sleepQuietly(1000);
    }

    private void sleepQuietly(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

// Concrete observer rendering numbers as bar charts
public class BarChartRenderer implements EventListener<Integer> {
    @Override
    public void onEvent(Integer number) {
        renderBarChart(number);
    }

    private void renderBarChart(Integer n) {
        System.out.print("BarChartRenderer: ");
        for (int i = 0; i < n; i++) {
            System.out.print("█");
        }
        System.out.println();
        sleepQuietly(1000);
    }

    private void sleepQuietly(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

Usage Example

The client code demonstrates decoupled composition: the emitter knows nothing about display logic, and observers remain agnostic of emission mechanics.

public class ObserverDemo {
    public static void main(String[] args) {
        RandomNumberEmitter source = new RandomNumberEmitter();
        EventListener<Integer> numericView = new NumericDisplay();
        EventListener<Integer> chartView = new BarChartRenderer();

        source.register(numericView);
        source.register(chartView);

        System.out.println("Starting emission sequence...");
        source.emitSequence(5);
        System.out.println("Emission complete.");
    }
}

Design Considerations

This implementation prioritizes:

  • Thread safety: Uses CopyOnWriteArrayList to allow safe iteration during concurrent registration/deregistration.
  • Type safety: Leverages generics to eliminate casting and improve compile-time guarantees.
  • Separation of concerns: Each observer encapsulates its own rendering logic, enabling independent testing and reuse.
  • Extensibility: New observers can be added without modifying the subject or existing observers—adhering to the Open/Closed Principle.

Note that while this manual implementation clarifies core concepts, production systems often adopt higher-level abstractions like java.util.Observer (deprecated), PropertyChangeListener, or reactive libraries such as Project Reactor or RxJava for richer event semantics (backpressure, error handling, composition).

Tags: observer-pattern java-design-patterns event-driven-architecture java-generics thread-safety

Posted on Mon, 24 Aug 2026 16:48:56 +0000 by XPertMailer