While working on a recent project, I ended up with three different models with integer
-type status
columns and nearly identical status-related functionality. They looked something like this:
class Post < ActiveRecord::Base
DRAFT = 0
PREVIEW = 1
PUBLISHED = 2
ARCHIVED = 3
STATUSES = {
DRAFT => :draft,
PREVIEW => :preview,
PUBLISHED => :published,
ARCHIVED => :archived
}
STATUSES.each do |value, name|
scope name, -> { where(status: value) }
define_method "#{name}?" do
status == value
end
define_method name do
update_attributes(status: value)
end
end
validates :status, presence: true, inclusion: { in: STATUSES.keys }
end
Having an integer
-type status
column gave me more flexibility over the number of statuses — as well as the flexibility to change the name of a status without having to make any database migrations (which would have been the case if I was storing strings). In my case, this was worth the tradeoff of the database having a different understanding of the data than the application.
After the third model, it was time to DRY things up. First, I thought about the simplest DSL to get me all the status functionality I needed, which led to the following:
class Post < ActiveRecord::Base
statuses :draft,
:preview,
:published,
:archived
end
As a result, SimplestStatus
was born!
Enter Enum
After implementing an extendable module (SimplestStatus
) with the statuses
DSL, I ran the idea past Mike, who mentioned the whole idea seemed just like enum
. At the time, I hadn't heard of enum
before.
If you're in that same boat, enum
is a model-level method introduced in Rails 4.1 that wraps integer
-type fields and provides a convenient set of functionality:
class Post < ActiveRecord::Base
enum status: [ :draft, :preview, :published, :archived ]
# or
enum status: {
draft: 0,
preview: 1,
published: 2,
archived: 3
}
end
This will get you class-level scopes as well as instance level setters and predicate methods — all things I had implemented myself in SimplestStatus. So why go with SimplestStatus?
Why SimplestStatus?
Compatibility
SimplestStatus's biggest advantage over enum
is that it doesn't depend on a certain version of Rails. It works with practically every version (only tested as far back as 2.0.5, but may well work fine with earlier versions than that).
Reader Methods and Constants
If a field is backed by an integer
column, I think there's value in exposing the integer to the application. Say you wanted to implement a Post#next_status
method that cycles through the list of statuses:
class Post < ActiveRecord::Base
extend SimplestStatus
statuses :draft, :preview, :published, :archived
def next_status
Post.statuses[(status + 1) % Post.statuses.size]
end
end
Looking at enum
, I wasn't a huge fan of the way it overwrote the reader method:
post.status # => 'draft'
SimplestStatus generates constants matching each status's name:
Post::DRAFT # => 0
Post::PREVIEW # => 1
Post::PUBLISHED # => 2
Post::ARCHIVED # => 3
Rather than having to remember the underlying integers, constants provide an expressive way to look up values. I found these helpful when setting up factories:
FactoryGirl.define do
factory :post do
# ...
status Post::PUBLISHED
# ...
end
trait :archived do
status Post::ARCHIVED
end
end
Label Methods
Let's say we added a status :crowd_favorite
to our list on the Post
model.
With enum
, you call post.status
and get a string matching the original name you gave it:
post.status # => 'crowd_favorite'
For use in a view or form, you'd probably end up capitalizing/titleizing everywhere.
SimplestStatus provides a #status_label
method:
post.status_label # => 'Crowd Favorite'
It also provides a label-friendly list of statuses for use in a form select:
Post.statuses.for_select
# => [['Draft', 0], ['Preview', 1], ['Published', 2], ['Archived', 3], ['Crowd Favorite', 4]]
Validations
SimplestStatus will automatically add the following validations:
validates :status, presence: true, inclusion: { in: proc { statuses.values } }
..and More!
Check out the README
to see all the other things you can do with SimplestStatus.
What's Next?
The original intent was to provide the simplest DSL possible to get all the convenient status functionality you could want. If there are things you find yourself doing around these integer
-backed fields that you'd like to see in SimplestStatus, open a Github issue or leave a comment below. I'd love to hear feedback.
When you're working on a Rails app having models with a status
— regardless of the Rails version — consider SimplestStatus
!
没有评论:
发表评论