QuEP 7: using the Visitor pattern to avoid interface bloat

Luigi Ballabio

Abstract

A technique is here proposed which allows one to specialize calculations on a per-derived-class basis without increasing the number of virtual member functions in the base class interface.

Such technique consists in the usage of the Visitor pattern which is shortly outlined together with possible applications to the existing class hierarchies.

Introduction

It has been argued [1] that functionalities that can be implemented by means of the public interface of a class should be made available as a non-friend, non-member function rather than another method of the class. This actually improves encapsulation since this enlarged class interface (meant as the set of the class methods and the additional non-member functions) is thus made less dependent on the class internals.

However, there are situations where this simple approach would not be convenient, either because knowledge of the exact type of the instance is actually needed, or because it would prevent the developer from applying useful optimizations depending on the instance type.

An example of the first case is a number of report functionalities which can be desired for the CashFlow hierarchy. Derived classes include simple cash flows whose amount is fixed or accrued coupons which in turn can be paid based on a fixed or a floating rate. For each of the latter types, whether the amount of the cashflow was already fixed or the extent of its basis-point sensitivity can be determined based entirely on their public interface, as shown in the following pseudo-code:

procedure WasFixed(CashFlow)
    case SimpleCashFlow:
        return true
    case FixedRateCoupon:
        return true
    case FloatingRateCoupon:
        if today > fixingDate()
            return true
        else
            return false

procedure BPS(CashFlow,TermStructure)
    case SimpleCashFlow:
        no such thing
    case Coupon:
        return nominal()*accrualPeriod()*discount(date())

The above could be naturally implemented as virtual member functions. However, a number of such functionalities could be found which would end up bloating the base CashFlow interface, and the more so should the hierarchy be extended with more complex coupons. Moreover, functionalities which make sense only for part of the hierarchy should not be exposed by the CashFlow interface.

An example of the second case is a number of calculations which could be performed on term structures. For instance, one might want to know the average of the instantaneous forward rate over a given period in order to feed it to an option pricer. According to the definition of zero yield rate, this could be implemented as (pseudo-code again):

procedure averageForward(TermStructure,Time t0,Time t1)
    return (t1*zeroYield(t1)-t0*zeroYield(t0))/(t1-t0)

This would be the optimal choice for a term structure which can calculate zero yields directly. However, if the latter were calculated by integrating the instantaneous forward rates, the above would be highly inefficient: the integral from 0 to t1 would be calculated twice, only to be canceled out by the subtraction. In this case, one would be better off calculating the integral from t0 to t1 himself. But in turn, this would be highly inefficient if the term structure were of the first kind---one which can calculate zero yields efficiently. Optimizations should clearly be done on a per-class basis. Again, this could be achieved by means of virtual functions. However, it would lead to interface bloat as more and more such functionalities are requested.

The solution would be a dispatch mechanism, other than virtual functions, which can call the appropriate external function for the given instance type without resorting to dynamic casts.

The Visitor pattern

The Visitor pattern [2,3] is a double-dispatch mechanism which allows one to specialize a function on a per-base class without modifying the original base class interface. The pattern structure is shown in the following diagram for the CashFlow class. However, it applies unchanged to any other hierarchy.

UML diagram

The only addition to the base class interface consist of an abstract acceptVisitor method which takes a CashFlowVisitor. The latter is an abstract class whose interface consist of a number of visitXXX methods, each one corresponding to a derived cash flow class and each one taking an instance of the corresponding derived type. Finally, each class derived from CashFlow implements acceptVisitor by passing itself to the right visit method of the passed visitor.

It is easy to see how this implementation allows one to dispatch a generic method call to the desired method specialization depending on the actual cash flow type. Its usage is the following:

Handle<CashFlow> cashFlow;      // (1)
WasFixedVisitor v;
cashFlow->acceptVisitor(v);     // (2)
bool fixed = v.wasFixed();      // (3)

(1) Let's say for the sake of explanation that this handle points to a FloatingRateCoupon. However, the latter is stored as an upcasted pointer to CashFlow.

(2) This method call triggers the virtual method dispatch mechanism, so that FloatingRateCoupon::acceptVisitor is executed. In turn, the latter calls WasFixed::visitFloatingRateCoupon and passes it a reference to the coupon as a floating-rate coupon. The called visitor method can then use the full FloatingRateCoupon interface to calculate the result rather than the base class interface only.

(3) The result is retrieved from the visitor. Such result could not be returned directly from CashFlow::acceptVisitor since the result type depends on the particular Visitor subclass, while the return type of acceptVisitor must be fully determined in the CashFlow interface. The solution is to have both acceptVisitor and the visitXXX methods return void and to have the latter methods store the result in a data member of the visitor, which can be of the desired type.

Finally, it can be noted that a variant of the Visitor pattern exists which is called Acyclic Visitor pattern [4]. It is based on the same principle, while addressing issues such as the mutual compilation dependency of CashFlow, CashFlowVisitor and all the CashFlow subclasses. The acyclic visitor is better suited for use in systems where the interested hierarchy is supposed to grow in time, which is the case in QuantLib. It also allows to express more naturally the case in which it makes only sense to apply a given visitor to a limited subset of the hierarchy.

Conclusion

The Visitor pattern was proposed for use in the QuantLib library. Such pattern allows one to effectively add virtual behavior to a function without making it a member of the interested class, thus avoiding interface bloat and improving encapsulation.

The Acyclic Visitor pattern is also proposed. The latter adds a bit of complexity to the Visitor implementation but effectively addresses a few shortcomings of the original pattern. The choice between the two patterns will hopefully made more clear by future discussion of this proposal.

References

[1] Scott Meyers, How Non-Member Functions Improve Encapsulation

[2] Gamma, Helm, Johnson, and Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software

[3] Jim Hyslop and Herb Sutter, Conversations: By Any Other Name

[4] Robert C. Martin, Acyclic Visitor

Feedback

Feedback on the above proposal should be posted to the QuantLib-dev mailing list.