Every repository with this icon (
Every repository with this icon (
tree e4cc112dad0520d83851d6a465f81eea2aa15074
parent 637bfa3b396eb8228032bdac2d5cb51e759419b4
| name | age | message | |
|---|---|---|---|
| |
MIT-LICENSE | Fri Apr 17 15:00:36 -0700 2009 | |
| |
README.rdoc | ||
| |
Rakefile | Fri Apr 17 15:00:36 -0700 2009 | |
| |
examples/ | Tue Apr 21 14:39:13 -0700 2009 | |
| |
init.rb | Fri Apr 17 15:00:36 -0700 2009 | |
| |
install.rb | Fri Apr 17 15:00:36 -0700 2009 | |
| |
lib/ | Mon Apr 20 14:04:30 -0700 2009 | |
| |
tasks/ | Fri Apr 17 15:00:36 -0700 2009 | |
| |
test/ | Fri Apr 17 15:00:36 -0700 2009 | |
| |
uninstall.rb | Fri Apr 17 15:00:36 -0700 2009 |
Golem Statemachine
Golem adds Finite State Machine (FSM) behaviour to Ruby classes. Basically, you get a nice DSL (domain-specific language) for defining the FSM rules, and some functionality to enforce those rules in your objects. Although Golem was designed specifically with ActiveRecord in mind, it should work with any Ruby object.
Although the FSM pattern can potentially apply to a number of scenarios, in practice you’ll probably find it most useful in implementing complex business logic — the kind that requires multi-page UML diagrams describing an entity’s behavior over a series of events. Golem makes it much easier to implement and keep track of complicated, stateful behaviour, and the DSL you use to define your state machine in Ruby is specifically designed to make translation to and from UML easy.
Contents
- A Trivial Example: The ON/OFF Switch
- The DSL Syntax: A Tutorial
- A Real-World Example: Seminar Registration
- Installation
- Gollem vs. AASM
A Trivial Example: The ON/OFF Switch
A light switch is initially in an "off" state. When you flip the switch, it transitions to an "on" state. A subsequent "flip switch" event returns it back to an off state.
Here’s the UML state machine diagram of an on/off switch:
And here’s what this looks like in Ruby code using Golem:
require 'golem'
class LightSwitch
include Golem
define_statemachine do
initial_state :OFF
state :OFF do
on :flip_switch, :to => :ON
end
state :ON do
on :flip_switch, :to => :OFF
end
end
end
switch = LightSwitch.new
puts switch.current_state # ==> :OFF
switch.flip_switch!
puts switch.current_state # ==> :ON
switch.flip_switch!
puts switch.current_state # ==> :OFF
The DSL Syntax: A Tutorial
To define a statemachine (inside a Ruby class definition, after including the Golem module), place your definition inside the define_statemachine block:
class Monster
include Golem
define_statemachine do
end
end
Now to create some states (we skip the include Golem call in subsequent code, but it should still be there!):
class Monster
define_statemachine do
state :HUNGRY
state :SATIATED
end
end
And an event:
class Monster
define_statemachine do
state :HUNGRY do
on :eat, :to => :SATIATED
end
state :SATIATED
end
end
The block for each state describes what will happen when a given event occurs. In this case, if the monster is in the HUNGRY state and the eat event occurs, the monster becomes SATIATED.
Now to make things a bit more interesting:
class Monster
def likes?(food)
return true if food.kind_of?(Human)
end
define_statemachine do
state :HUNGRY do
on :eat do
transition :to => :SATIATED, :if => Proc.new{|obj, food| obj.likes?(food)}
transition :to => :HUNGRY, :action => Proc.new{|obj| puts "BLAH!!"}
end
end
state :SATIATED
end
end
Here the monster becomes SATIATED only if it likes the food that it has been given. The :if option (a.k.a. a ‘guard’ condition) takes a Proc that checks whether the monster likes the food. To better illustrate how this works, here’s how we would use our Monster statemachine:
monster = Monster.new food = Banana.new monster.eat!(food) # ==> "BLAH!!" puts monster.current_state # ==> :HUNGRY food = Human.find(:first) monster.eat!(food) puts monster.current_state # ==> :SATIATED
In the interest of cleaner, prettier code, we can re-write the same definition as follows:
class Monster
def likes?(food)
food.kind_of?(Human)
end
define_statemachine do
state :HUNGRY do
on :eat do
transition :to => :SATIATED, :if => :likes?
transition do |obj|
puts "BLAH!!"
end
end
end
state :SATIATED
end
end
Note that :if (i.e. the transition guard condition) can take a Symbol instead of a Proc. Unless there’s a good reason to do so, you should probably avoid using Procs and instead refer to methods within your class definition by name. This makes it easier to keep your business logic clean and organized. Also, if a transition does not cause the object to change states (i.e. we self-transiton from HUNGRY back to HUNGRY), you can skip the :to part. Note too that you can use a block to specify a transition action (instead of using the :action option).
Finally, every state can have an enter and exit action. This can take the form of a callback (via a Symbol), a Proc, or a block:
class Monster
def likes?(food)
food.kind_of?(Human)
end
define_statemachine do
state :HUNGRY do
on :eat do
transition :to => :SATIATED, :if => :likes?
transition do |obj|
puts "BLAH!!"
end
end
end
state :SATIATED do
enter do
puts "BURP!!!"
end
end
end
end
For a full list of commands available inside the define_statemachine block, have a look at the code in golem/dsl (starting with golem/dsl/state_machine_def.rb).
A Real-World Example: Seminar Registration
Monsters and On/Off switches are all well end good, but once you get your head around how a finite state machine works, you’ll probably want to do something a little more useful. Here’s an example of a course registration system, adapted from Scott W. Ambler's primer on UML2 State Machine Diagrams:
The UML state machine diagram:
The Ruby implementation (see blow for discussion):
$: << File.expand_path(File.dirname(__FILE__)+"/../lib")
require 'golem'
class Seminar
attr_accessor :status
attr_accessor :students
attr_accessor :waiting_list
attr_accessor :max_size
def initialize
@students = [] # list of students enrolled in the course
@max_class_size = 5
end
def seats_available?
@students.size < @max_class_size
end
def waiting_list_is_empty?
@waiting_list.empty?
end
def student_is_enrolled?(student)
@students.include? student
end
def add_student_to_waiting_list(student)
@waiting_list << student
end
def create_waiting_list
@waiting_list = []
end
def notify_waiting_list_that_enrollment_is_closed
@waiting_list.each{|student| puts "#{student}: waiting list is closed!"
end
def notify_students_that_the_seminar_is_cancelled
(@students + @waiting_list).each{|student| puts "#{student}: the seminar has been cancelled!"
end
include Golem
define_statemachine do
state :proposed do
on :schedule, :to => :scheduled
end
state :scheduled do
on :open, :to => :open_for_enrollment
end
state :open_for_enrollment do
on :close, :to => :closed_to_enrollment
on :enroll_student do
transition :if => :seats_available? do |seminar, student|
seminar.students << student
end
transition :to => :full, :if => Proc.new{|seminar, student| not seminar.student_is_enrolled?} do |seminar, student|
seminar.add_student_to_waiting_list(student)
end
end
end
state :full do
on :move_to_bigger_classroom, :to => :open_for_enrollment
on :drop_student do
transition :to => :open_for_enrollment,
:if => Proc.new{|seminar, student| seminar.student_is_enrolled?(student) && seminar.waiting_list_is_empty?} do
seminar.students.delete student
end
transition :if => :student_is_enrolled? do |seminar, student|
seminar.students.delete student
seminar.enroll_student! seminar.waiting_list.shift
end
end
on :enroll_student do
transition :if => :seats_available? do |seminar, student|
seminar.students << student
end
transition :action => :add_student_to_waiting_list
end
on :close, :to => :closed_to_enrollment
enter :create_waiting_list
end
state :closed_to_enrollment do
enter :notify_waiting_list_that_enrollment_is_closed
end
state :cancelled do
enter :notify_students_that_the_seminar_is_cancelled
end
# The 'cancel' event can occur in all states.
all_states.each do |state|
state.on :cancel, :to => :cancelled
end
initial_state :proposed
current_state_from :status
on_all_transitions Proc.new{|obj, event, transition, event_args| puts "Transitioning from #{transition.from.name.inspect} to #{transition.to.name.inspect}"}
end
end
s = Seminar.new
s.schedule!
s.open!
s.enroll_student! "bobby"
puts s.inspect
s.enroll_student! "eva"
puts s.inspect
s.enroll_student! "sally"
puts s.inspect
s.enroll_student! "matt"
puts s.inspect
s.enroll_student! "karina"
puts s.inspect
s.enroll_student! "tony"
puts s.inspect
s.enroll_student! "rich"
puts s.inspect
s.enroll_student! "suzie"
puts s.inspect
s.enroll_student! "fred"
puts s.inspect
s.drop_student! "sally"
puts s.inspect
s.drop_student! "bobby"
puts s.inspect
s.drop_student! "tony"
puts s.inspect
s.drop_student! "rich"
puts s.inspect
s.drop_student! "eva"
puts s.inspect
There are a few things to note in the above code:
- We use `current_state_from` to tell Golem that the current state will be stored in the `status` column (by default the state is stored in the `state` column).
- We log each transition by specifying a callback function for `on_all_transitions`. The Seminar object’s `log_transition` method will be called on each successful transition. The Event that caused the transition, and the Transition itself are automatically passed as the first two arguments to the callback, along with any other arguments that may have been passed in the event trigger.
Installation
Install as a Rails plugin:
script/plugin install git://github.com/zuk/golem_statemachine.git
If using Golem in an ActiveRecord model:
class Example < ActiveRecord::Base
include Golem
define_statemachine do
# ... write your statemachine definition ...
end
end
Also make sure that the underlying SQL table has a ‘state’ column of type string. If you want to store the state in a different column, use current_state_from like this:
define_statemachine do
current_state_from :status
# ...
end
For plain old Ruby classes, everything works the same way, except the state is not persisted, only stored in the object’s member variable (@state, by default).
Golem vs. AASM
There is already another popular FSM implementation for Ruby — rubyist's AASM (also known as acts_as_state_machine). Golem was developed from scratch as an alternative to AASM, with the intention of a better DSL and cleaner, easier to read code.
Golem’s DSL is centered around States rather than Events; this makes Golem statemachines easier to visualize in UML (and vice-versa). Golem’s DSL also implements the decision pseudostate (a concept taken from UML), making complicated business logic easier to implement.
Golem’s code is also more modular and more consistent, which will hopefully make extending the DSL easier.








