state_machine gem

C

Chuck Remes

I'm working on a somewhat complex project that really begs for a state machine to keep the logic all straight. I've been playing with the state_machine [1] gem the last several days to see if it's suitable. I like the syntax and feature set, but I'm having difficulty mapping its capabilities into code.

The main problem has to do with the relationship between state machines and classes. While a class can have 1 (or more) state machines declared inside of it, a state machine cannot be used across 1 (or more) *classes.*

If I am correct about that limitation, I am wondering how to structure a large and complex state machine. I like to keep my classes small and simple so that they are easy to understand and debug. In the case where a class has many states and events, the business logic attached to the transitions (in my case) are rather complex. The size of the code in the class very quickly balloons (a few hundred lines).

Here's an example.

Assume I am building an application that connects to a web service. The web service is a gateway to other services behind it, but it acts as a broker for all requests. Upon connection to the web service gateway, the gateway can indicate if it is available to forward requests, delayed/behind in forwarding requests, or if the back-end services are offline.

So, I build a machine that has several states along with unique behavior for each one.

State - Starting
Initialize all libraries for connecting to the web service. Upon completion, transition to the Connecting state.

State - Connecting
Open a network connection to the web service. Upon timeout, transition to ConnectionError state. Upon successful connection, transition to the Connected state.

State - Connected
Ask the web service for its current status. The web service can indicate it is in 1 of 3 states itself. Okay, Delayed, or Offline. Depending upon the response, transition to the appropriate state. In the meantime, each of the Okay/Delayed/Offline states can get events indicating the web service's current status. It seems to me like this is appropriately handled by the "Super state" Connected rather than duplicating this functionality across multiple sub states.

As you can imagine from the example given above, jamming all of this functionality into a single class would result in a pretty huge class (hundreds or even thousands of lines).

I would like to offload some of that logic to other classes and have a state transition "hand off" to another class while everything is still controlled by the same overall machine. So, I guess what I want is a machine to be able to encompass multiple classes.

Does anyone else have any experience with this gem and building machines with more complex use-cases than the examples? Am I trying to shoe-horn this gem into a use-case that it isn't suited to solving? Should I consider using a different statemachine gem? Suggestions?

cr

[1] https://github.com/pluginaweek/state_machine
 
C

Chuck Remes

I'm working on a somewhat complex project that really begs for a state machine to keep the logic all straight. I've been playing with the state_machine [1] gem the last several days to see if it's suitable. I like the syntax and feature set, but I'm having difficulty mapping its capabilities into code.

The main problem has to do with the relationship between state machines and classes. While a class can have 1 (or more) state machines declared inside of it, a state machine cannot be used across 1 (or more) *classes.*

If I am correct about that limitation, I am wondering how to structure a large and complex state machine. I like to keep my classes small and simple so that they are easy to understand and debug. In the case where a class has many states and events, the business logic attached to the transitions (in my case) are rather complex. The size of the code in the class very quickly balloons (a few hundred lines).

I figured out how to do this. I had to move from the state_machine gem to the statemachine gem (note the lack of underscore in the second one). It has support for a "class context" in which all state actions are executed. It's actually pretty easy to change the context while the machine is running to get new or different behavior from another business logic class.

Here's a code example. This example also contains some code to show how to register for events in one context class (as a closure) but still have the event delivered to the correct context class if it changes.

require 'rubygems'
require 'statemachine'

class Registered
def on_event &blk
@block = blk
end

def call
@block.call
end
end

class Startup
attr_accessor :statemachine
attr_reader :block

def do_init()
register_events
statemachine.process_event:)started)
end

def change_context
fire
puts "change context for next state"
statemachine.context = Connected.new(statemachine, @registered_events)
end

def register_events
@registered_events = Registered.new
@registered_events.on_event { statemachine.process_event:)data_error) }
end

def fire
@registered_events.call
end

def on_error
puts "error in #{self.class}"
end
end

class Connected
attr_accessor :statemachine
attr_reader :block
def initialize sm, registered
@statemachine = sm
@registered_events = registered
@once = true
end

def initial_gw_logon
fire
puts "entered connected"

if @once
puts "attempting GW logon"
@once = false
end
end

def enable_order_modification
puts "enable order modification"
end

def enable_order_entry
puts "enable order entry"
end

def disable_order_entry
puts "disable order entry"
end

def disable_orders
puts "disable orders"
end

def fire
@registered_events.call
end

def on_error
puts "#{self.class}#on_error"
end
end # Connected

connection_machine = Statemachine.build do
startstate :eek:perational

superstate :eek:perational do
# we don't set a context within this builder block, so using :starting
# as the startstate causes an error; it immediately transitions to :starting
# and tries to run #on_init which doesn't exist anyplace yet (no context)
startstate :waiting

trans :waiting, :startup, :starting
event :data_error, :error_mode

state :starting do
on_entry :do_init

event :started, :connected, :change_context
end

superstate :connected do
startstate :gateway_down
on_entry :initial_gw_logon

state :gateway_down do
event :dc_down, :gateway_down
event :dc_up, :gateway_down
event :gw_down, :gateway_down
event :gw_up, :gateway_up
end

state :gateway_up do
on_entry :enable_order_modification
on_exit :disable_orders

event :dc_down, :gateway_up, :disable_order_entry
event :dc_up, :gateway_up, :enable_order_entry
event :gw_up, :gateway_up
event :gw_down, :gateway_down
end

end # superstate connected
end # superstate operational

state :error_mode do
on_entry :eek:n_error
event :eek:perate, :eek:perational_H, Proc.new { puts "exiting error mode"}
event :gw_up, :error_mode
end

end


startup = Startup.new
connection_machine.context = startup
connection_machine.context.statemachine = connection_machine

# after using the business logic in Startup, it switches the machine over to using
# the business logic in the Connected class
connection_machine.process_event:)startup)

connection_machine.process_event:)gw_up)

This produces:

Event: startup
exiting 'waiting' state
entering 'starting' state
Event: started
exiting 'starting' state
Event: data_error
exiting 'starting' state
exiting 'operational' superstate
entering 'error_mode' state
error in Startup
change context for next state
entering 'connected' superstate
Event: data_error
exiting 'gateway_down' state
exiting 'connected' superstate
exiting 'operational' superstate
entering 'error_mode' state
Connected#on_error
entered connected
attempting GW logon
entering 'gateway_down' state
Event: gw_up
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,580
Members
45,055
Latest member
SlimSparkKetoACVReview

Latest Threads

Top