2015年6月15日星期一

SimplestStatus — An Enhanced Enum Implementation for Any Version of Rails

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!

没有评论:

发表评论