Enumerated Types in ActiveRecord

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 enum keyword:

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).

Transcending Strings

To avoid strings, we elected to use the clean and simple EnumeratedType gem to represent payment types. We began by defining a PaymentType class:

The 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 PaymentType instances.

The EnumeratedType 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 "cash".

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

with

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 cash? and 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.

ActiveRecordEnumeratedType

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 bundle install.

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

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 human method.

Custom Serialization

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 #serialize and .deserialize methods allows us to coerce types to and from integers.

EnumeratedType vs Rails 4 enum

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.

  1. enum is not supported in versions of Rails prior to Rails 4
  2. 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.
  3. enum doesn’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.
  4. enum uses strings to represent states. As discussed above, strings are Ruby primitives which cannot acquire domain behavior.
  5. enum lacks I18n support (though it’s not tough to roll your own).
  6. enum expects you to serialize types as integers, and as such, isn’t compatible with legacy string attributes.

Rails 4 enums are a step in the right direction, but we like enumerated_type and 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.

Further Reading

  1. EnumeratedType docs provide a good background for why enums are advantageous
  2. Enumerated Types on Wikipedia
Tweet at Foraker

Share this post!