Building Rich Enums

November 18, 2011

Headspring has recently updated its open-source Enumeration class. We recommend this as an alternative to using plain old .NET enums in your domain models. Most of the time, you use a .NET enum in order to define a finite set of named values. Enumeration serves that purpose too.

What's so bad about enums? They provide a very thin abstraction over integers, and that abstraction is both leaky and insufficient when building a rich domain model.

The .NET enum abstraction leaks

When you define an enum, you are really defining several integer constants.

Whether you declare the integers or not, each enum value is really an integer. Unfortunately, this means that unintended values are allowed:

This is .NET's way of reminding you of its own implementation details. -1 has nothing to do with the roles in my system. Enums fail to limit values to those I consider meaningful, so why am I using them as domain objects?

The .NET enum abstraction is insufficient

You can think of an enum definition as shorthand for a class. That class effectively inherits from int, exposing public static getters that return each defined constant, including convenience methods for converting to and from strings.

What if the shorthand just isn't enough to describe the class we really wanted to write? What if your type deserves some additional methods, or you need to associate additional metadata with each value? You could resort to attaching metadata via attributes, and provide some extension methods to reflect on them, but that's a long way to go to avoid admitting that we really just wanted to constrain our domain objects to a fixed set of values. It's nice to use ints as the 'unique identifier' for each value in the set, but we don't need that int to be the whole story.

Using Headspring's Enumeration Class

Using Enumeration, we could define a more useful set like so:

Here we see a fixed set of objects, with the convenient int and string representations we're used to having with enums. Role.FromInt32(int) allows you to load the value associated with a given int, such as when reading from a database, but only the fixed set of integers will be allowed. Each value can have arbitrary properties, and the enumeration can contain additional methods.

Enumeration's Generic Type Constraint

Enumeration contains an interesting generic constraint. Several of the methods on the base class need to deal with the type of the individual values, so Role is declared to implement Enumeration<Role>. You can think of the Role class as "passing" its own definition up to the base class so Enumeration can make use of it. Similarly, Enumeration needs to be declared in such a way that it can know what's being "passed up" to it:

In other words, Enumeration will be working with any type that is itself declared as an Enumeration. This constraint basically mimics the "shape" of the first line of the Role type. Likewise, it demands that any child class be declared with a line of that shape.

At first, it may seem like class Role : Enumeration<Role> should result in a compiler error. It looks like we're trying to use a type before we're even finished defining it. The truth is, the compiler need only compare the "shape" of the two classes' declarations in order to convince itself that the required pattern is being respected, and then it can go on with the business of verifying the content of each class.

Conclusion

.NET enums are still useful, such as when you need to pack several bit flags into a single integer, but when using them to define finite sets of objects in your domain model, their brevity and ubiquity may tempt you away from writing rich domain objects. When you reach for the handy dandy enum keyword, consider whether Enumeration is a better fit.