Sometimes objects can act as intermediaries in a calculation. This means they receive data from one object, perform some operation on the data and provide the results to a third object. Such objects act as both observer and observable objects. In this setting an object A does some calculation and notifies our object B of the changes. The object B in turn updates itself with the new data from object A and notifies its observer, let’s call it object C. A straightforward implementation of our object might carry out the calculations every time the underlying data changes, i.e. every time the update() function is called. Such an implementation would, however, be ineffective. What if the object was not observed by any observer at the time that the calculation was carried out? The the update function would carry out the calculations but the results would never be used. In this case it would be better if the calculations were carried out only when requested by some other entity.
The solution to this problem is the lazy evaluation design pattern. In this pattern the calculation of the dependent results is carried out only on demand. The object implementing the lazy evaluation pattern will still notify its observers of any changes of the underlying data. But only if the observer of the lazy evaluator actually requests an update will the dependent calculations be carried out.
In QuantLib the LazyObject implements the lazy evaluation design pattern. LazyObject inherits from both Observer and Observable. It is therefore intended to be chained in a sequence of observers and observables.
We start with the method that performs the actual calculation.
protected: virtual void performCalculations() const = 0;
This method is defined abstract and must be implemented by any concrete class. It is also declared protected. The method performCalaculations() should never be called directly. It is called by the other methods on demand.
Being an Observer, LazyObject implements the update() method. However, update() will not call the performCalaculations() method directly. Instead, it sets a flag indicating that it should be updated before use and then notifies its own observers.
virtual void calculate() const;
After update() has notified the object’s observers, these might want to access the lazy object’s data. But this data is now out of sync and needs to be calculated again. To get the data back in sync, the observer of the lazy object first calls the calculate() method. This method checks if the data is out of sync and, only if it needs updating, calls the performCalaculations() method. Note how both calculate() and performCalaculations() are declared const, even though the performCalaculations() method will modify internal data. This is because, from the point of view of the observer of the lazy object, the data had already changed when it was notified. The calculate() function will just cause those changes to manifest themselves. On the other hand, this means that any data that does change as a consequence of the performCalaculations() function has to be declared as mutable.
LazyObject also has the option of being frozen and unfrozen.
void freeze(); void unfreeze();
These two methods toggle an internal flag that influences the behavior of the object. When a LazyObject is frozen it will not notify its observers of any changes when update() is called, nor will the calculate() method update the internal data. The update() function will remember that the data needs to be calculated again but will not pass the information on to the observers of the lazy object. When unfreeze() is called, the status of the object will be changed and all the observers will be notified. This allows the observers to update their data once the lazy object has been unfrozen. Freezing objects can increase performance in certain situations, but it is essential when dealing with observers and observables linked together in a circular fashion to break the infinite recursion that would otherwise occur.
Finally, the recalculate() method will cause the internal data to be re-calculated, independent of whether the object was frozen or not. Under normal circumstances this method should not be used. Instead, a dependent object should always call the calculate() method. recalculate() is only needed when bootstrapping the data in certain cases.