Observer
Behavioral Design Patterns
Imagine you're building an e-commerce application. You have a Product
class that tracks inventory. When a popular product comes back in stock, several different parts of the system need to react:
- An
EmailService
needs to send "back in stock" email alerts to interested customers. - An
SmsService
needs to send text message notifications. - An
InventoryDashboard
UI component needs to update its display to show the new stock level.
A naive approach would be to put all this logic inside the Product
class's setStockLevel
method:
public class Product {
private int stockLevel;
// BAD: The Product is tightly coupled to other services
private EmailService emailService = new EmailService();
private SmsService smsService = new SmsService();
public void setStockLevel(int newStockLevel) {
if (this.stockLevel == 0 && newStockLevel > 0) {
// The Product knows way too much about other parts of the system
emailService.sendBackInStockAlerts(this);
smsService.sendBackInStockTexts(this);
// InventoryDashboard.update(this);
}
this.stockLevel = newStockLevel;
}
}
This is a poor design. The Product
class, whose single responsibility should be managing product data, is now tightly coupled to the notification and UI systems. If you want to add a new PushNotificationService
, you have to modify the Product
class, violating the Open/Closed Principle.
A Subscription Notification System
The Observer is a behavioral design pattern that defines a one-to-many dependency between objects. When one object (the "Subject") changes its state, all of its dependents (the "Observers") are notified and updated automatically.
The core idea is to create a subscription model.
- The Subject is the object that has the interesting state. It maintains a list of subscribers.
- Observers are the objects that want to be notified of the Subject's changes. They "subscribe" to the Subject.
When the Subject's state changes, it simply iterates through its list of subscribers and calls an update()
method on each one. The Subject doesn't know or care what the observers are or what they do; it only knows that they implement a common Observer
interface. This completely decouples the Subject from its Observers.
The Participants
- Subject: An interface or abstract class that declares methods for managing subscribers (e.g.,
subscribe()
,unsubscribe()
) and a method to notify them (notifyObservers()
). - Concrete Subject: The class that implements the
Subject
interface. It maintains the list of observers and callsnotifyObservers()
when its state changes. - Observer: The interface that all observers must implement. It typically declares a single
update()
method. - Concrete Observer: A class that implements the
Observer
interface. It registers with aSubject
and defines the specific action to take when it receives a notification.
Example: A Product Stock Alert System
Let's refactor our e-commerce example using the Observer pattern.
// The Observer interface
public interface IObserver {
void update(String productName);
}
// The Subject interface
public interface ISubject {
void subscribe(IObserver observer);
void unsubscribe(IObserver observer);
void notifyObservers();
}
// Concrete Subject
public class Product implements ISubject {
private List<IObserver> observers = new ArrayList<>();
private String name;
private boolean inStock = false;
public Product(String name) { this.name = name; }
// When the state changes to "in stock", notify everyone.
public void setInStock(boolean isInStock) {
if (this.inStock == false && isInStock == true) {
this.inStock = true;
System.out.println(this.name + " is now back in stock!");
notifyObservers();
}
this.inStock = isInStock;
}
@Override
public void subscribe(IObserver observer) { observers.add(observer); }
@Override
public void unsubscribe(IObserver observer) { observers.remove(observer); }
@Override
public void notifyObservers() {
for (IObserver observer : observers) {
observer.update(this.name);
}
}
}
// Concrete Observers
public class EmailAlertObserver implements IObserver {
private String userEmail;
public EmailAlertObserver(String email) { this.userEmail = email; }
@Override
public void update(String productName) {
System.out.println("Email to " + userEmail + ": " + productName + " is back in stock!");
}
}
public class SmsAlertObserver implements IObserver {
private String userPhone;
public SmsAlertObserver(String phone) { this.userPhone = phone; }
@Override
public void update(String productName) {
System.out.println("SMS to " + userPhone + ": " + productName + " is back in stock!");
}
}
// The Client, which wires everything up
public class Client {
public static void main(String[] args) {
// The subject
Product iPhone = new Product("iPhone 15");
// The observers
IObserver user1 = new EmailAlertObserver("jasir@email.com");
IObserver user2 = new SmsAlertObserver("+91-1234567890");
IObserver user3 = new EmailAlertObserver("elon@x.com");
// Users subscribe to the product
iPhone.subscribe(user1);
iPhone.subscribe(user2);
iPhone.subscribe(user3);
// A different product they don't care about
Product keyboard = new Product("Mechanical Keyboard");
keyboard.setInStock(true); // No notifications are sent
System.out.println("\n--- iPhone comes back in stock ---");
// When the iPhone's state changes, all subscribed observers are notified.
iPhone.setInStock(true);
}
}
Interview Focus: Analysis and Trade-offs
Benefits:
- Loose Coupling: The Subject and Observers are very loosely coupled. The Subject only knows that its observers implement the
IObserver
interface. - Open/Closed Principle: You can introduce new
ConcreteObserver
classes at any time without ever modifying theSubject
's code. - Dynamic Relationships: You can establish and remove relationships between subjects and observers at runtime.
Drawbacks:
- Unexpected Update Order: Observers are notified in an unpredictable order. You should not rely on one observer's update finishing before another's begins.
- Memory Leaks (Lapsed Listener Problem): If an observer subscribes but is later destroyed without unsubscribing, the Subject will continue to hold a "lapsed" reference to it. This prevents the observer from being garbage collected and creates a memory leak.
How Observer Relates to Other Concepts
- Publish/Subscribe (Pub/Sub): The Observer pattern is the foundation of the broader Pub/Sub pattern. While classic Observer involves direct subscription, a full Pub/Sub system often introduces a central "message broker" or "event bus." This decouples publishers from subscribers even further—they don't need to know about each other at all; they just talk to the broker.
- Event-Driven Architecture: This pattern is a cornerstone of event-driven systems, where components react to events rather than being invoked in a rigid, procedural sequence.
- Model-View-Controller (MVC): The Observer pattern is famously used to synchronize the Model (data) and the View (UI). The View "observes" the Model. When data in the Model changes (e.g., due to a database update), it notifies the View, which then redraws itself to display the new data.
- Modern Implementations: While older Java versions had a built-in
java.util.Observer
, it is now deprecated. The core idea is now implemented through event listener systems (e.g., in JavaFX/Swing) and powerful reactive programming libraries like RxJava and Project Reactor.