A common programming language feature is an “enum” type, or enumerated type, which is simply a static list of values. For example, in the C programming language, enumerated types are declared using the
Only values within the enumeration can be referenced:
Enums are a tool for consistency and safety. Declaring the “cardinal_directions”
enum ensures that all cardinal direction references are valid.
Unlike C, Ruby is a dynamic language which eschews type enforcement. Any object is replacable by any other object, provided that the replacing object implements the same interface as the replaced object. In Ruby, for example, one might say “anything can be a cardinal direction if it is capable of acting like a cardinal direction”.
Enumerated types could be considered inconsistent with Ruby’s design philosophy. Nevertheless, there are occasions when the constraints of an enumerated type are worth going against the grain.
An All Too Common Case
I recently worked on an application which had a
payment_type attribute on the
Order model. The
payment_type was stored as a string in the database, and the
Order class had a
PAYMENT_TYPES constant and validations to prevent the persistance of rogue payment types.
Code of this ilk is reasonable and quite common, and is effective for keeping cruft out of the database. Unfortunately, it is prone to a couple of problems.
1. Invalid payment types can still exist in our application code and tests
We are free to instantiate orders with invalid payment types as long as we don’t attempt to persist them.
2. References to allowed payment types will leak
Storing payment types as strings encourages the use of strings in application logic. After all, it is trivially easy to instantiate string literals.
This unrestricted propagation of strings is a dangerous maintenance burden. There is no guarantee that references to valid payment types will stay valid, and replacing or changing payment types will warrant widespread updates (aka shotgun surgery).
To avoid strings, we elected to use the clean and simple EnumeratedType gem to represent payment types. We began by defining a
PaymentType class ensures that all payment type references in the application code or tests are valid.
Next, we updated validations on the
Order model, added getters to return
PaymentType instances, and added setters to accept
coerce method grants flexibility when setting the “payment_type” attribute. It is able to handle both formal types, such as
PaymentType[:cash], as well as strings which might be submitted by a form, such a
Adding behavior to types
Strings, arrays, hashes, symbols; these primitive Ruby objects are versatile and ubiquitous, but are necessarily unspecialized, and cannot provide the sophistication required to represent a domain concept like a payment type. For example, if we wanted to replace
we would have to patch Ruby’s
String class with a
mail_delivery_required? method, which undermines
String‘s general purpose nature. Moreover, altering the behavior of core classes is regarded as a unwise.
Since instances of EnumeratedType are simple classes and not core primitives of the Ruby language, we can safely extend their behavior.
Notice that EnumeratedType is kind enough to provide us with handy predicate methods like
check?. Additionally, EnumeratedType has a facility for adding arbitrary metadata to declared types. For example, we might need to deprecate the “cash” payment type.
We can then modify our
Order setter method to warn us about deprecated types.
Writing custom getters and setters on ActiveRecord models is tiresome, so we extracted a gem called active_record_enumerated_type for convenience. Install the gem by adding
gem 'active_record_enumerated_type' to your Gemfile, and then run
To integrate EnumeratedType with a model, use the
restrict_type_of method to impose a type restriction for a given attribute.
In addition to getter/setter functionality, the gem also provides
I18n support and custom serialization.
I18n support is useful for humanizing formal-sounding type names. For example, we might want to display “Credit or Debit Card” to users rather than “credit card”. This is accomplished by adding translation data to “en.yml” and invoking the
Custom serialization is an important capability for traditional
enums. Developers care about symbolic, semantically strong representations of types, but there is no need to preserve such symbolic representations when types are compiled or serialized to the database. A C compiler, for example, will often substitute
enum values like
NORTH with integers to optimize performance. Fewer bytes are expended storing integers than strings.
By default, active_record_enumerated_type expects to store types as strings. However, it will support whatever serialization/deserialization strategy you prefer. Let’s assume that, for performance reasons, the “payment_type” attribute is mapped to an integer column rather than a string column. Defining custom
.deserialize methods allows us to coerce types to and from integers.
EnumeratedType vs Rails 4
Rails 4 ships with its own mechanism for declaring
enums with ActiveRecord, which is pretty cool. Here is an example usage from the Rails docs:
While convenient, there are some drawbacks to this approach.
enumis not supported in versions of Rails prior to Rails 4
- The dynamically generated mutation, predicate and scoping methods feel like method bloat to me. I’d prefer to opt-in to the generation of these methods or write my own.
enumdoesn’t handle collisions. For example, if we wanted to add a second attribute called “moderation status” which contains an “active” state, Rails would throw an exception.
enumuses strings to represent states. As discussed above, strings are Ruby primitives which cannot acquire domain behavior.
enumlacks I18n support (though it’s not tough to roll your own).
enumexpects you to serialize types as integers, and as such, isn’t compatible with legacy string attributes.
enums are a step in the right direction, but we like
active_record_enumerated_type because we like to Keep It Simple ™.
Enumerated types are ideal for situations when string representations of types begin to leak around your application, and are a reasonable balance between the dynamism of Ruby and the safety of a stronger type system.