observer
Propagate changes between loosely coupled objects.
The observer design pattern.
PNG
SVG
The observer Design Pattern
Frequency
Complexity

Today, most applications are implemented in layers to separate the business logic from other concerns like user interface or infrastructure code.

The invocation flow usually goes from top to bottom: The user interface calls the application layer, and the application layer has access to the database. But this never goes the other way round, i.e., lower layers, like the database, should not have direct access to the user interface components.

A layered architecture consisting of presentation layer, application layer, data access and database layer. The dependency flow goes from top to bottom.
A layered architecture. The dependencies always have to go from top to bottom. A lower layer should never directly access a higher layer.

Following this rule reduces the coupling within the system, which, in general, is a good thing as it makes the life of developers easier - we can test, maintain, or update several layers independently without the need to change the whole application.

This layering works as long as our information flow goes only from top to bottom, but what if we need it in the reverse direction, starting at a lower layer?

Think of a weather app that receives the current temperature through an incoming network call and needs to notify its user interface about the change. Or an operating system where several explorer windows point to a single folder. If the folder is renamed in one window, all others should reflect this change immediately. Sure, we could constantly poll our business objects from the presentation layer periodically and check if anything has changed, but this creates a lot of unnecessary traffic, especially if changes happen only sporadically.

So how can we propagate changes up in the hierarchy if, according to our rule, we can't call into higher layers from lower ones?

The Observer design pattern offers a solution for tracking changes between objects by providing two key features:

  • Classes do not have to poll for changes. Instead, the Observer pattern pushes new data to all classes that are interested in these updates.
  • It lets us propagate changes from lower layers into higher ones without breaking any dependency rules.

While recent frameworks and libraries like Angular, React Redux, Flutter Bloc, or even WPF can deal with this problem by design, they often rely on the concepts introduced by the Observer pattern. Hence, understanding how it works also gives us a better understanding of today's technologies, so let's have a deeper look at this pattern.

The Observer Pattern

If we follow our dependency rule, we cannot share implementation types from higher layers with lower ones. However, we can define an interface in our bottom layer and let the upper layers implement it. In that way, our lower layer can make calls to the upper layer without relying on specific implementation details.

This interface already gives our pattern its name, Observer, allowing other objects to observe our state and receive an update whenever it changes.

The observer interface is implemented by a user interface class. Business objects can now call back into the UI layer without breaking the layering.
An observer interface is used to connect UI elements with business objects. The application layer can notify the UI layer through the observer interface without violating the dependency rule.

Remember that our dependency rule applies only to implementation types at compile time. At runtime, we can keep a reference to an object from a higher layer as long as the reference type is defined within our layer (which is the case for our Observer interface).

While this is everything we need for firing updates into higher layers, we can make our approach even more general. Let's call the objects we want to observe Subjects and put all their common behavior related to this observation mechanism in a separate class. They need:

  1. A way to manage (add/remove) all observers we wish to notify.
  2. A notification mechanism that sends our updated state to all observers.
  3. (Depending on the model - see below) Provide the observers with a copy of the actual state.

Adding this Subject as a base class to our previous design completes our observer pattern.

A complete observer pattern where a subject class is used to manage and notify the observers.
A complete observer pattern with subjects and observers. The subject class keeps track of all registered observers and notifies them whenever the state changes.

The subject keeps track of its observers without knowing their concrete types. All it knows is that whenever its state changes, it has to notify all observers by calling their update method to propagate its changes. Whether the object we want to notify is an explorer window, a browser window, or even a printer doesn't matter as long as it has an update method we can call for sending our latest changes.

Sounds easy, right? Well, it actually is...there are only a few details we have to look at regarding the updates between subject and observer - as there are different strategies for that.

Implementation Details

When implementing the update mechanism between subject and observers, there are roughly two different strategies you can follow:

Polling Model: This is the more minimalistic approach. The subject only notifies the observers that an update has happened, and the observers have to figure out what changed on their own by pulling the new state directly from the subject.

While this approach keeps the observer interface small, comparing the current state with the new one to identify the changes can be challenging and time-consuming.

