programming-examples/ruby/Patterns/state.rb

111 lines
2.8 KiB
Ruby
Raw Normal View History

2019-11-15 12:59:38 +01:00
# 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