Stateology

Clean and fast Object state transitions in Ruby using the Mixology C extension.

Supports:

  • Dynamic switching between states (mixing and unmixing modules)
  • Optional state_entry() and state_exit() hooks for each state (automatically called upon state entry and exit)
  • support for subclassing of classes that include Stateology (see below)
  • support for nested states, i.e states defined within other states
  • Clean DSL-style syntax
  • NO MAGIC! Stateology does not use any behind-the-scenes method aliasing nor does it make use of any hooks.

Use as in the following:

class Sample
    include Stateology

    state(:Happy) {
        def state_entry
            puts "entering Happy state"
        end

        def do_something
            puts "Pets a puppy"
        end

        def state_exit
            puts "exiting Happy state"
        end
    }

    state(:Angry) {
        def state_entry
            puts "entering Angry state"
        end

        def do_something
            puts "Kicks a puppy"
        end

        def state_exit
            puts "exiting Angry state"
        end
    }
end

s = Sample.new

# now switch to Happy state
s.state :Happy
s.do_something  #=> "Pets a puppy"

# now switch to Angry state
s.state :Angry
s.do_something  #=> "Kicks a puppy"

UPDATE:

  • made it so subclasses can inherit states from their superclasses e.g

    class A
        include Stateology
    
        state(:Happy) {
            def state_entry
                puts "entering Happy state"
            end
    
            def hello
                puts "hello from A"
            end
        }
    end
    
    class B < A
        state(:Happy) {
            def hello
                puts "hello from B"
            end
        }
    end
    
    b = B.new
    
    b.state :Happy
    #=> "entering Happy state"
    
    b.hello
    #=> "hello from B"
    
  • prior behaviour was for state_entry not to exist in class B as Happy module from class A was overwritten by the new Happy module in B

  • how does this fix work? the Happy module in B just includes any extant Happy module accessible in B

A FEW THINGS TO NOTE

  • When an object is instantiated it begins life in no state and only ordinary instance methods are accessible (The ordinary instance methods are those defined outside of any state() {} block)

  • The ordinary instance methods are available to any state so long as they are not overridden by the state.

  • To change from any given state to 'no state' pass nil as a parameter to the state method e.g s.state nil

  • 'no state', while not a state, may nonetheless have state_entry() and state_exit() methods; and these methods will be invoked on 'entry' and exit from 'no state'

  • The state_entry method for 'no state' is not automatically called on object instantiation. If you wish state_entry to run when the object is instantiated invoke it in the initialize() method.

  • The state_entry method can also accept parameters: e.g s.state :Happy, "hello" In the above the string "hello" is passed as a parameter to the state_entry() method of the Happy state.

  • The #state method can accept either a Symbol (e.g :Happy) or a Module (e.g Happy or Sample::Happy). The following are equivalent: s.state :Happy #=> change state to Happy s.state Sample::Happy #=> equivalent to above (note the fully qualified name; as Happy is a module defined under the Sample class)

  • The #state method can take a block; the block will be executed after the successful change of state: e.g s.state(:Happy) { s.hello } #=> hello method invoked immediately after change of state as it's in the block

  • alternatively; if the #state method is invoked internally by another instance method of the Sample class then a fully qualified module name is not required: state Happy #=> Fully qualified module name not required when #state invoked in an instance method

  • The #state method can also act as a 'getter' method when invoked with no parameters. It will return the current state name in Symbol form (e.g :Happy)

  • The #state_mod method works similarly to the #state 'getter' except it returns the Module representing the current state (e.g Sample::Happy)

  • The #state?(state_name) returns boolean true if the current state is equal to state_name, and false if not. state_name can be either a Module or a Symbol