A Ruby state pattern implementation.
This library intentionally follows the classic state pattern implementation (no mixins, classical delegation to simple state classes, etc.) believing that it increases flexibility (internal DSL constraints vs plain object oriented Ruby power), simplicity and clarity.
The gem is ready for Rails active record integration (see below and the examples folder).
- Define the set of states you want your stateful object to have by creating a class for each state and inheriting from
StatePattern:State. - All public methods defined in this state classes, except
enterandexit(see below), are then available to the stateful object and their behaviour will depend on the current state . - If this automatic delegation to the current state public methods is not enough for your stateful object then you can just reopen the method and use super whenever you want to call the state implementation.
- Inside each state instance you can access the stateful object through the +stateful+ method.
- Inside each state instance you can access the previous state through the +previous_state+ method.
- Define
enterorexitmethods to hook any behaviour you want to execute whenever the stateful object enters or exits the state. - An event is just a method that calls
transition_toat some point. - If you want guards for some event just use plain old ifs before your
transition_to. - In the stateful object you must
set_initial_state.
So here's a simple example that mimics a traffic semaphore
require'state_pattern'classStop < StatePattern::Statedefnextsleep3transition_to(Go)enddefcolor"Red"endendclassGo < StatePattern::Statedefnextsleep2transition_to(Caution)enddefcolor"Green"endendclassCaution < StatePattern::Statedefnextsleep1transition_to(Stop)enddefcolor"Amber"endendclassTrafficSemaphoreincludeStatePatternset_initial_stateStopendsemaphore=TrafficSemaphore.newloopdoputssemaphore.colorsemaphore.nextendLet's now use one nice example from the AASM documentation and translate it to state_pattern.
require'state_pattern'classDating < StatePattern::Statedefget_intimatetransition_to(Intimate)ifstateful.drunk?enddefget_marriedtransition_to(Married)ifstateful.willing_to_give_up_manhood?enddefenterstateful.make_happyenddefexitstateful.make_depressedendendclassIntimate < StatePattern::Statedefget_marriedtransition_to(Married)ifstateful.willing_to_give_up_manhood?enddefenterstateful.make_very_happyenddefexitstateful.never_speak_againendendclassMarried < StatePattern::Statedefenterstateful.give_up_intimacyenddefexitstateful.buy_exotic_car_and_wear_a_comboverendendclassRelationshipincludeStatePatternset_initial_stateDatingdefdrunk?;@drunk;enddefwilling_to_give_up_manhood?;@give_up_manhood;enddefmake_happy;enddefmake_depressed;enddefmake_very_happy;enddefnever_speak_again;enddefgive_up_intimacy;enddefbuy_exotic_car_and_wear_a_combover;endendInside your state classes, any code that you put inside the enter method will be executed when the state is instantiated. You can also use the exit hook which is triggered when a successful transition to another state takes place.
If the automatic delegation to the current state public methods is not enough for your stateful object then you can just reopen the method and use super whenever you want to call the state implementation.
classTrafficSemaphoreincludeStatePatternset_initial_stateStopdefcolor# some great code here# now we call the current state implementationsuper# more cool hacking hereendendTo use the state pattern in your Rails models you need to:
- Add a state column for your model table of type string
- Include
StatePattern::ActiveRecordin your model file - Use the state pattern as you would do in a plain Ruby class as shown above
Please see the examples folder for a Rails 3 example.
Remember to put each class in its correct file following Rails naming conventions.
moduleBlogStates#we can put common state behaviour into a base state class or we could have implemented it inside the model with methods that call super, your choiceclassStateBase < StatePattern::Statedefsubmit!enddefpublish!enddefreject!transition_to(Rejected)stateful.save!enddefverify!endendclassPublished < StateBaseendclassPending < StateBasedefpublish!transition_to(Published)ifstateful.valid?stateful.save!endendclassUnverified < StateBasedefsubmit!ifstateful.submitter.manager?ifstateful.profile_complete?transition_to(Published)elsetransition_to(Pending)endstateful.save!endenddefverify!transition_to(Pending)stateful.save!endendclassRejected < StateBasedefpublish!transition_to(Published)ifstateful.valid?stateful.save!enddefenterNotifier.notify_blog_owner(stateful)endendendclassBlog < ActiveRecord::BaseincludeStatePattern::ActiveRecordset_initial_stateUnverified...endBy default StatePattern::ActiveRecord expects a column named state in the model. If you prefer to use another attribute do:
set_state_attribute:state_columnHow do I decide? state_pattern or AASM?
- Lot of state dependent behavior? Lot of conditional logic depending on the state? => state_pattern
- Not much state dependent behavior? => AASM
- Alvaro Gil for being the first using this gem in a real Rails project.
- Nicolás Sanguinetti for his great feedback.
gem install state_patternTo run the tests bundle and then bundle exec rake
http://github.com/dcadenas/state_pattern
Copyright (c) 2009 Daniel Cadenas. See LICENSE for details.


