volute¶ ↑
I wanted to write something about the state of multiple objects, I ended with something that feels like a subset of aspect oriented programming.
It can be used to implement toy state machines, or dumb rule systems.
include Volute¶ ↑
When the Volute mixin is included in a class, its attr_accessor call is modified so that the resulting attribute set method, upon setting the value of the attribute triggers a callback defined outside of the class.
require 'rubygems' require 'volute' # gem install volute class Light include Volute attr_accessor :colour attr_accessor :changed_at end volute :colour do object.changed_at = Time.now end l = Light.new p l # => #<Light:0x10014c480> l.colour = :blue p l # => #<Light:0x10014c480 @changed_at=Fri Oct 08 20:01:52 +0900 2010, @colour=:blue>
There is a catch in this example, the volute will trigger for any class that includes Volute and which sees a change to its :colour attribute.
Those two classes would see their :colour hooked :
class Light include Volute attr_accessor :colour attr_accessor :changed_at end class Flower include Volute attr_accessor :colour end
To make sure that only instances of Light will be affected, one could write :
volute Light do volute :colour do object.changed_at = Time.now end end
Inside of a volute, these are the available ‘variables’ :
-
object - the instance whose attribute has been set
-
attribute - the attribute name whose value has been set
-
previous_value - the previous value for the attribute
-
value - the new value
thus :
volute Light do volute :colour do puts "#{object.class}.#{attribute} : #{previous_value.inspect} --> #{value.inspect}" end end l = Light.new l.colour = :blue l.colour = :red
would output :
Light.colour : nil --> :blue Light.colour : :blue --> :red
filters / guards¶ ↑
A volute combines a list of arguments with a block of ruby code
volute do puts 'some attribute was set' end volute Light do puts 'some attribute of an instance of class Light was set' end volute Light, Flower do puts 'some attribute of an instance of class Light or Flower was set' end volute :count do puts 'the attribute :count of some instance got set' end volute :count, :number do puts 'the attribute :count or :number of some instance got set' end volute Light, :count do puts 'some attribute of an instance of class Light was set' puts 'OR' puts 'the attribute :count of some instance got set' end
As soon as 1 argument matches, the Ruby block of the volute is executed. In other words, arg0 OR arg1 OR … OR argN
If you need for an AND, read on to “nesting volutes”.
Filtering on attributes who match a regular expression :
class Invoice include Volute attr_accessor :amount attr_accessor :customer_name, :customer_id end volute /^customer_/ do puts "attribute :customer_name or :customer_id got modified" end
‘transition volutes’¶ ↑
It’s possible to filter based on the previous value and the new value (with :any as a wildcard) :
volute 0 => 100 do puts "some attribute went from 0 to 100" end volute :any => 100 do puts "some attribute was just set to 100" end volute 0 => :any do puts "some attribute was at 0 and just got changed" end
Multiple start and end values may be specified :
volute [ 'FRA', 'ZRH' ] => :any do puts "left FRA or ZRH" end volute 'GVA' => [ 'SHA', 'NRT' ] do puts "reached SHA or NRT from GVA" end
Regular expressions are OK :
volute /^S..$/ => /^F..$/ do puts "left S.. and reached F.." end
volute :not¶ ↑
A volute may have :not has a first argument
volute :not, Invoice do puts "some instance that is not an invoice..." end volute :not, :any => :delivered do puts "a transition to something different than :delivered..." end volute :not, Invoice, :paid do puts "not an Invoice and not a variation of the :paid attribute..." end
Not Bob or Charlie, Nor Bob and neither Charlie.
nesting volutes¶ ↑
Whereas enumerating arguments for a single volute played like an OR, to achieve AND, one can nest volutes.
volute Invoice do volute :paid do puts "the :paid attribute of an Invoice just changed" end end volute Grant do volute :paid do puts "the :paid attribute of a Grant just changed" end end
‘guarding’ inside of the volute block¶ ↑
As long as one doesn’t use a ‘return’ inside of a block, they’re just ruby code…
volute Patient do if object.sore_throat == true && object.fever == true puts "needs further investigation" elsif object.fever == true puts "only a small fever" end end
‘state volutes’¶ ↑
“I want this volute to trigger when the patient has a sore_throat and the flu”, would translate to
volute Patient do volute :sore_throat do if value == true && object.fever == true puts "it triggers" end end volute :fever do if value == true && object.sore_throat == true puts "it triggers" end end end
hairy isn’t it ? There is a simpler way, it constitutes an exception to the “volute arguments join in an OR”, but it reads well (I hope) :
volute Patient do volute :sore_throat => true, :fever => true do puts "it triggers" end end
:not applies as well :
volute Patient do volute :not, :leg_broken do send_patient_back_home # we only treat broken legs end end
Note that our sore_throat and flu example could be rewritten with Ruby ifs as :
volute Patient do if object.sore_throat == true && object.flu == true puts "it triggers" end end
which isn’t hairy at all.
Pointing to multiple values is OK :
volute Package do volute :delivered => true, :weight => [ '1kg', '2kg' ] do puts "delivered a package of 1 or 2 kg" end end
Not mentioning an attribute implies its value doesn’t matter when matching state, :any and :not_nil could prove useful though :
volute Package do volute :delivered => :not_nil do puts "package entered delivery circuit" end end volute Item do volute :weight => :any, :package => true do # dropping ":weight => :any" would make sense, but sometimes, when # tweaking volutes, a quick editing of :any to another value is # almost effortless end end
For attributes whose values are strings, regular expressions may prove useful :
volute :location => /^Fort .+/ do puts "Somewhere in Fort ..." end
There is an example that uses those ‘state volutes’ at github.com/jmettraux/volute/blob/master/examples/diagnosis.rb
‘over’¶ ↑
Each volute that matches sees its block called. In order to prevent further evaluations, the ‘over’ method can be called.
volute Package do volute do over if object.delivered # prevent further volute evaluation if the package was delivered end volute :location do (object.comment ||= []) << value end end
application of the volutes on demand¶ ↑
Up until now, this readme focused on the scenario where volute application is triggered by a change in the state of an attribute (in a class that includes Volute).
It is entirely OK to have classes that do not include Volute but are the object of a volute application :
class Engine attr_accessor :state def turn_key! @key_turned = true Volute.apply(self, :key_turned) end def press_red_button! Volute.apply(self) end end volute Engine do if attribute == :key_turned object.state = :running else object.state = :off end end
The key here is the call to
Volute.apply(object, attribute=nil, previous_value=nil, value=nil)
In fact, for classes that include Volute, this method is called for each attribute getting set.
This technique is also a key when building system where the volutes aren’t called all the time but only right before their result should matter (‘decision’ versus ‘reaction’).
volute blocks, closures¶ ↑
TODO
volute management¶ ↑
TODO
examples¶ ↑
github.com/jmettraux/volute/tree/master/examples/
alternatives¶ ↑
states
-
there is a list of ruby state machines at the end of jmettraux.wordpress.com/2009/07/03/state-machine-workflow-engine/
rules
aspects
-
github.com/search?type=Repositories&language=ruby&q=aspect&repo=&langOverride=&x=15&y=25&start_value=1 (github search)
hooks and callbacks
author¶ ↑
John Mettraux - github.com/jmettraux/
feedback¶ ↑
-
IRC freenode #ruote
-
jmettraux@gmail.com
license¶ ↑
MIT, see LICENSE.txt