Enum Pattern

by Gong Liu July 29, 2009 20:49

This article is also published on The Code Project. 

Introduction 

A programmer often has to deal with a fixed number of cases in his program. For example, if your program is about emergency management, you may consider following cases:

  1. Fire,
  2. Injured,
  3. Burglary,
  4. Blue Screen of Death,
  5. Run out of Beer

and your program may look like:

    int emergency = 3;
    switch (emergency)
    {
        case 1:
        case 2:
        case 3:
            Console.WriteLine("Call 911");
            break;
        case 4:
            Console.WriteLine("Sue Microsoft");
            break;
        case 5:
            Console.WriteLine("Brew your own");
            break;
    }

Here, each emergency case is assigned a number. The problem with it is that the program becomes hard to read if the reader doesn't know what each number stands for. This is where enum comes to the rescue:

    enum Emergency
    {
        Fire = 1,
        Injured,
        Burglary,
        Blue_Screen_of_Death,
        Run_out_of_Beer
    }

    Emergency emergency = Emergency.Injured;
    switch (emergency)
    {
        case Emergency.Fire:
        case Emergency.Injured:
        case Emergency.Burglary:
            Console.WriteLine("Call 911");
            break;
        case Emergency.Blue_Screen_of_Death:
            Console.WriteLine("Sue Microsoft");
            break;
        case Emergency.Run_out_of_Beer:
            Console.WriteLine("Brew your own");
            break;
    }

So enum is just a way of naming numbered cases. It makes your program more readable. In .Net it's very easy to get the names of numbered cases as strings:

    Console.WriteLine(emergency.ToString());
    //output: Injured
    Console.WriteLine(string.Join(",", Enum.GetNames(typeof(Emergency))));
    //output: Fire,Injured,Burglary,Blue_Screen_of_Death,Run_out_of_Beer 

However, in principle, you should reject the temptation of using enum names in the presentation layer, as the names are meant for the convenience of programmers, not for the users of your program. "Blue_Screen_of_Death" or even "BSoD" is perfectly fine in your program, but when presented to the users, it's just not that user friendly.

What if you do want to associate a user-friendly string to each enumerated case? Muaddubby in his article A Perfect C# String Enumerator was trying to do just that. He claims his string enumerator has all the right behaviors of a real enum. But he fails to address the fact that his enumerator is not a value type constant so it can't be used in a switch statement, nor can it do flag bit operations. So it is not that perfect after all. A better approach, as described in String Enumerations in C# by Matt Simner and Enum With String Values In C# by Stefan Sedich, is to leverage existing enum. They associate a string to an enumerated case by way of a custom attribute, and use reflection to gain access to the string via an extension method.

Enum Pattern 

From previous efforts of devising a string enumerator, it's evident that we need a general design pattern of associating a string, or any object for that matter, to an enumerated case. In fact, Matt Simner and Stefan Sedich in their articles have demonstrated a special case of the pattern. We just need to make it a little bit more generic so it can handle any object, not just a string. Here is what is involved:

