Dynamic Dispatch is Control Flow
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:
.fill.draw();
shape.stroke.draw(); shape
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.