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
CopyOnWriteArrayListto 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).