Using the observer pattern to update dependent variables

The Observer Pattern [] is a design pattern often used in software designs to update dependent variables.

The idea is that when variable Y has a dependency on variable X, X is treated as a subject and Y as an observer which attaches to the subject in order to be notified of changes.

Notification messages provide a mechanism to recalculate caches

However, using the observer pattern is hard and error-prone. There are a number of pitfalls that can easily occur if cache maintenance is not implemented correctly:

Some disadvantages of the observer pattern are described in the paper Deprecating the Observer Pattern by Maier, Rompf and Odersky.

The explicit attaching and detaching of observers must be paired correctly. It is a common problem that observers are not detached when appropriate and this is a common source of memory leaks in OOPLs. This is called the lapsed listener problem [].

PROBLEM 1: Infinite recursion in notifications

This occurs when there is a cycle in the dependency graph. Now technically cycles should be impossible, and that is indeed the case if data dependency is represented at a fine-grained enough level. However in practice it is common for objects to host multiple caches, so that there may be cycles at the coarse resolution of objects.

PROBLEM 2: Dirty reads

Let Y = f(X) and Z = g(X,Y). This typically leads to a redundant edge in the dependency graph:

Let X be changed and X notifies Z before Y. Then Z may access a dirty cached value in Y, because Y hasn't yet been marked as dirty by X.

A dirty read can be a source of surprises (and for example may trigger exceptions in real programs). For example, Z may see a value of Y that should be impossible given the value of X.

PROBLEM 3: Observer attaches or detaches during a notification

The problem is that while a subject is notifying one of the observers, an observer attaches or detaches to/from the subject. This may cause a crash depending on the data structure used by the subject to record the observers.

A solution is to use a "one shot observer". Another is to make a copy of the list of observers before issuing the notifications.

PROBLEM 4: Memory management problems

This affects languages like C++ with no automatic garbage collection. It also affects reference counting systems.

Consider a programmer tool kit that provides various concrete classes like Room, Light and Switch that support assembly. A Light and Switch can be added to a Room, making them visible in the GUI. There is a need to make the Switch control the Light. This involves an object instance of a "wiring" class that implements IButtonListener, responding to the OnPressed() notification message, and calling Set() on the Light.

The problem is: What deletes the object used for wiring purposes?

It seems nececessary for the subject to hold strong references on its observers! But if that is the case then the observer pattern easily leads to cyclic references

PROBLEM 5: Nested contract attachments

When OO is used to build complex run time assemblies, it can be important for observers to connect to their subjects lazily (i.e. only when they are themselves being observed). This can lead to subtle reentrancy problems with attachments.

Consider a namespace object with two child objects named "x" and "y". The object named "x" contains a formula that refers to sibling y by name. When the object named "x" is first observed, it in turn needs to attach to the parent namespace object. The namespace object in turn has to attach to each of its children (because the logical namespace is a function of the names of the children), and in particular attaches to the object named "x".

So when a subject has an observer attach making it lively (ie under contract to in turn attach to its independents), it is possible that an upstream independent tries to attach back to a dependent.

Nested attachments don't cause infinite loops. However, they make it difficult to tell when an object logically has no more observers. The problem is that some of the observers are associated with a nested contract, and therefore it is possible that logically there are no observers, yet physically there are. This is reminiscent of the problem of detecting cyclic garbage using reference counting.

Lazy evaluation

The concept of lazy evaluation is very helpful in writing a correct and efficient program. The basic idea is that caches are only calculated on demand. This avoids dirty reads because calculation is automatically done in the right order (i.e. as prescribed by the dependency graph).

When changes occur, dependent caches are only marked as dirty, rather than being eagerly recalculated. This can provide significant efficiency gains, because the inputs may be changed many times before caches are recalculated. It allows an interface on an object to have lots of fine grained mutative methods without a concern that dependent caches will be recalculated many times. In fact objects in the system can accumulate many changes for an extended period before any recalculation begins.