Enumerations are widespread in OO programming, and are basically strongly-typed name/value pairs. There are a number of benefits to using enumerations, including:

  • The compiler ensures the values used in code are valid.
  • Reducing errors caused by transposing or mistyping numbers.
  • Making it easy to change values in the future.
  • Making code easier to read, this in turn reduces errors.
  • With enumerations, your code is less likely to fail if in the future someone changes the values corresponding to the member names. This section adapted from MSDN

While the OO ABL does not support the definition of native enumerations, we can emulate them to get some of these benefits.

In the reference implementation, all enumerations are named <info being enumerated>Enum, and appear in the package with which the data is most closely identified. A large number of enumerations will also appear in the support code package.

The basic method we use is to define the enumeration members as PUBLIC, STATIC, OpenEdge.Lang.EnumMember (or inheriting from that class) properties. We use a class for the enumeration members because it gives an additional level of strong-typing: we can now tell from an Interface which arguments or return values are enumerations instead of ordinal values. We can use the general OpenEdge.Lang.EnumMember class, or we can have our enumeration inherit from it, in which case we get even stronger typing. Note that this may not be feasible when working with flags.

When the static constructor runs - i.e. when the enumeration is referenced for the first time - the enumeration's members are initialised with an instance of a OpenEdge.Lang.EnumMember class. This only happens once for the enumeration, regardless of how many times the enumeration is accessed, because of the static nature of the property.

Each enumeration member is effectively a read-only property. This means that once the values are set in the code, they cannot be changed at runtime. Enumeration classes can also be made FINAL so that they cannot be overridden.

Example enumeration: QueryTypeEnum

In the example below, we have an enumeration for ABL query types: EACH, LAST, FIRST.

class OpenEdge.Lang.QueryTypeEnum inherits EnumMember:

class OpenEdge.Lang.QueryTypeEnum inherits EnumMember:
 
    define public static property Default as QueryTypeEnum no-undo get. private set.
    define public static property Each    as QueryTypeEnum no-undo get. private set.
    define public static property First   as QueryTypeEnum no-undo get. private set.
    define public static property Last    as QueryTypeEnum no-undo get. private set.
 
    constructor static QueryTypeEnum():
        QueryTypeEnum:Each  = new QueryTypeEnum('Each').
        QueryTypeEnum:First = new QueryTypeEnum('First').
        QueryTypeEnum:Last  = new QueryTypeEnum('Last').
 
        QueryTypeEnum:Default = QueryTypeEnum:Each.
    end constructor.
 
    constructor public QueryTypeEnum ( input pcName as character ):
        super (input pcName).
    end constructor.
 
    method public static QueryTypeEnum EnumFromString(pcQueryType as char):        
        define variable oMember as QueryTypeEnum no-undo.
 
        case pcQueryType:
            when QueryTypeEnum:Each:ToString() then oMember = QueryTypeEnum:Each. 
            when QueryTypeEnum:First:ToString() then oMember = QueryTypeEnum:First.
            when QueryTypeEnum:Last:ToString() then oMember = QueryTypeEnum:Last.
        end.
 
        return oMember.
    end method.
 
end class.

OpenEdge.Lang.EnumMember

The EnumMember type has the following properties. Both are optional, since each enumeration member is a unique instance.

NameTypeAccess LevelNotes
Value INTEGER public get, protected set A value is required when used for flags
Name CHARACTER public get, protected set Some human-readable value

Default values

In the reference implementation, we also have a Default property. This allows us to designate (again in code) which of the members acts as the default. It also means that consumers of this enumeration can code to QueryTypeEnum:Default and know that the behavior will always be the same across all uses when the Default is changed.

Not all enumerations have or need Default values.

EnumFromString() and EnumFromValue()

The ToString() method allows us to convert an enumeration value into a ABL- or human-readable value. There are cases where we have an integer or character value and want to turn it into an EnumMember value.

The STATIC EnumFromString() and EnumFromValue() methods provide for this, depending on the input values. Note that not all enumerations require either or both methods.

method static public EnumMember EnumFromValue(piDataType as integer):
  define variable oMember as EnumMember no-undo.
        
  case piDataType:
    when DataTypeEnum:None:Value               then oMember = DataTypeEnum:None.
    when DataTypeEnum:Character:Value          then oMember = DataTypeEnum:Character.
    // plenty more here 
  end case.
  return oMember.
