Skip to main content

Observer Pattern

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Problem

You need to notify multiple objects when another object's state changes, without coupling them tightly together.

Common Scenarios:

  • Event handling systems (GUI components)
  • Model-View-Controller (MVC) architecture
  • Real-time data feeds (stock prices, weather)
  • Notification systems (social media, email)

Design Principles Applied

  1. Strive for loosely coupled designs - Subject and observers are loosely coupled
  2. Program to an interface - Use Observer and Subject interfaces
  3. Open/Closed Principle - Add new observers without modifying subject

UML Diagram

Implementation

Example 1: Weather Station

A classic example from Head First Design Patterns.

// Observer interface - all observers must implement this
public interface Observer {
void update(float temperature, float humidity, float pressure);
}

// Subject interface - manages observers
public interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}

// Display interface for our weather displays
public interface DisplayElement {
void display();
}

Concrete Subject

// WeatherData is the Subject that observers register with
public class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;

public WeatherData() {
observers = new ArrayList<>();
}

@Override
public void registerObserver(Observer o) {
observers.add(o);
System.out.println("Observer registered: " + o.getClass().getSimpleName());
}

@Override
public void removeObserver(Observer o) {
observers.remove(o);
System.out.println("Observer removed: " + o.getClass().getSimpleName());
}

@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature, humidity, pressure);
}
}

// Called when weather measurements have been updated
public void measurementsChanged() {
notifyObservers();
}

// For testing - set new measurements
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}

// Getters for observers that want to pull data
public float getTemperature() { return temperature; }
public float getHumidity() { return humidity; }
public float getPressure() { return pressure; }
}

Concrete Observers

// Current Conditions Display
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
private Subject weatherData;

public CurrentConditionsDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}

@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}

@Override
public void display() {
System.out.println("Current conditions: " + temperature +
"°F and " + humidity + "% humidity");
}
}

// Statistics Display
public class StatisticsDisplay implements Observer, DisplayElement {
private float maxTemp = Float.MIN_VALUE;
private float minTemp = Float.MAX_VALUE;
private float tempSum = 0.0f;
private int numReadings = 0;
private Subject weatherData;

public StatisticsDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}

@Override
public void update(float temperature, float humidity, float pressure) {
tempSum += temperature;
numReadings++;

if (temperature > maxTemp) {
maxTemp = temperature;
}

if (temperature < minTemp) {
minTemp = temperature;
}

display();
}

@Override
public void display() {
System.out.println("Avg/Max/Min temperature = " +
(tempSum / numReadings) + "/" + maxTemp + "/" + minTemp);
}
}

// Forecast Display
public class ForecastDisplay implements Observer, DisplayElement {
private float currentPressure = 29.92f;
private float lastPressure;
private Subject weatherData;

public ForecastDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}

@Override
public void update(float temperature, float humidity, float pressure) {
lastPressure = currentPressure;
currentPressure = pressure;
display();
}

@Override
public void display() {
System.out.print("Forecast: ");
if (currentPressure > lastPressure) {
System.out.println("Improving weather on the way!");
} else if (currentPressure == lastPressure) {
System.out.println("More of the same");
} else {
System.out.println("Watch out for cooler, rainy weather");
}
}
}

// Heat Index Display
public class HeatIndexDisplay implements Observer, DisplayElement {
private float heatIndex = 0.0f;
private Subject weatherData;

public HeatIndexDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}

@Override
public void update(float t, float rh, float pressure) {
heatIndex = computeHeatIndex(t, rh);
display();
}

private float computeHeatIndex(float t, float rh) {
return (float) ((16.923 + (0.185212 * t) + (5.37941 * rh) -
(0.100254 * t * rh) + (0.00941695 * (t * t)) +
(0.00728898 * (rh * rh)) + (0.000345372 * (t * t * rh)) -
(0.000814971 * (t * rh * rh)) +
(0.0000102102 * (t * t * rh * rh)) -
(0.000038646 * (t * t * t)) + (0.0000291583 * (rh * rh * rh)) +
(0.00000142721 * (t * t * t * rh)) +
(0.000000197483 * (t * rh * rh * rh)) -
(0.0000000218429 * (t * t * t * rh * rh)) +
0.000000000843296 * (t * t * rh * rh * rh)) -
(0.0000000000481975 * (t * t * t * rh * rh * rh)));
}

