Building Rich Enums
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
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.
.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.