A subject notifying an observer. The observer has to request the latest state from the subject.
The Pull model requires the Observer to determine the changes on its own by comparing the subject state with its the last one it used for rendering.

Push Model: Here, the subject notifies all observers and already provides them with detailed changes. This is more convenient for observers as they don't have to request additional information. However, the subject must somehow collect and transfer all changes to the observers, which may require additional data structures. Also, even observers not interested in these updates will receive all the detailed information, creating an unnecessary load.

A subject pushing the latest changes to its observer.
In the Push variant, the subject pushes the latest changes directly to its observers.

The best choice may often be something in between these two extremes. A subject could fire different types of updates, and observers can decide for which updates they want to register, e.g. by using some kind of topic during registration.

Some things to keep in mind when implementing the observer pattern:

Avoid cascading updates

There are situations where a subject notifies an observer about an update, and while writing the data back to the UI, the observer updates the subject again, resulting in another notification. This can often result in a UI responding slowly, as it permanently has to rerender because of incoming change notifications.

You can reduce this problem by ensuring that updates are only fired and handled if the state really changes. However, the problem is best fixed by redesigning your UI and backend to avoid redundant updates.

Avoid inconsistent states during updates

Sometimes, a change can affect several attributes at once. Yet, the update should only be fired after all changes are processed. Think of someone updating different address fields like street, number, or city name. While you could fire an update every time one of these fields changes, this would result in some observers showing inconsistent data, like a wrong number of a street not present in the given city. The same can happen if your subject is a derived class and its base class manages the update handling. Calling the base's setter method could result in an update call before any logic from the derived class was called.

The best way to avoid these issues is to group related updates into a transaction and ensure that the notification is only fired after the transaction is completed successfully. But this also means that you may need an external class that orchestrates the updates and explicitly calls the subject's update method.

There is no order when updating observers.

Usually, the order in which observers are updated is not deterministic and is up to the subject's decision. You should, therefore, avoid any logic that depends on a specific order in which the observers receive their update call, as this may likely change. Since you don't know which observers will eventually register at the subject, there is also no easy way to enforce any rules here.

However, if you still need a specific order for a limited number of observers, you could wrap them into a root observer and register only that observer at the subject. This root observer would then forward and orchestrate the update notifications to its children.

Observer vs. Publisher/Subscriber

The Observer pattern shares many similarities with the Publisher-Subscriber pattern, at least on the conceptual level. Both aim to propagate changes between several participants, and both achieve this without creating strong coupling. With this in mind, we can interpret the Publisher-Subscriber as a special case of the Observer pattern, where the subject and the observers are separated by an additional mediator (i.e., the message broker). Hence, if you want to implement an update mechanism but need your objects maximally decoupled, using a Publisher-Subscriber approach might be a good option.

A Publisher-Subscriber design pattern where the Publisher is also the subject and the Subscriber is the observer.
The Publisher-Subscriber pattern is a special case of the observer pattern that uses a message broker as an additional mediator. In that way, the coupling between both components can be reduced further.

Another difference is that the Observer pattern operates on objects which are most likely located within the same process. In contrast, the Publisher-Subscriber can also be used on higher levels of abstractions, e.g., to transfer updates between distributed applications. It is not restricted to any programming paradigm and can even be implemented in different languages. Tauri, for instance, uses an IPC-based Publisher-Subscriber implementation to transfer updates between its backend process written in Rust and its web-based frontend process.

Reactive Programming

Reactive Programming is a programming paradigm that takes the Observer Pattern to the limit. The whole approach is centered around data streams to which observers can subscribe to receive updates. Besides that, most reactive programming libraries - like, for instance RxJS, also provide a rich toolset for filtering and manipulating these datastreams, allowing the implementation of very sophisticated data pipelines.

With reactive programming, we can, for instance, interpret any user input as an input stream of interaction data that we translate into domain actions. Applying these actions further to our application state results in a new and derived state, which we then propagate to observers like different UI components. This push behavior is very convenient for handling asynchronous data streams, like user input or incoming network messages, and made this paradigm especially popular in front-end or web development.

References