@Override
public void display() {
System.out.println("Heat index is " + heatIndex);
}
}

Test Code

public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();

// Create displays (they auto-register with weatherData)
CurrentConditionsDisplay currentDisplay =
new CurrentConditionsDisplay(weatherData);
StatisticsDisplay statisticsDisplay =
new StatisticsDisplay(weatherData);
ForecastDisplay forecastDisplay =
new ForecastDisplay(weatherData);
HeatIndexDisplay heatIndexDisplay =
new HeatIndexDisplay(weatherData);

// Simulate new weather measurements
System.out.println("\n=== First Reading ===");
weatherData.setMeasurements(80, 65, 30.4f);

System.out.println("\n=== Second Reading ===");
weatherData.setMeasurements(82, 70, 29.2f);

System.out.println("\n=== Third Reading ===");
weatherData.setMeasurements(78, 90, 29.2f);
}
}

Output

Observer registered: CurrentConditionsDisplay
Observer registered: StatisticsDisplay
Observer registered: ForecastDisplay
Observer registered: HeatIndexDisplay

=== First Reading ===
Current conditions: 80.0°F and 65.0% humidity
Avg/Max/Min temperature = 80.0/80.0/80.0
Forecast: Improving weather on the way!
Heat index is 82.95535

=== Second Reading ===
Current conditions: 82.0°F and 70.0% humidity
Avg/Max/Min temperature = 81.0/82.0/80.0
Forecast: Watch out for cooler, rainy weather
Heat index is 86.90124

=== Third Reading ===
Current conditions: 78.0°F and 90.0% humidity
Avg/Max/Min temperature = 80.0/82.0/78.0
Forecast: More of the same
Heat index is 83.64967

Example 2: Stock Market Ticker

// Stock data (Subject)
public class Stock implements Subject {
private List<Observer> observers;
private String symbol;
private double price;
private double change;

public Stock(String symbol, double initialPrice) {
this.symbol = symbol;
this.price = initialPrice;
this.change = 0.0;
this.observers = new ArrayList<>();
}

public void setPrice(double newPrice) {
double oldPrice = this.price;
this.price = newPrice;
this.change = newPrice - oldPrice;
notifyObservers();
}

public String getSymbol() { return symbol; }
public double getPrice() { return price; }
public double getChange() { return change; }

@Override
public void registerObserver(Observer o) {
observers.add(o);
}

@Override
public void removeObserver(Observer o) {
observers.remove(o);
}

@Override
public void notifyObservers() {
for (Observer observer : observers) {
if (observer instanceof StockObserver) {
((StockObserver) observer).update(symbol, price, change);
}
}
}
}

// Stock Observer interface
public interface StockObserver {
void update(String symbol, double price, double change);
}

// Investor (Observer)
public class Investor implements StockObserver {
private String name;
private Map<String, Double> portfolio;

public Investor(String name) {
this.name = name;
this.portfolio = new HashMap<>();
}

public void buyStock(Stock stock, double shares) {
portfolio.put(stock.getSymbol(), shares);
stock.registerObserver(this);
System.out.println(name + " bought " + shares +
" shares of " + stock.getSymbol());
}

@Override
public void update(String symbol, double price, double change) {
if (portfolio.containsKey(symbol)) {
double shares = portfolio.get(symbol);
double value = shares * price;
double changeValue = shares * change;

String direction = change > 0 ? "↑" : change < 0 ? "↓" : "→";

System.out.printf("%s: %s %s $%.2f (%.2f%%) | Portfolio value: $%.2f (%+.2f)%n",
name, symbol, direction, price,
(change / (price - change)) * 100,
value, changeValue);
}
}
}

// Stock Market Simulator
public class StockMarketSimulator {
public static void main(String[] args) {
// Create stocks
Stock apple = new Stock("AAPL", 150.00);
Stock tesla = new Stock("TSLA", 700.00);

// Create investors
Investor alice = new Investor("Alice");
Investor bob = new Investor("Bob");

// Investors buy stocks (auto-registers them as observers)
alice.buyStock(apple, 10);
alice.buyStock(tesla, 5);
bob.buyStock(apple, 20);

System.out.println("\n=== Market Update 1 ===");
apple.setPrice(155.50); // +3.67%

System.out.println("\n=== Market Update 2 ===");
tesla.setPrice(685.00); // -2.14%

System.out.println("\n=== Market Update 3 ===");
apple.setPrice(152.25); // -2.09%
}
}

