Michael Ekstrand

Dynamic Dispatch is Control Flow

Published on 28 May 2017 and tagged with programming.

In 2010, I saw a tweet that changed how I think about programming. I do not remember who tweeted it, and I have been unable to locate it again, but its gist was “The Tao of OOP is that dynamic dispatch is control flow”.

This is true, and it creates an entirely different way of thinking about designing objects. I also find it applies somewhat to functional programming.

Let's start with a small, contrived example:

if (shape.isFilled()) {
    fillAndStroke(shape);
} else {
    stroke(shape);
}

This could be replaced by a single call to shape.draw(), where the draw method is implemented differently for filled and unfilled shapes. Rather than detecting what kind of object is to be drawn at draw time, create different kinds of objects for different kinds of shapes (classes in Java-style OO languages).

This does require creating more classes, and in a language like Java, creating quite a few more interfaces — each replaced if may require a new interface. But the resulting code can be shorter and clearer at the call site: we can draw the shape, and do not need to care how that particular shape is drawn. Indeed, this is the promise of OOP.

Another application of this idea is to replace null objects with no-op objects. If we have fill and stroke with no-op objects for unfilled or unstroked shapes, we can write:

shape.fill.draw();
shape.stroke.draw();

The code is straightfoward, and whether or not filling & stroking happen is governed entirely by the choice of fill and stroke objects.

In the Common Lisp Object System, or another language with CLOS-like generic methods, then we can define entire new operations with dynamic dispatch without modifying the underlying object types, provided that the existing type hierarchy makes the distinctions that our control flow requires.

Now, there is a downside. The prevalence of objects means that the full logic for an operation may be scattered across methods in many classes, making it more difficult to identify exactly everything that is happening in a method call. Exact behavior will vary based on the objects in play. It's likely unwise to replace all your if statements with different objects, but used judiciously, this paradigm can leverage the abstraction capabilities of object-oriented design for great effect.

I was doing some JavaScript development when I first encountered this framing, and it fits very well with JavaScript's lightweight notion of objects. It is almost zero-effort to create a new object with a particular method, and there is zero ceremony around interface definitions. I was able to experiment with small objects encapsulating behavior cores that could be plugged in to larger functions — much like first-class functions in FP, except you can easily bundle multiple ones together — and was quite impressed. It's probably pat of why I like JavaScript so much.

I've also brought these ideas into LensKit's OO design. Building on the strategy pattern, many places where we have created interfaces and split something into multiple classes arose from the need to have multiple types of behavior. Breaking these out into separate objects keeps the main methods shorter, and also makes it possible to extend the range of behaviors by creating new strategy implementations.