end method.
 
  
 
 
method static public EnumMember EnumFromString(pcDataType as character):
  define variable oMember as EnumMember no-undo.
        
  case pcDataType:
    when DataTypeEnum:None:ToString()               then oMember = DataTypeEnum:None.
    when DataTypeEnum:Character:ToString()          then oMember = DataTypeEnum:Character.
    when DataTypeEnum:CharacterArray:ToString()     then oMember = DataTypeEnum:CharacterArray.
    // lots more 
  return oMember.
end method.

And a usage example


do iLoop = 1 to phBuffer:num-fields:
 
    hField = phBuffer:buffer-field(iLoop).
            
    case DataTypeEnum:EnumFromString(hField:data-type):
        when DataTypeEnum:Character then /* do something */.

 Overriding Equals()

An enum member can be compared with the ABL equality operator (= or eq) with no trouble. There are cases where developers want to compare a string value with an enumerator's member; in this case a STATIC Equals() method should be created.

An example of this would be in the case of using a DataTypeEnum to determine whether a BUFFER-FIELD's DATA-TYPE is DataTypeEnum:Character. The DATA-TYPE will return a string data type; we can either compare that using the ToString() method above, or encapsulate that comparison in the Equals() method (which is recommended).

Not all enums require an Equals() override.

Flag-style enumerators

Certain enums are to be used as flags, which allow a single member (variable, property etc) to hold more than one enumeration value. For instance, a UI control might be both enabled and hidden. Furthermore, the control might only have a single property to store the state of the control. Instead of creating separate enumerations for these 2 states, we can create a single enumeration (ActionStateEnum, below) that inherits from the reference implementation's FlagsEnum. We can then store (or use) a single value to represent multiple states for the control.

class OpenEdge.PresentationLayer.Common.ActionStateEnum inherits FlagsEnum:
 
    define public static property Enable  as ActionStateEnum no-undo get. private set.
    define public static property Disable as ActionStateEnum no-undo get. private set.
    define public static property View    as ActionStateEnum no-undo get. private set.
    define public static property Hide    as ActionStateEnum no-undo get. private set.
 
    constructor static ActionStateEnum():
        ActionStateEnum:Enable = new ActionStateEnum(1).
        ActionStateEnum:Disable = new ActionStateEnum(2).
        ActionStateEnum:Hide = new ActionStateEnum(4).
        ActionStateEnum:View = new ActionStateEnum(8).
    end constructor.
 
    constructor public ActionStateEnum (input piValue as integer):
        super(input piValue).
    end constructor.
 
end class.

The OpenEdge.Lang.FlagsEnum class has an IsA() method which is used to determine whether a value has a particular state, since we cannot simply use the ABL equality operator. Note that this requires that the properties' initial values are all powers of 2.

class OpenEdge.Lang.FlagsEnum : 
  method static public logical IsA (piValueSum as int, poCheckEnum as EnumMember):
    define variable iPos as integer no-undo.
    define variable iVal as integer no-undo.
    define variable iCheckVal as integer no-undo.
        
    iCheckVal = poCheckEnum:Value.
                        
    do iPos = 1 to 32 while iVal eq 0:
      iVal = get-bits(iCheckVal, iPos, 1).
    end.
        
    return (get-bits(piValueSum, iPos - 1, 1) eq iVal).
  end method.
end class.

For example:

def var oMyControl as UserControl.
 
oMyControl:ControlState = ActionStateEnum:Enable:Value + ActionState:View:Value.
/* 
MESSAGE oMyControl:ControlState is now 5
*/
 
/* other intervening code */
oMyControl:Visible = ActionStateEnum:IsA(
                          oMyControl:ControlState, /* set above */
                          ActionStateEnum:View).
 

References

An interesting overview of C# enumerations appears at http://geekswithblogs.net/BlackRabbitCoder/archive/2010/07/08/c-fundamentals-the-joys-and-pitfalls-of-enums.aspx ; the first comment on the post is especially interesting given that it takes the approach outlined in this document even with the existence of enums in the language.