| Lesson 7 | Observer Pattern Variations |
| Objective | Implement a Java time subject that notifies registered observers when its state changes. |
Several displays may depend on the same time value. A lobby clock, an office clock, and an audit component all need to react when that value changes, but the class that stores the time should not contain display-specific or logging-specific code. The dependent objects also should not need to poll the time repeatedly to discover whether it has changed. The Observer pattern solves this problem by establishing a one-to-many relationship between a subject and a dynamic collection of observers.
In this example, Time is the subject and owns the current time. TimeObserver is the notification contract. The DigitalClock and AuditClock classes are concrete observers that respond to a time change in different ways. Each observer registers with the subject when it wants updates and unregisters when those updates are no longer needed. The subject knows the observers only through the TimeObserver interface, so it remains independent of their concrete behavior.
An observer contract specifies the callback that the subject invokes after a relevant state change. The callback receives a LocalTime value:
interface TimeObserver {
void timeChanged(LocalTime newTime);
}
Using LocalTime is clearer and safer than representing a time of day with an integer such as 1200 or 1230. A LocalTime value expresses the intended domain concept directly and supplies standard creation, comparison, and formatting behavior. Marking the interface with @FunctionalInterface also documents that it has one abstract operation and may be implemented with a class, method reference, or lambda expression.
The following single-file program implements the complete collaboration. It registers three observers, publishes one time change to all three, removes the lobby clock, and then publishes a second change only to the remaining observers.
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class TimeObserverDemo {
@FunctionalInterface
interface TimeObserver {
void timeChanged(LocalTime newTime);
}
static final class Time {
private final List<TimeObserver> observers = new ArrayList<>();
private LocalTime currentTime;
Time(LocalTime initialTime) {
currentTime = Objects.requireNonNull(initialTime);
}
LocalTime getCurrentTime() {
return currentTime;
}
void registerObserver(TimeObserver observer) {
Objects.requireNonNull(observer);
if (!observers.contains(observer)) {
observers.add(observer);
}
}
void unregisterObserver(TimeObserver observer) {
observers.remove(observer);
}
void setCurrentTime(LocalTime newTime) {
Objects.requireNonNull(newTime);
if (newTime.equals(currentTime)) {
return;
}
currentTime = newTime;
notifyRegisteredObservers();
}
private void notifyRegisteredObservers() {
for (TimeObserver observer : List.copyOf(observers)) {
observer.timeChanged(currentTime);
}
}
}
static final class DigitalClock implements TimeObserver {
private final String name;
DigitalClock(String name) {
this.name = Objects.requireNonNull(name);
}
@Override
public void timeChanged(LocalTime newTime) {
System.out.printf("%s displays %s%n", name, newTime);
}
}
static final class AuditClock implements TimeObserver {
@Override
public void timeChanged(LocalTime newTime) {
System.out.printf(
"Audit log recorded time change: %s%n", newTime);
}
}
public static void main(String[] args) {
Time time = new Time(LocalTime.of(12, 0));
DigitalClock lobbyClock = new DigitalClock("Lobby clock");
DigitalClock officeClock = new DigitalClock("Office clock");
AuditClock auditClock = new AuditClock();
time.registerObserver(lobbyClock);
time.registerObserver(officeClock);
time.registerObserver(auditClock);
time.setCurrentTime(LocalTime.of(12, 30));
time.unregisterObserver(lobbyClock);
time.setCurrentTime(LocalTime.of(13, 0));
}
}
The TimeObserver interface defines the stable boundary between the subject and every observer. The Time class does not import, construct, or test for DigitalClock or AuditClock. It can call timeChanged on any object that satisfies the contract. New observer types can therefore be added without modifying the subject.
The Time class stores its current LocalTime and a private list of observers. registerObserver rejects null and avoids adding the same observer object twice. unregisterObserver removes an observer when its lifecycle or interests change. This dynamic registration is essential because the set of objects interested in a subject often changes while an application is running.
The setCurrentTime method validates the proposed value and compares it with the current state. Supplying the same value causes an immediate return, because no meaningful change has occurred. After a real change, the method updates the state and calls notifyRegisteredObservers. This is selective notification in its simplest form: observers receive events only when the value they depend on actually changes.
The notification loop traverses List.copyOf(observers), which creates a stable snapshot for that notification pass. If a callback indirectly registers or unregisters an observer, it does not structurally modify the list currently being traversed. The change takes effect during a later notification. This snapshot is useful for predictable iteration, but it does not make the class thread-safe. Concurrent reads, writes, and notifications require an explicit synchronization or concurrency policy.
The concrete observers give the same event different meanings. A DigitalClock presents the new value, while AuditClock records the change. The main method acts as the client that composes these objects. It decides which observers belong to the collaboration without forcing the subject to manage application-specific components.
Lobby clock displays 12:30
Office clock displays 12:30
Audit log recorded time change: 12:30
Office clock displays 13:00
Audit log recorded time change: 13:00
The first update is multicast to three registered observers. After the lobby clock unregisters, it receives no further notification. The second update reaches only the office clock and the audit observer. This result shows that the relationship is maintained at runtime rather than being fixed in the subject's source code.
Observer has several useful variations. A design can use concrete or abstract subjects, push or pull state, multicast or unicast registration, and broad or selective notifications. These choices do not change the pattern's central intent, but they affect coupling, performance, and the information available to an observer.
A concrete subject is appropriate when one domain class owns both the state and the registration behavior. The example uses a concrete Time class because only one kind of subject is needed and placing the observer-list behavior there keeps the program easy to follow. The class can be used immediately without creating another subclass.
An abstract subject can be useful when several closely related subjects need identical registration and notification logic. The abstraction can maintain the observer collection while subclasses decide which state changes are significant. Inheritance is not the only option, however. A reusable helper object can provide registration behavior through composition without forcing otherwise unrelated domain classes into the same hierarchy.
In the push model, the subject sends changed state or an event object with the notification. This lesson uses push notification because timeChanged(LocalTime newTime) includes the new value. An observer can react without making a second call to the subject. The subject must decide which data is useful to all recipients, and a callback containing too much unrelated data can become difficult to maintain.
In the pull model, the notification indicates that something changed and the observer queries the subject for the state it needs. For example, a callback could receive the Time subject, and the observer could call getCurrentTime(). Pull notifications can be small and flexible, but observers become more aware of the subject's public interface and may perform several queries after one update.
Many systems use a hybrid approach. A notification carries essential context, such as an event type or changed value, while an observer pulls additional details only when necessary. The correct choice depends on the size of the state, the cost of querying it, and how much the observers should know about the subject.
The customary Observer relationship is multicast: one subject notifies zero or more registered observers. The time program demonstrates multicast by updating two displays and an audit component from one state change. Each observer can join or leave independently, and the subject does not need to know why any observer is interested.
A unicast variation permits at most one observer. The subject stores one callback instead of a collection and may reject or replace a second registration. Unicast can express exclusive ownership or a single completion handler, but it does not provide the one-to-many collaboration normally associated with Observer.
Not every internal mutation should trigger every observer. The Time subject suppresses a notification when the proposed time equals its current value. A more complex subject may expose separate observer interfaces for different categories, let observers subscribe to selected event types, include the changed property in an event object, or combine several small changes into one meaningful notification.
Consider a word-processing application. An observer responsible for saving document content may care when a user inserts or deletes text, but not when the viewport scrolls. A different observer may care only about view changes. Publishing events at the appropriate semantic level prevents unnecessary work and makes the relationship between an event and its subscribers easier to understand.
Observer reduces direct coupling between a state-owning subject and the components that respond to its changes. Observers can be registered and removed at runtime, one event can update several dependent components, and new observer classes can be introduced without changing the subject. The interface also makes each role easier to test independently.
The flexibility has costs. Notification order may become an unintended dependency. A slow observer can delay every later observer when callbacks run synchronously, and an exception from one observer can interrupt the notification pass unless the subject defines an error policy. Cascading updates may be difficult to trace, especially when one callback changes another subject. A long-lived subject can also retain an observer that is no longer needed if the observer fails to unregister.
Multithreaded software introduces further questions about safe publication, list modification, memory visibility, and whether callbacks may overlap. Those concerns require a deliberate concurrency design; snapshot iteration alone is not sufficient. Larger or asynchronous systems may instead use property-change listeners, application events, reactive streams, or messaging infrastructure when they require filtering, backpressure, persistence, retries, or communication across processes.
The GoF Observer pattern should not be confused with the old java.util.Observable class and java.util.Observer interface. Those library types were deprecated in Java 9, but the design pattern itself remains valuable. Modern Java programs commonly express it through application-specific callback interfaces, listener and event types, PropertyChangeSupport, reactive APIs, or message-oriented components. The example in this lesson uses an application-specific interface and does not extend the deprecated class.
Observer is more than a loop over callbacks. It is a dependency-management pattern in which a subject publishes meaningful state changes through a stable contract, while independently developed observers subscribe, react, and unsubscribe. The Time example makes registration, multicast delivery, selective notification, push-based state transfer, and observer removal visible in one compact program.
Apply the same roles and responsibilities by writing a Time class that uses the Observer pattern.
Time Observer Interface Exercise