Step 1, we need to define a base class that represents the attribute of an enumerated case.

    [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
    public class EnumAttr : Attribute
    {
        public EnumAttr()
        {
        }    
    } 

Since the attribute can be any object, we don't want it to have any particular behaviors by implementing methods or properties, except that it must apply to an enum field no more than once.

Step 2, we define the following extension method to gain access to the attribute object associated with an enum field:

    public static class EnumExtension
    {
        public static EnumAttr GetAttr(this Enum value)
        {
            Type type = value.GetType();
            FieldInfo fieldInfo = type.GetField(value.ToString());
            var atts = (EnumAttr[])fieldInfo.GetCustomAttributes(typeof(EnumAttr), false);
            return atts.Length > 0 ? atts[0] : null;
        }
    }

Step 3, define your own attribute class derived from EnumAttr.

Step 4, define your enum and add an instance of your attribute class in Step 3 to each enum field. Your enum can be regular or flagged.

Step 5, use your enum as usual. Whenever you need to access the attribute data of an enum field, just call the GetAttr() extension method.

Examples

The other day I was looking for something on Craig's List and wondered around to the personal classified ad section. To my surprise (well, not that surprised) I found that there were a lot more categories than just man-seeking-woman and woman-seeking-man. Here is a partial list:

    public class SexInterestAttr : EnumAttr
    {
        public string Desc { get; set; }
    }

    public enum SexInterest
    {
        [SexInterestAttr(Desc = "woman seeking man")]
        w4m,
        [SexInterestAttr(Desc = "man seeking man")]
        m4m,
        [SexInterestAttr(Desc = "man seeking woman")]
        m4w,
        [SexInterestAttr(Desc = "woman seeking woman")]
        w4w,
        [SexInterestAttr(Desc = "couple seeking couple")]
        mw4mw,
        [SexInterestAttr(Desc = "couple seeking woman")]
        mw4w,
        [SexInterestAttr(Desc = "couple seeking man")]
        mw4m,
        [SexInterestAttr(Desc = "woman seeking couple")]
        w4mw,
        [SexInterestAttr(Desc = "man seeking couple")]
        m4mw,
        [SexInterestAttr(Desc = "woman seeking lesbian couple")]
        w4ww,
        [SexInterestAttr(Desc = "man seeking gay couple")]
        m4mm,
        [SexInterestAttr(Desc = "gay couple seeking man")]
        mm4m,
        [SexInterestAttr(Desc = "lesbian couple seeking woman")]
        ww4w,
        [SexInterestAttr(Desc = "lesbian couple seeking man")]
        ww4m,
        [SexInterestAttr(Desc = "gay couple seeking woman")]
        mm4w,
        [SexInterestAttr(Desc = "man seeking lesbian couple")]
        m4ww,
        [SexInterestAttr(Desc = "woman seeking gay couple")]
        w4mm,
    }

Here, our attribute class, SexInterestAttr, contains only one property Desc, a user-friendly string to describe the sex interest. Notice that in our SexInterest enum we can use the object initializer syntax to create instance of SexInterestAttr. The following code snip shows how to use the enum and its attribute:

    var interests = (SexInterest[])Enum.GetValues(typeof(SexInterest));
    Console.WriteLine(string.Join(System.Environment.NewLine,
        interests.Select(i => string.Format("{0} - {1}", i.ToString(),
            ((SexInterestAttr)i.GetAttr()).Desc)).ToArray()));
Output:
    w4m - woman seeking man
    m4m - man seeking man
    m4w - man seeking woman
    w4w - woman seeking woman
    mw4mw - couple seeking couple
    mw4w - couple seeking woman
    mw4m - couple seeking man
    w4mw - woman seeking couple
    m4mw - man seeking couple
    w4ww - woman seeking lesbian couple
    m4mm - man seeking gay couple
    mm4m - gay couple seeking man
    ww4w - lesbian couple seeking woman
    ww4m - lesbian couple seeking man
    mm4w - gay couple seeking woman
    m4ww - man seeking lesbian couple
    w4mm - woman seeking gay couple

Adding attribute to enum doesn't alter its value type. We can still use it as usual such as in a switch statement:

    var interest = SexInterest.w4m;
    switch (interest)
    {
        case SexInterest.m4m:
            Console.WriteLine("you are a gay");
            break;
        case SexInterest.w4w:
            Console.WriteLine("you are a lesbian");
            break;
        case SexInterest.m4w:
        case SexInterest.w4m:
            Console.WriteLine("you are a hetero");
            break;
        default:
            Console.WriteLine("whatever. it's a free country");
            break;
    }
    //ourput: you are a hetero

As a side point, one of the advantages of using extension method is that it seemingly becomes part of our enum type and actually shows up in the IntelliSense:   

 

The above example is quite simple. We only associate a string to each enumerated case. Now let's take a look at a more complex example. Assume we have an enumeration of US presidents and each president can be described with the following PresidentAttr class:

    public class PresidentAttr : EnumAttr
    {
        public PresidentAttr(string name, string party, int yearTookOffice,
            int yearBorn, int yearDied)
        {
            Name = name;
            Party = party;
            YearTookOffice = yearTookOffice;
            YearBorn = yearBorn;
            YearDied = yearDied;
        }
        public string Name { get; set; }
        public string Party { get; set; }
        public int YearTookOffice { get; set; }
        public int YearBorn { get; set; }
        public int YearDied { get; set; }
        public bool IsAlive { get { return (YearDied <= 0); } }
        public int AgeTookOffice { get { return YearTookOffice - YearBorn; } }
    } 

Our President enum is defined as:

    public enum President
    {
        [PresidentAttr("George Washington", "No Party", 1789, 1732, 1799)]
        GeorgeWashington,
        [PresidentAttr("John Adams", "Federalist", 1797, 1735, 1826)]
        JohnAdams,
        [PresidentAttr("Thomas Jefferson", "Democratic-Republican", 1801, 1743, 1826)]
        ThomasJefferson,
        ...  
      
        [PresidentAttr("Bill Clinton", "Democratic", 1993, 1946, 0)]
        BillClinton,
        [PresidentAttr("George W. Bush", "Republican", 2001, 1946, 0)]
        GeorgeWBush,
        [PresidentAttr("Barack Obama", "Democratic", 2009, 1961, 0)]
        BarackObama
    } 

Now we can have some funs with our President enum:

    var presidents = (President[])Enum.GetValues(typeof(President));
    Console.WriteLine(string.Join(System.Environment.NewLine,
        presidents.Select(p => (PresidentAttr)p.GetAttr()).
        Select(a => string.Format("{0} ({1}-{2}), took office in {3}",
            a.Name, a.YearBorn, (a.YearDied<=0)? "" : a.YearDied.ToString(),
            a.YearTookOffice)).ToArray()));

Output:
    George Washington (1732-1799), took office in 1789
    John Adams (1735-1826), took office in 1797
    Thomas Jefferson (1743-1826), took office in 1801
    ...
    Bill Clinton (1946-), took office in 1993
    George W. Bush (1946-), took office in 2001
    Barack Obama (1961-), took office in 2009

    Console.WriteLine("There are {0} Democratic presidents",
        presidents.Count(p => ((PresidentAttr)p.GetAttr()).Party == "Democratic"));
    Console.WriteLine("They are:");
    Console.WriteLine(string.Join(System.Environment.NewLine,
        Array.FindAll(presidents, p => ((PresidentAttr)p.GetAttr()).Party == "Democratic").
        Select(p => ((PresidentAttr)p.GetAttr()).Name).ToArray()));

Output:
    There are 16 Democratic presidents
    They are:
    Andrew Jackson
    Martin Van Buren
    James K. Polk
    Franklin Pierce
    James Buchanan
    Andrew Johnson
    Grover Cleveland
    Grover Cleveland (2nd term)
    Woodrow Wilson
    Franklin D. Roosevelt
    Harry S. Truman
    John F. Kennedy
    Lyndon B. Johnson
    Jimmy Carter
    Bill Clinton
    Barack Obama

    Console.WriteLine("Presidents still alive: {0}",
        string.Join(",", Array.FindAll(presidents, p =>
            ((PresidentAttr)p.GetAttr()).IsAlive).Select(p =>
                ((PresidentAttr)p.GetAttr()).Name).ToArray()));

Output:
    Presidents still alive:
        Jimmy Carter,George H. W. Bush,Bill Clinton,George W. Bush,Barack Obama

    int maxAge = presidents.ToList().Max(p =>((PresidentAttr)p.GetAttr()).AgeTookOffice);
    var maxPAttr = (PresidentAttr)Array.Find(presidents, p =>
        ((PresidentAttr)p.GetAttr()).AgeTookOffice == maxAge).GetAttr();
    Console.WriteLine("The oldest president was {0}, a {1}, who took office at age {2}.",
        maxPAttr.Name, maxPAttr.Party, maxPAttr.AgeTookOffice);

Output:
    The oldest president was Ronald Reagan, a Republican, who took office at age 70.

In our last example we will demonstrate President enum with [Flags] attribute, or PresidentFlagged. Here is its definition:

    [Flags]
    public enum PresidentFlagged : long
    {
        [PresidentAttr("George Washington", "No Party", 1789, 1732, 1799)]
        GeorgeWashington = 0x1,
        [PresidentAttr("John Adams", "Federalist", 1797, 1735, 1826)]
        JohnAdams = 0x2,
        [PresidentAttr("Thomas Jefferson", "Democratic-Republican", 1801, 1743, 1826)]
        ThomasJefferson = 0x4,
        ...
        [PresidentAttr("Bill Clinton", "Democratic", 1993, 1946, 0)]
        BillClinton = 0x20000000000,
        [PresidentAttr("George W. Bush", "Republican", 2001, 1946, 0)]
        GeorgeWBush = 0x40000000000,
        [PresidentAttr("Barack Obama", "Democratic", 2009, 1961, 0)]
        BarackObama = 0x80000000000,
        _MostInfluential = AbrahamLincoln | FranklinDRoosevelt | GeorgeWashington
            | ThomasJefferson | AndrewJackson | TheodoreRoosevelt,
        _DiedInOffice = WilliamHenryHarrison | ZacharyTaylor | AbrahamLincoln | JamesAGarfield
            | WilliamMcKinley | WarrenGHarding | FranklinDRoosevelt | JohnFKennedy,
        _Assassinated = AbrahamLincoln | JamesAGarfield | WilliamMcKinley | JohnFKennedy,
    } 

Notice that we need to assign an integer number to each enum field in the order of 2^n in order for the flag bit operation to work. Also, we predefine three combined values, _MostInfluential, _DiedInOffice and _Assassinated. I prefer to prefix the combined values with an underscore "_" so they can be differentiated from single values and are put on top of the list of all possible values when displayed in the IntelliSense (see image below).

In the example we also need following helper method to parse a combined enum value into individual single enum values:

    private static PresidentFlagged[] ParseValues(PresidentFlagged combinedValue)
    {
        List<PresidentFlagged> values = new List<PresidentFlagged>();
        PresidentFlagged[] allvalues = (PresidentFlagged[])Enum.
            GetValues(typeof(PresidentFlagged));
        return Array.FindAll(allvalues, v =>
            ((combinedValue & v) == v) && !v.ToString().StartsWith("_"));
    } 

Notice when finding individual single values of a combined enum value we exclude the predefined combined values that are identified by the "_" prefix. Now the fun part:

    PresidentFlagged jfk = PresidentFlagged.JohnFKennedy;
    var jfkAttr = (PresidentAttr)jfk.GetAttr();
    Console.WriteLine("Was {0} died in office? {1}", jfkAttr.Name,
        ((PresidentFlagged._DiedInOffice & jfk) == jfk));
    Console.WriteLine("Was he Assassinated? {0}",
        ((PresidentFlagged._Assassinated & jfk) == jfk));

Output:
    Was John F. Kennedy died in office? True
    Was he Assassinated? True

    var Influentials = ParseValues(PresidentFlagged._MostInfluential);
    Console.WriteLine("Most influential presidents:");
    Console.WriteLine(string.Join(System.Environment.NewLine,
        Influentials.Select(p => ((PresidentAttr)p.GetAttr()).Name).ToArray()));

Output:
    Most influential presidents:
    George Washington
    Thomas Jefferson
    Andrew Jackson
    Abraham Lincoln
    Theodore Roosevelt
    Franklin D. Roosevelt

    var myFavorites = ParseValues(PresidentFlagged.AbrahamLincoln
        | PresidentFlagged.FranklinDRoosevelt
        | PresidentFlagged.BillClinton | PresidentFlagged.BarackObama);
    Console.WriteLine("My favorite presidents:");
    Console.WriteLine(string.Join(System.Environment.NewLine,
        myFavorites.Select(p => ((PresidentAttr)p.GetAttr()).Name).ToArray()));
    Console.WriteLine("Are they all Democratic? {0}",
        myFavorites.All(p => ((PresidentAttr)p.GetAttr()).Party == "Democratic"));

Output:
    My favorite presidents:
    Abraham Lincoln
    Franklin D. Roosevelt
    Bill Clinton
    Barack Obama
    Are they all Democratic? False

Conclusions

An enum pattern, in which each enumerated case is associated to an instance of a generic attribute class, is identified and implemented. It is particularly useful in situations where there is a finite number of cases and each case is characterized by a set of constants.

You can download the C# project below.

TestEnum.zip (9.74 kb)

About

A seasoned computer professional. A tofu culture evangelist...
more >>

Tag Cloud

Calendar

<<  April 2017  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

View posts in large calendar
Copyright © 2008-2011 Gong Liu. All rights reserved. | credits | contact me
The content on this site represents my own personal opinions, and does not reflect those of my employer in any way.