The Distributed Enum Anti-Pattern

Author: Adam Wilson, Sr. Engineering Manager at ThousandEyes

All too often I come across codebases that make liberal use of conditionals that operate on the members of an enum, usually spread widely throughout the code. Unfortunately, this is one of the most widely used anti-patterns that’s still idiomatic in most languages today.

To give a more concrete example, imagine we’re building a simple calculator application and want to model the operations with an “enum”. I’m sure you’re all familiar with code like this:

Looks fairly regular and innocuous, right? While this will work perfectly fine in the present, in practice we’ve just created a redundant source of truth over the valid members of the enum.

What does it mean to have multiple sources of truth?

Effectively, this means that you have multiple representations of what is, semantically, the same piece of code or state. In practice this means that anytime you update one part of your code, you’ll also have to update the duplicate representation.

For example, imagine we want to enhance our simple calculator by adding support for division. We now have to update the code in two places to support this new member.

While it’s true that our Operation enum is only defined in one place, our switch statement is making an assumption about the valid members of Operation, effectively creating a second source of truth. It’s as if we’re maintaining the list of Operation members in multiple places.

Of course, updating both the enum and switch statement seems easy enough, but that’s because we’re working with a small, contrived example.

There are three main problems with structuring your code this way:

  1. It’s easy to forget to update all references to the enum. What if the developer adding the division operation is not the same developer who wrote the switch statement? Additionally, what if we have many, perhaps even tens of switch statements that work off of this enum? You’ll quickly run into a problem where these switch statements aren’t updated correctly in response to changes to the base enum, an issue that will not get caught at compile time in most mainstream languages. (Note: Kotlin’s when expression is one example where the language provides type safety at compile time). At scale, you’re unlikely to correctly identify and update all of the various bits of conditional logic when adding new members to your enum.
  2. It spreads metadata around the code. But beyond keeping the switch statements and enum object in sync, placing logic in various switch statements in your code also spreads the metadata between those enums throughout the code. This can make it difficult to really see the differences between the various enum members without scouring the code for all of these bits of logic.
  3. Your code no longer reads in a generic fashion. Rather than separating code and “configuration” your configuration lives in the code itself. This results in a codebase that reads more imperatively and “configuration” values that are more difficult to tweak.

Is there a better way?

Yes! We can model the enums in a “polymorphic” way, where the differences between members are captured as properties on the members themselves.

But why is this better? For a few reasons:

  1. Single source of truth. The declaration of the enum is the only place where metadata about the specific enum types is housed. Adding a new member is as simple as copying the structure of existing members and filling in the blanks.
  2. Less branching in consuming code. As can be seen in the example above, using a polymorphic approach leads to more natural reading code. All of the implementation details are hidden by the name of the function being called instead of being inlined in a big switch statement.
  3. Compile time safety. In languages like Java this also gives us a compile time guarantee that all enum members have the same set of properties defined. This can be accomplished either via declaring a private constructor or interface for the enum.

Are there problems with this approach?

You may have questions around how modeling your enums polymorphically works at scale. Surely as usage grows the enum declaration will also grow and become unwieldy? Or perhaps you want to model a property that’s very context specific and doesn’t belong in a more general representation of your enum. We can address both of these concerns by borrowing from the principles of domain-driven design and creating separate representations of our enum for each domain in our code. Check back for a future post to answer these questions and more.

--

--