August 28, 2025

OOP & the expression problem

When modeling the behavior of data, we usually start by treating a concept either as data inside a system of functions, or as data coupled with behavior in a polymorphic hierarchy. The right choice depends on where the complexity actually lives.

Take a game like Baldur’s Gate, or more abstractly, Dungeons and Dragons. In these games we need to model weapons. Weapons are numerous, but their behavior is uniform. A longsword and a dagger differ by hitbox, reach, weight, and damage profile. Those properties fit cleanly into a combat system that computes results from data. Adding a new weapon is then a data entry, not a new subclass. It doesn’t make sense for each weapon to implement its own calculateDamage method if the behavior is uniform. The system owns the behavior; the weapon parameterizes system behavior through its data.

But now take PlayerClasses like Barbarian, Wizard, or Bard. PlayerClasses are few, but each carries distinctive rules. Barbarian rage, Wizard spell slots, and other class features are complex, interdependent, and not easily reduced to a single general system. As rules expand, you mostly add new operations over the same small set of classes: combat, progression, resource recovery, UI summaries, and so on.

But how do we model polymorphic behaviors? There are two general strategies: enumerative and polymorphic.

This is the expression problem. Enumerative designs are open to new functions but closed to new variants: adding a new operation is simple, but every new variant requires edits across all existing switches. Polymorphic designs are open to new variants but closed to new functions: adding a new subclass is trivial, but every new operation requires updates to every class in the hierarchy. In short, the enumerative style centralizes variation in one place at the cost of variant sprawl, while the polymorphic style distributes variation across many places at the cost of behavioral rigidity.

In practice, our PlayerClasses collapse toward the enumerative side. The work accrues in new behaviors, not new variants. Teams rarely invent new fundamental PlayerClasses. They extend existing ones with new rules, new mechanics, new operations. The pressure falls on behaviors, not on variants.

And that is the paradox. OOP holds up polymorphism as a defining strength, yet when polymorphism does make sense (lots of variants with little behavior) it usually looks like the Weapons example: dozens of items, all handled cleanly as data in a system. But when we deal with PlayerClasses (few variants with complex behavior) the pressure shifts toward enums and switches, because what actually grows are the operations over a stable set of types. In other words, the very situations where OOP polymorphism seems most natural are the same ones where polymorphism fails to deliver.



Copyright Nathanael Bennett 2025 - All rights reserved