This article is entirely based on a YouTube video by Christopher Okhravi. The original book was not consulted for this design pattern.
The Observer Pattern addresses a common scenario in software design where multiple objects need to be informed about a change in the state of another object. This pattern facilitates a one-to-many dependency between objects, ensuring that when one object (the observable) changes its state, all its dependent objects (the observers) are automatically notified and updated.
Consider a system where one entity’s state changes over time. For instance, a weather station collects data that is continuously updated. Other entities, such as displays or logging systems, might need to react to these changes. The Observer pattern provides a structured way for these dependent entities to be notified whenever the weather data is updated.
The core problem the Observer Pattern solves is the need to avoid tight coupling between the observable object and its dependents. If the observable directly knew about and called methods on its dependents, any change to the set of dependents would require modifications to the observable itself. This lack of flexibility and potential for code duplication motivates the use of the Observer pattern.
Push vs. Polling (Pull-based)
The Observer pattern typically implements a push-based mechanism: when the observable’s state changes, it actively notifies all registered observers, optionally sending the updated data along with the notification. This contrasts with polling, a pull-based approach in which observers repeatedly query the observable to check whether its state has changed. Polling can be inefficient when changes are infrequent, as observers waste resources checking for updates, and it may miss rapid state changes between polling intervals.
By pushing notifications, the Observer pattern ensures timely updates without unnecessary overhead.
Core Concepts: Observable and Observer
The Observer pattern revolves around two key interfaces or abstract classes:
- Observable (or Subject): This is the object whose state is of interest. It maintains a list of observers and provides methods for attaching (registering) and detaching (unregistering) observers. When its state changes, the observable notifies all registered observers.
- Observer: This interface defines a method that is called when the observable’s state changes. Concrete observer classes implement this interface to react to the notifications in their own specific way.
UML Diagram
The UML diagram for the Observer pattern typically includes the following elements:
IObservable(orISubject): An interface defining methods for managing observers, such asaddObserver(IObserver),removeObserver(IObserver), andnotifyObservers().ConcreteObservable: A concrete class that implements theIObservableinterface. It maintains the state that is of interest and triggers notifications to its observers when this state changes. It holds a collection ofIObserverinstances.IObserver: An interface defining theupdate()method, which is called by the observable to notify observers of a change.ConcreteObserver: Concrete classes that implement theIObserverinterface. Each concrete observer provides its own specific implementation of theupdate()method to handle the notification and react to the observable’s state change.
The 0...* symbol indicates the one-to-many association that exists between the IObservable and the IObserver interface, indicating that one observable can have multiple observers.
The “Head First” book also suggests that the ConcreteObservable should have methods to getState() and setState(). I’ll focus on getState() for now. The setState() method is highly dependent on what the ConcreteObservable actually does. For example, if the observable is a collection of articles, setState() might be something like addNewArticle(). If it’s a chat room, it might be broadcastMessage(). So, setState() is a placeholder for the core functionality of the observable. The getState() method, on the other hand, is about providing the current state of the observable to the observers when they are notified.
It’s worth noting that having both the core functionality (like managing articles or messages) and the observer management within the ConcreteObservable might seem to violate the Single Responsibility Principle. We could potentially separate the observer management into a different class. However, let’s stick with the book’s example for now to avoid overcomplication.
Now, here’s a crucial and sometimes confusing part of the Observer pattern as presented in “Head First”: the ConcreteObserver often holds a reference back to the ConcreteObservable it is observing:
This is represented by an association arrow pointing from ConcreteObserver to ConcreteObservable. This was odd initially because we usually aim for dependencies on abstractions (interfaces) rather than concrete implementations to increase flexibility.
The reason for this back-reference is as follows: when a ConcreteObserver is instantiated, it is often passed a reference to the specific ConcreteObservable it wants to observe through its constructor. While the ConcreteObserver registers itself with the observable using the add() method, having a direct reference allows the observer to directly query the observable for its state when the update() method is called.
Think about it: the notify method in the IObservable and the update method in the IObserver in this basic form don’t pass any data about what has changed. The update() method simply signals that a change has occurred. If the observer needs to know what has changed or needs to access the updated information, having a direct link to the ConcreteObservable allows it to call the getState() method (or other relevant methods) to retrieve the necessary data. So, when we create a new ConcreteObserver(), we might pass in an instance of the ConcreteObservable that it should observe. This allows the observer, upon receiving an update() notification, to then ask the observable for its current state and react accordingly.
Interaction Flow
- An observer that is interested in the state of an observable registers itself with the observable using the observable’s
addObserver()method. - When the observable’s internal state changes, it calls its
notifyObservers()method. - The
notifyObservers()method iterates through its list of registered observers and calls theupdate()method on each observer. - Each concrete observer’s
update()method then performs the necessary actions based on the notification. Often, the observer will query the observable to retrieve the updated state.
Concrete Example: Weather Monitoring System
Imagine a weather station that receives updates from its sensors. This weather station observes the environment and updates its internal state with measurements like temperature, humidity, and pressure. To keep it simple, let’s focus only on temperature. We have a weather station (or, as the book calls it, WeatherData, but I’ll stick with WeatherStation as it represents the physical entity collecting data).
The goal is that whenever the temperature data in the weather station changes, we want to notify other things, which are displays. Think of a sensor measuring temperature in a room. The sensor itself doesn’t show the temperature. Instead, it sends a message to registered observers whenever the temperature changes. These observers could be a smartphone app or a physical display.
So, we have the WeatherStation (the observable) and multiple displays (the observers), such as a phone display and a window display.
The WeatherStation implements the IObservable interface, meaning it has the add(), remove(), and notify() methods. The phone display and the window display implement the IObserver interface, so they both have an update() method that takes no arguments.
Recall the getState() method we discussed earlier for the concrete observable. In our weather station example, if we are interested in temperature, the WeatherStation would have a getTemperature() method. This allows observers to inspect the current temperature of the weather station.
We also established that an observable has-a relationship with its observers (zero to many).
The book introduces another interface, IDisplay, which the displays implement. This interface defines a display() method. So, when an observer’s internal data is updated via the update() method, it might not immediately show it. A separate call to the display() method would then render the information. This addition, while not strictly part of the Observer pattern itself, highlights the flexibility achieved by using interfaces rather than inheritance. If IObservable and IObserver were abstract classes, the displays would only have one slot for inheritance. By using interfaces, a display can implement IObserver, IDisplay, and any other necessary interfaces.
Here’s the UML diagram representing this scenario:
Consider the flow of the program:
- We instantiate a
WeatherStationobject. This is our observable. - We instantiate various observer objects (displays) that want to be notified of changes in the
WeatherStation. For example, we create aPhoneDisplayand aWindowDisplay. - Each observer needs to know about the
WeatherStationit’s observing. This is often done by passing a reference to theWeatherStationinstance to the observer’s constructor. - The observers then register themselves with the
WeatherStationby calling theadd()method on theWeatherStationinstance, passing themselves as arguments. TheWeatherStationmaintains a collection of these registered observers. - When the
WeatherStation’s state (e.g., temperature) changes, it calls itsnotify()method. - The
notify()method iterates through its collection of registered observers and calls theupdate()method on each one. - Inside their
update()methods, the observers can then access the new state of theWeatherStationby calling methods likegetTemperature(). If they also implementIDisplay, they might then call their owndisplay()method to show the updated information.
Code Implementation
Okay, let’s jump into the code. We’ll start with the interfaces. Using a C-like syntax, we have an interface called IObservable (or ISubject in the book). This interface declares an add() method that takes an IObserver, a remove() method that takes an IObserver (though this might not always be necessary depending on the scenario), and a notify() method that takes no arguments.
interface IObservable {
add(IObserver observer);
remove(IObserver observer);
notify();
}Then, we have another interface for the observers, called IObserver. This interface essentially has a single method: update().
interface IObserver {
update();
}Next, we have the concrete classes: WeatherStation and PhoneDisplay. The WeatherStation implements the IObservable interface, and the PhoneDisplay implements the IObserver interface (and potentially others like IDisplay).
Let’s look at the implementation of the WeatherStation. It needs to implement add(), remove(), and notify(). It also has a getTemperature() method specific to its functionality.
class WeatherStation : IObservable {
private List<IObserver> observers = new List<IObserver>();
private float temperature;
public void setTemperature(float newTemperature) {
this.temperature = newTemperature;
notifyObservers();
}
public float getTemperature() {
return temperature;
}
public void add(IObserver o) {
this.observers.Add(o);
}
public void remove(IObserver o) {
this.observers.Remove(o);
}
public void notify() {
foreach (IObserver o in this.observers) {
o.update();
}
}
}The add() method takes an IObserver and adds it to a list of observers maintained within the WeatherStation. This list keeps track of all the observers that want to be notified of changes. The remove() method does the opposite, removing a given observer from the list.
The notifyObservers method iterates through the list of registered observers and calls the update() method on each one. This is the core of the notification mechanism. When the WeatherStation’s state changes (e.g., the temperature is updated), this method is called to inform all interested observers.
The getTemperature() method is specific to the WeatherStation and allows external entities (including observers) to retrieve the current temperature.
Now, let’s look at the PhoneDisplay implementation:
class PhoneDisplay : IObserver {
private WeatherStation weatherStation;
public PhoneDisplay(WeatherStation station) {
this.weatherStation = station;
}
public void update() {
// Both `weatherStation.getTemperature()` and `this.weatherStation.getTemperature()` are correct.
float currentTemp = weatherStation.getTemperature();
Console.WriteLine($"Phone Display: Temperature is now {currentTemp}");
// Potentially call a display() method if implementing IDisplay
}
}The PhoneDisplay class implements the IObserver interface, so it has an update() method. Importantly, its constructor takes a WeatherStation object as an argument and saves it as an instance variable. This establishes a direct link back to the observable.
When the update() method is called (as a result of the WeatherStation calling notifyObservers), the PhoneDisplay can then access the WeatherStation instance it holds and call its getTemperature() method to retrieve the latest temperature. It then uses this information to update its display (in this example, by printing to the console).
The crucial point here is the constructor of PhoneDisplay taking a concrete WeatherStation. This allows the observer to access the specific data it needs from the observable when it receives a notification.
The flow is as follows:
- A
WeatherStationobject is created. - A
PhoneDisplayobject is created, and theWeatherStationinstance is passed to its constructor. - The
PhoneDisplayregisters itself as an observer of theWeatherStationusing theadd()method. - When the
WeatherStation’s temperature changes, itssetTemperature()method callsnotifyObservers(). notifyObservers()iterates through the registered observers (including thePhoneDisplay) and calls theirupdate()method.- The
PhoneDisplay’supdate()method then uses its stored reference to theWeatherStationto get the current temperature and update its display.
This implementation demonstrates a “push-pull” approach. The observable pushes the notification of a change, but the observers then pull the specific data they need from the observable.
There is a variation where the observable pushes the data directly to the observers as arguments of the update() method. In that case, the IObserver’s update() method would take arguments (e.g., update(float temperature)), and the observable’s notifyObservers() method would pass the relevant data to each observer’s update() method. This would remove the need for the observer to have a direct reference back to the concrete observable.