One of the goals of The Project was to enable arbitrary reports. As a result, the amount of free-form text fields was kept to a minimum, and the amount of drop-down menus was as large as possible.
This, combined with the need to edit the contents and the order of the drop-downs at run-time, led me to consciously design a “pattern” of some kind to manage the complexity that would occur as more and more drop-downs were found hidden in the requirements.
The first inclination is to use VB.NET’s native enumerations like so:
Public Enum HatType Padres = 1 Chargers College Other End Enum
And we’re all familiar with its usage:
Dim superBowlHatType As HatType = _ HatType.Chargers
The pattern and its derivation
We’d like this same consumer-side usage, except we cannot hard-code the enumeration. I did float the possibility of using System.Reflection, but quickly rejected because 1) I didn’t think it was possible 2) if it was, building a dummy-friendly interface for it would likely be complex, and 3) centralizing it for all users across The Company would be equally horrible.
So I set on designing a simpler solution. First, I thought of what I wanted the consumer-side to look like. Essentially, just like the native enumeration maps a human-readable symbol to a number, I want the new Dynamic Enumeration to do the same:
Dim hatDynamicEnum As DynamicEnum = _ HatTypeDynEnum.getReference() Dim superBowlHatType As EnumValue = _ hatDynamicEnum.valueOf( "Chargers" ) ' superBowlHatType is now 2
This leads to our first class: EnumValue:
Simple enough. ID corresponds to the value of the enumeration. Order is what order in the list it appears.
Our initial consumer-side sketch also implied that there is a DynamicEnum type with at least a valueOf method:
DynamicEnum is an abstract base class. The mappings from String to EnumValue is done with the use of a Hashtable. Furthermore, just as native enumerations have a range of acceptable values (example in this post ranges from 1 to 4), so does the Dynamic Enumeration. Any values outside of this range will throw an exception.
convertToStrings() gives a string array representation of the contents of the dynamic enumeration, sorted on order. This comes in handy when filling the contents of a ComboBox.
DynamicEnum is abstract because of the populate() method. Subclasses must define how the inherited Hashtable, min value, and max values are filled in.
Speaking of which, in an attempt to follow DDD, let’s define a data access class that will let us retrieve and set values from our persistent source, whatever it may be:
If two variables have the same EnumValues from the same Dynamic Enumeration, then they are equivalent by virtue of their attributes. That sounds like a Value Object. On the other hand, I just defined a repository for it. That sounds like an Entity. So…I guess we could classify EnumValues as persistent Value Objects.
At this point, theoretically we have all we need to glue the in-memory data structure with its persistent representation. To adhere to the Single Responsibility Principle, though, I throw in a factory that will grab the values from the repository and populate the Hashtable, the min value, and the max value:
The build() method’s arguments are all by reference. Ideally, the build() method would take a DynamicEnum as its argument, but you’d run into a circular project dependence. Notice I also throw in getKeysSortedOnOrder()… kind of a kludge and it is there to avoid adding a dependency between DynamicEnum and its repository.
The reason why some classes are interfaces and the reason why there even is a DynamicEnumValueFactory will become apparent later in this post.
Let’s put it all together. Assume that HatTypeEnumValueRepository implements DynamicEnumValueRepository, HatTypeEnumValueFactory implements DynamicEnumValueFactory, and HatTypeEnum inherits from DynamicEnum.
From the consumer side, here is what would be necessary to set up the dynamic enumeration (one-time setup per process):
Dim hatTypeEnumValueRepo As HatTypeEnumValueRepository = _ HatTypeEnumValueRepository.getReference() Dim hatTypeEnumBldr As HatTypeEnumValueFactory = _ HatTypeEnumValueFactory.getReference(hatTypeEnumValueRepo) Dim hatTypeEnum As HatTypeEnum = _ HatTypeEnum.getReference(hatTypeEnumBldr)
Here is a test scenario of loading what is already in persistence, then adding a new value to the enumeration at run-time and using it immediately after:
' Usage ' Assume contents in persistence at this point are: ' ' ID Name Order ' 1 Padres 1 ' 2 Chargers 3 ' 3 College 2 ' 4 Other 4 hatTypeEnum.populate() Dim superBowlHatType As EnumValue = _ hatTypeEnum.valueOf( "Chargers" ) ' superBowlHatType.ID = 2 ' superBowlHatType.order = 3 ' inserts into persistence for this enum ' with specified order ' assumes persistence has auto-increment ' capability turned on Dim insertedValue As EnumValue = _ hatTypeEnumValueRepo.addValue( "Hunting" , 5) hatTypeEnum.populate() superBowlHatType = hatTypeEnum.valueOf( "Hunting" ) ' superBowlHatType.ID = 5 ' superBowlHatType.order = 5
In UML terms, here is the overall Dynamic Enumeration “pattern”:
In practice, DynamicEnumValueRepository can be an abstract base class with implementations for the usual case (see below). Subclasses can then simply redefine the name of the table in which the contents of the dynamic enumeration can be found. The same commentary applies to DynamicEnumValueFactory. They are presented as interfaces to retain maximal generality. This is, after all, a “pattern”.
In most cases, one implementation class for each interface will suffice because the usual case, in my experience, is the following:
|Table name: LocationEnum|
Occasionally, you will run into enumerations that are related. For example, a dynamic enumeration for car makes, and another for car models:
|Table name: CarMakeEnum|
|Table name: CarModelEnum|
In this case, a CarModelEnumValue class should inherit from EnumValue and define a make attribute. Similarly, CarModelDynEnumValueRepository should implement DynamicEnumValueRepository.
The same can be said for CarMakeEnumValue. I’d give this class a collection of car model Strings that, along with the CarModelDynEnum itself, CarMakeDynEnumValueFactory takes to construct CarMakeEnumValues. That way, it is trivial to have ComboBoxes that filter models based on the selected make.
This example is mainly why the “pattern” uses interfaces and an abstract base class. In most scenarios, one implementation will suffice, but you always have special cases where specific implementations are needed.
The next topic will probably be the Hashtable of Polymorphic Conversion Services! ooooh, sounds fancy…