Wednesday, August 02, 2006

» Implementing a state pattern in Ruby +

A few weeks ago, Maurice Codik blogged about a better state pattern than the one on RubyGarden. I posted a comment suggesting an alternative implementation using a hash of state blocks. Here is an updated version with a little bit of fleshing out:

module Stateful
class StateInitError < Exception; end
class StateSetError < Exception; end
def with_key(key, ex, &b)
if (@s_states.has_key?(key))
yield b
else
raise(ex, "No such state `#{key}'", caller)
end
end
def accept_states(*states)
@s_states = {}
@s_initial = @s_current = states[0]
states.each { |s| @s_states[s] = nil }
end
def reject_states(*states)
states.each { |s|
@s_states.delete(s)
if (@s_current == s)
if (@s_initial == s)
@s_current = nil
else
@s_current = @s_initial
end
end
}
end
def transition_to(s)
with_key(s, StateSetError) {
if (@s_states[s])
@s_states[s].call
end
@s_current = s
}
end
def state(s=nil, &b)
return @s_current if (s.nil?)
with_key(s, StateInitError) {
if (block_given?)
@s_states[s] = b
if (@s_initial == s)
transition_to(s)
end
else
transition_to(s)
end
}
end
end

########

class Connection
include(Stateful)
def initialize()
accept_states(:initial, :connected)
state(:initial) {
def connect
puts("connected")
transition_to(:connected)
end
def disconnect
puts("not connected yet")
end
}
state(:connected) {
def connect
puts("already connected")
end
def disconnect
puts("disconnecting")
transition_to(:initial)
end
}
end
def reset()
puts("reseting outside a state")
transition_to(:initial)
end
end

c = Connection.new
c.disconnect # not connected yet
c.connect # connected
c.connect # already connected
c.disconnect # disconnecting
c.connect # connected
c.reset # reseting outside a state
c.disconnect # not connected yet
c.transition_to(:connected)
c.disconnect # disconnecting
p c.state # :initial
c.reject_states(:initial)
p c.state # nil
c.disconnect # not connected yet
c.transition_to(:initial) # No such state...

Updated: Added #reject_states and made it auto-initialize the default state (i.e., first arg to #accept_states) without needing to call #transition_to explicitly in the class initialization, refactored #transition_to and #state.

0 Comments:

Post a Comment

<< Home