Solutions for const and enums per class in Ruby
You may have encountered a situation where you must define constants and enums per class. In this article, we will explore three different solutions.
Active Record Style
One solution is to use ActiveRecord to define enums per class. With this approach, you can define an enum as a class method that takes a hash of symbol-value pairs as arguments. The following code snippet defines an enum called status with three possible values.
require 'active_record'
require 'active_record/enum'
class UserActiveRecordStyle < ActiveRecord::Base
enum status: { pending: :pending, registered: :registered, deleted: :deleted }
# or use shorthand
#enum :status, %i[ pending registered deleted ].index_by(&:itself)
def self.read_status
p UserActiveRecordStyle::statuses[:pending] # => :pending
p UserActiveRecordStyle.statuses # => {:pending=>:pending, :registered=>:registered, :deleted=>:deleted}
p statuses[:pending] # => :pending
# p UserActiveRecordStyle::PENDING # => error, not possible
end
end
UserActiveRecordStyle.read_status
Pros:
- Easy to use, with a concise syntax for defining enums per class.
- It provides a built-in way to retrieve the possible enum values as a hash.
- Automatically adds methods like
User.last.pending?
,User.last.pending!
Cons:
- Requires the use of
ActiveRecord
, which may not be necessary for all projects. - Enums defined in this way cannot be accessed as constants (e.g.,
UserActiveRecordStyle::PENDING
is not possible).
Mixed Style
Another solution is to use a combination of constants and enums to define values per class. In this approach, you define an array of constants, each representing a possible value for the enum, and then use a hash to map each constant to its value. See the following example:
class UserActiveRecordStyleMixed < ActiveRecord::Base
STATUSES = [
PENDING = :pending.freeze,
REGISTERED = :registered.freeze,
DELETED = :deleted.freeze,
].freeze
enum statuses: STATUSES.zip(STATUSES).to_h
def self.read_status
p UserActiveRecordStyleMixed::PENDING # => :pending
p UserActiveRecordStyleMixed.statuses # => {:pending=>:pending, :registered=>:registered, :deleted=>:deleted}
p UserActiveRecordStyleMixed.statuses[:pending] # => :pending
p PENDING # => :pending
p statuses[:pending] # => :pending
p UserActiveRecordStyleMixed::STATUSES # => [:pending, :registered, :deleted]
end
end
UserActiveRecordStyleMixed.read_status
Pros:
- Same as the ActiveRecord style from above
- Possible use of constants like
PENDING
,UserActiveRecordStyleMixed::PENDING
, etc.
Cons:
- More to write, but also more possibilities to use
- Requires the use of two different constructs - constants and enums
PORO Style
A third solution is to use plain old Ruby objects (POROs) to define constants per class. In this approach, you represent an array of constants and then use a hash to map each constant to its value. For example, the following code defines a variety of constants for the status enum and then uses a hash to map each regular to its value:
class UserPoroStyle
STATUSES = [
PENDING = :pending.freeze,
REGISTERED = :registered.freeze,
DELETED = :deleted.freeze,
].freeze
STATUS = STATUSES.zip(STATUSES).to_h.freeze
def self.read_status
p UserPoroStyle::REGISTERED # => :registered
p UserPoroStyle::STATUSES # => [:pending, :registered, :deleted]
p REGISTERED # => :registered
p STATUS # => {:pending=>:pending, :registered=>:registered, :deleted=>:deleted}
p STATUSES # => [:pending, :registered, :deleted]
p UserPoroStyle::STATUS # => {:pending=>:pending, :registered=>:registered, :deleted=>:deleted}
p UserPoroStyle::STATUS[:registered] # => :registered
p UserPoroStyle::STATUS[REGISTERED] # => :registered
end
end
UserPoroStyle.read_status
Pros:
- No need to import
ActiveRecord
- Ability to use constants like
PENDING
,UserActiveRecordStyleMixed::PENDING
, etc. - Full control over methods in class
Cons:
- You need to write your own methods like
pending?
pending!
, etc.
Summary
Each approach has its advantages and disadvantages. The Active Record style is the easiest to use and provides the most built-in functionality. The Mixed style allows you to use constants, which can also be helpful in another part of the code. The PORO style is the most flexible and can be used in non-Active Record code.