Moving Beyond Enumerations
 
Published: 30 Aug 2011
Abstract
Enumerations provide far more readable code than magic strings or magic numbers. However, often developers try to stretch enums beyond their intended use. This article describes how to appropriately extend enums, and how to move beyond enums when the needs of the system require it.
by Steven Smith
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 30794/ 42

Introduction

Enumerations in C# and .NET are value types with a set of related named constants.  Using enumerations often results in more readable code, and can help to ensure developers choose appropriate values by limiting possible options to those defined in the enum.  However, when the things represented by the enum start to require additional behavior, it is often best to move beyond the enum and start using classes instead.

Simple Enum Use

For this example, let's say we have a list of Roles that our application uses, and these roles include Authors, Editors, and Sales Representatives.  To model this with an enum, we can use something like this:

Now anywhere in our code that we need to use a Role, instead of using a string or an integer ID for the role, we can simply specify Role.Author or Role.Editor, etc.  The resulting code will be much more readable.

However, enums are not without their limitations.  A very common use case for enumerations is the creation of user interface elements that allow the end user to choose from a list of options.  Since enums' element names must follow C# naming conventions, they cannot, for instance, have spaces in them.  Thus if you were to have the following bit of code for displaying the above enum listing, and you expected to have "Sales Representative" listed instead of the concatenated version use by the enum, your code (and the test shown) would fail.

Enumerations, while type-safe, are not as safe as many believe when it comes to using them as parameters.  For instance, if enumerations only supported the named values specified, the following method would correctly model the behavior that Editors and Sales Representatives should be able to do something, but Authors should not. 

However, it allows for the possibility of an invalid input to be granted access.  Consider this test and result:

There are no built-in range checks performed on enumerations, for performance reasons (see more here).  If you're going to use them, it's your responsibility to verify the values you're receiving are valid.  The simplest way to do so is to use the Enum.IsDefined() method, like so:

Extending Enums With Attributes

You can add a *little* bit of behavior to enums through the use of attributes and reflection.  One common approach to the problem of displaying enums in a user-friendly manner is to add an attribute that can be used for display purposes.  You can create your own attribute, but it's probably better to use one that already exists and is well-suited to this purpose, such as the System.ComponentModel.Description attribute.  You can add it to any or all of your enumeration values in order to specify the "friendly" name that you want to display in your application.  The Role enum would then look like this (note the addition of a few more roles that I'll get to in a moment):

Now we can create a new method to help use fetch the description of a given enum.  This one is borrowed from this post.

If we adjust our method for displaying the enumerations, we'll now get "Sales Representative" output as we wanted.

You can of course continue down this road for as long as you wish, but personally I draw the line with one attribute.  If your enumeration's items each require 2 or more attributes, I would argue that you have stepped beyond capabilities of the enum type, and that you should upgrade to a class.  The additional behavior required of enums can come in many forms, but one I've seen frequently relates to visibility of options to end users.

As you saw above, there are now two Secret roles that should not be displayed with the other roles.  Not only that, but additional roles are also planned, some of which will be visible and some not (such as the new Manager role, which should be visible).  Our initial test of the new requirement that Secret roles not be displayed fails:

We could fix it in the DisplayFriendlyNames() method by adding individual checks for each role, but this gets ugly quickly.  Better would be if there were some kind of IsVisible property on the enum.  We could again turn to attributes, and perhaps create a [Visible(false)] or similar for the purpose, but as I noted, I draw the line with one attribute.

Replacing Enums with Classes

Eventually, you need to upgrade from enums to classes in order to encapsulate both their state and behavior, if their behavior expands beyond mere named constants.  The best way to achieve this without breaking the enum use pattern is through the use of static classes.  When done properly, this refactoring can be done with little or no changes required in existing code that previously used the enum.  Consider the following Role class that can be used everywhere our existing enum was used in the previous examples:

In Visual Studio, when you type in "Role." you will get the following Intellisense:

Thus, the behavior mirrors that of an enum, constraining the options available.  The friendly display value is easily displayed without the need for attributes or reflection, using this code:

The issue with passing in values that are outside the range of the enumeration disappears as well.  The only way one could do such a thing would be through the use of reflection to remove the sealed marker on Role and create a subtype.  Otherwise, whether an invalid value is cast or an uninitialized value is passed into a method expecting a Role, an exception will result (you may wish to do your own null check).  These two tests both pass now:

The new implementation of RoleCanDoSomething() is simply:

And of course, now that Role is an actual class, some of these utility methods that were in EnumHelper or RoleHelper classes can be rolled into the Role class itself, if appropriate.

Summary

In many cases, enumerations are a good fit for the needs of a system.  However, if and when the concept being modeled by the enum gains any kind of behavior beyond its numeric constant and developer-friendly label, it may be time to consider moving from an enum to a class.  In this article, you've seen how you can easily make this transition with minimal changes in your code that already makes use of an enum, provided that you keep the same naming convention for your class and its named static instances.

Learn more about software development best practices in my Principles of Object Oriented Design course available from PluralSight.

You can follow me on my blog at SteveSmithBlog.com and on Twitter at twitter.com/ardalis.  You can download the (very small) sample code for this article.



User Comments

Title: Good Catch   
Name: Steve Smith
Date: 2011-08-30 7:56:15 PM
Comment:
@Ian,
Good catch. Yes, those fields should be readonly, of course.

Thanks!
Steve
Title: public static roles should be readonly   
Name: Ian Mercer
Date: 2011-08-30 5:01:05 PM
Comment:
Totally agree that enums should be replaced with classes as soon as they stop being enums. One improvement to your class would be to make all the values readonly, e.g.

public static readonly Author ...






Community Advice: ASP | SQL | XML | Regular Expressions | Windows


©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-03-19 1:49:37 AM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search