Push vs. Pull

Push Model (Used Above)

Subject pushes data to observers in the update() method.

// Observer gets all data pushed to it
public interface Observer {
void update(float temp, float humidity, float pressure);
}

Pros: Simple, observers get exactly what they need Cons: Less flexible, must change interface if data changes

Pull Model

Subject notifies observers, they pull data they need.

// Observer pulls data from subject
public interface Observer {
void update(Subject subject);
}

public class CurrentConditionsDisplay implements Observer {
@Override
public void update(Subject subject) {
if (subject instanceof WeatherData) {
WeatherData weatherData = (WeatherData) subject;
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
display();
}
}
}

Pros: More flexible, observers choose what data to pull Cons: Observers coupled to concrete Subject type

Benefits

Loose coupling

  • Subject and observers are loosely coupled
  • Subject only knows observers implement an interface

Dynamic relationships

  • Add/remove observers at runtime
  • Subject doesn't need to know number of observers

Broadcast communication

  • Subject doesn't need to know who or how many observers
  • All registered observers get notified

Reusability

  • Subjects and observers can be reused independently

Drawbacks

Unexpected updates

  • Observers don't know about each other
  • Can cause cascading updates

Memory leaks

  • Observers must be explicitly removed
  • Forgotten observers keep references alive

Update order

  • No guarantee of notification order
  • Can be problematic for dependent observers

When to Use

Use Observer When:

  • One object changes and others need to be notified
  • You want loose coupling between objects
  • Number of dependents is unknown or dynamic
  • Objects need to be notified without knowing who they are

Don't Use When:

  • There's only one dependent object
  • Updates happen very frequently (performance concern)
  • Order of notification matters critically

Real-World Examples

Java Built-in Observer (Deprecated in Java 9)

import java.util.Observable;
import java.util.Observer;

// Using Java's built-in classes (legacy - don't use in new code)
public class WeatherData extends Observable {
private float temperature;
private float humidity;
private float pressure;

public void measurementsChanged() {
setChanged(); // Mark as changed
notifyObservers(); // Notify observers
}

public void setMeasurements(float temp, float humidity, float pressure) {
this.temperature = temp;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}

// Getters...
}

public class CurrentConditionsDisplay implements Observer {
@Override
public void update(Observable obs, Object arg) {
if (obs instanceof WeatherData) {
WeatherData weatherData = (WeatherData) obs;
// Pull data and display
}
}
}

JavaBeans PropertyChangeListener

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

public class Person {
private String name;
private PropertyChangeSupport support;

public Person() {
support = new PropertyChangeSupport(this);
}

public void addPropertyChangeListener(PropertyChangeListener listener) {
support.addPropertyChangeListener(listener);
}

public void removePropertyChangeListener(PropertyChangeListener listener) {
support.removePropertyChangeListener(listener);
}

public void setName(String name) {
String oldName = this.name;
this.name = name;
support.firePropertyChange("name", oldName, name);
}
}

Swing Event Listeners

JButton button = new JButton("Click me");

// Observer pattern - ActionListener is the observer
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Button clicked!");
}
});

// Modern Java with lambda
button.addActionListener(e -> System.out.println("Clicked!"));
  • Mediator: Centralizes communication, Observer distributes it
  • Singleton: Often used for Subject in observer pattern
  • MVC Architecture: Observer is fundamental to Model-View separation

Best Practices

  1. Always remove observers when they're no longer needed
// Good - cleanup in dispose/close method
public void dispose() {
weatherData.removeObserver(this);
}
  1. Use weak references to prevent memory leaks
private List<WeakReference<Observer>> observers = new ArrayList<>();
  1. Consider thread safety for multi-threaded applications
private final List<Observer> observers =
Collections.synchronizedList(new ArrayList<>());
  1. Use Java 9+ alternatives to deprecated Observable
// Use PropertyChangeSupport, Flow API, or reactive libraries
import java.util.concurrent.Flow.*;

Summary

The Observer Pattern is essential for:

  • Event-driven programming
  • MVC architecture
  • Real-time data updates
  • Loosely coupled designs

Key Takeaway: One-to-many dependency where subjects notify observers of state changes without tight coupling.