The State Pattern, Ruby-style
One of the cool things about using a language that allows for metaprogramming is that you can reduce the amount of boilerplate code you have to write. Common patterns can be put into libraries, and give you a small DSL that you can use to replicate this pattern all over your code. A good example of a common pattern with lots of boilerplate is the state pattern: where the behavior of an object changes depending on the internal state of the object. This can be accomplished by using a simple 'if' statement, or by modeling a finite state machine.
Check out a sample implementation on the RubyGarden wiki. We have a class for each state, a common parent class for the states, and a context class that delegates to the current active state class. That's a lot of code that doesnt really do much. Enter the StatePattern module, which puts all of this boilerplate in a small mix-in module, and ties everything together with some metaprogramming glue. Now we can write:
class Connection
include StatePattern
state :initial do
def connect
puts "connected"
transition_to :connected, "hello from initial state"
end
def disconnect
puts "not connected yet"
end
end
state :connected do
def initialize(msg)
puts "initialize got msg: #{msg}"
end
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
# initialize got msg: hello from initial state
c.connect # already connected
c.disconnect # disconnecting
c.connect # connected
# initialize got msg: hello from initial state
c.reset # reseting outside a state
c.disconnect # not connected yet
.. and the boilerplate is all gone. How's it all work? Each call to state defines a new subclass of Connection that is stored in a hash. Then, a call to transition_to instantiates one of these subclasses and sets it to the be the active state. Method calls to Connection are delegated to the active state object via method_missing. This was a tricky one to get right! Download the source from here.


2 Comments:
Here's a different take on a State-ful module:
module Stateful
def accept_states(*states)
@states = {}
states.each do |s|
@states[s] = nil
end
end
def transition_to(s)
if (@states.has_key?(s))
if (@states[s])
@states[s].call
end
end
end
def state(s, &b)
if (@states.has_key?(s))
if (block_given?)
@states[s] = b
end
end
end
end
########
class Connection
include Stateful
def initialize
accept_states(:initial, :connected)
state(:initial) do
def connect
puts("connected")
transition_to(:connected)
end
def disconnect
puts("not connected yet")
end
end
state(:connected) do
def connect
puts("already connected")
end
def disconnect
puts("disconnecting")
transition_to(:initial)
end
end
transition_to(:initial)
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
nice implementation. exactly what I need.
Post a Comment
<< Home