111 lines
2.8 KiB
Ruby
111 lines
2.8 KiB
Ruby
|
# Allow an object to alter its behavior when its internal state changes.
|
||
|
# The object will appear to change its class.
|
||
|
|
||
|
# 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
|
||
|
|
||
|
|
||
|
module StatePattern
|
||
|
class UnknownStateException < Exception
|
||
|
end
|
||
|
|
||
|
def StatePattern.included(base)
|
||
|
base.extend StatePattern::ClassMethods
|
||
|
end
|
||
|
|
||
|
module ClassMethods
|
||
|
attr_reader :state_classes
|
||
|
|
||
|
def state(state_name, &block)
|
||
|
@state_classes ||= {}
|
||
|
|
||
|
new_klass = Class.new(self, &block)
|
||
|
new_klass.class_eval do
|
||
|
alias_method :__old_init, :initialize
|
||
|
|
||
|
def initialize(context, *args, &block)
|
||
|
@context = context
|
||
|
__old_init(*args, &block)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
@state_classes[state_name] = new_klass
|
||
|
end
|
||
|
end
|
||
|
|
||
|
attr_accessor :current_state, :current_state_obj
|
||
|
|
||
|
def transition_to(state_name, *args, &block)
|
||
|
new_context = @context || self
|
||
|
klass = new_context.class.state_classes[state_name]
|
||
|
if klass
|
||
|
new_context.current_state = state_name
|
||
|
new_context.current_state_obj = klass.new(new_context, *args, &block)
|
||
|
else
|
||
|
raise UnknownStateException,"tried to transition to unknown state,#{state_name}"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def method_missing(method, *args, &block)
|
||
|
unless @current_state_obj
|
||
|
transition_to :initial
|
||
|
end
|
||
|
|
||
|
if @current_state_obj
|
||
|
@current_state_obj.send(method, *args, &block)
|
||
|
else
|
||
|
super
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
class Connection
|
||
|
include StatePattern
|
||
|
|
||
|
# you always need a state named initial
|
||
|
state :initial do
|
||
|
|
||
|
def connect
|
||
|
# move to state :connected. all other args to transition_to# are passed
|
||
|
# to the new state's constructor transition_to:connected, "hello from
|
||
|
# initial state"
|
||
|
puts "connected"
|
||
|
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
|
||
|
# you can also change the state from outside of the state objects
|
||
|
# transition_to:initial
|
||
|
puts "resetting outside a state"
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# Usage
|
||
|
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
|