README.rdoc in volute-0.1.0 vs README.rdoc in volute-0.1.1
- old
+ new
@@ -1,140 +1,397 @@
= volute
-It could be a 'set event bus', or a 'business logic relocator'.
+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.
-See examples/ and specs/
+== include Volute
-== usage
+When the Volute mixin is included in a class, its attr_accessor call is modified so that the resulting attributer set method, upon setting the value of the attribute triggers a callback defined outside of the class.
- gem install volute
+ require 'rubygems'
+ require 'volute' # gem install volute
+ class Light
+ include Volute
-== example : equation
+ attr_accessor :colour
+ attr_accessor :changed_at
+ end
- # license is MIT
+ volute :colour do
+ object.changed_at = Time.now
+ end
- $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+ l = Light.new
+ p l # => #<Light:0x10014c480>
- require 'volute'
+ l.colour = :blue
+ p l # => #<Light:0x10014c480 @changed_at=Fri Oct 08 20:01:52 +0900 2010, @colour=:blue>
- #
- # our class
+There is a catch in this example, the volute will trigger for any class that inccludes Volute and which sees a change to its :colour attribute.
- class Equation
+Those two classes would see their :colour hooked :
+
+ class Light
include Volute
- attr_accessor :km, :h, :kph
+ attr_accessor :colour
+ attr_accessor :changed_at
+ end
- def initialize
- @km = 1.0
- @h = 1.0
- @kph = 1.0
+ class Flower
+ include Volute
+
+ attr_accessor :colour
+ end
+
+To make sure that only instance of Light will be concerned, one could write :
+
+ volute Light do
+ volute :colour do
+ object.changed_at = Time.now
end
+ end
- def inspect
+Inside of a volute, these are the available 'variables' :
- "#{@km} km, #{@h} h, #{@kph} kph"
+* 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
- #
- # a volute triggered for any 'set' operation on an attribute of book
+ l = Light.new
+ l.colour = :blue
+ l.colour = :red
- volute Equation do
+would output :
- # object.vset(:state, x)
- # is equivalent to
- # object.instance_variable_set(:@state, x)
+ Light.colour : nil --> :blue
+ Light.colour : :blue --> :red
- volute :km do
- object.vset(:h, value / object.kph)
+
+== 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
- volute :h do
- object.vset(:kph, object.km / value)
+ end
+
+ volute Grant do
+ volute :paid do
+ puts "the :paid attribute of a Grant just changed"
end
- volute :kph do
- object.vset(:h, object.km / value)
+ 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
- #
- # trying
- e = Equation.new
- p e # => 1.0 km, 1.0 h, 1.0 kph
+== 'state volutes'
- e.kph = 10.0
- p e # => 1.0 km, 0.1 h, 10.0 kph
+"I want this volute to trigger when the patient has a sore_throat and the flu", would translate to
- e.km = 5.0
- p e # => 5.0 km, 0.5 h, 10.0 kph
+ 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) :
-== example : some kind of state-machine
+ volute Patient do
+ volute :sore_throat => true, :fever => true do
+ puts "it triggers"
+ end
+ end
- # license is MIT
- #
- # a state-machine-ish example, inspired by the example at
- # http://github.com/qoobaa/transitions
+:not applies as well :
- $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+ volute Patient do
+ volute :not, :leg_broken do
+ send_patient_back_home # we only treat broken legs
+ end
+ end
- require 'volute'
+Our sore_throat and flu example could be rewritten with Ruby ifs as :
- #
- # our class
+ volute Patient do
+ if object.sore_throat == true && object.flu == true
+ puts "it triggers"
+ end
+ end
- class Book
- include Volute
+which isn't hairy at all.
- attr_accessor :stock
- attr_accessor :discontinued
+Pointing to multiple values is OK :
- attr_reader :state
+ volute Package do
+ volute :delivered => true, :weight => [ '1kg', '2kg' ] do
+ puts "delivered a package of 1 or 2 kg"
+ end
+ end
- def initialize (stock)
- @stock = stock
- @discontinued = false
- @state = :in_stock
+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
- #
- # a volute triggered for any 'set' operation on an attribute of book
+ 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
- volute Book do
+For attributes whose values are strings, regular expressions may prove useful :
- # object.volute_do_set(:state, x)
- # is equivalent to
- # object.instance_variable_set(:@state, x)
+ volute :location => /^Fort .+/ do
+ puts "Somewhere in Fort ..."
+ end
- if object.stock <= 0
- object.volute_do_set(
- :state, object.discontinued ? :discontinued : :out_of_stock)
+There is an example that uses those 'state volutes' at http://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.volute_do_set(
- :state, :in_stock)
+ object.state = :off
end
end
- #
- # trying
+The key here is the call to
- emma = Book.new(10)
+ Volute.apply(object, attribute=nil, previous_value=nil, value=nil)
- emma.stock = 2
- p emma.state # => :in_stock
+In fact, for classes that include Volute, this method is called for each attribute getting set.
- emma.stock = 0
- p emma.state # => :out_of_stock
+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').
- emma.discontinued = true
- p emma.state # => :discontinued
+
+== volute blocks, closures
+
+TODO
+
+
+== volute management
+
+TODO
+
+
+== examples
+
+http://github.com/jmettraux/volute/tree/master/examples/
+
+
+== alternatives
+
+states
+
+- there is a list of ruby state machines at the end of http://jmettraux.wordpress.com/2009/07/03/state-machine-workflow-engine/
+
+rules
+
+- http://github.com/codeaspects/ruleby
+- http://rools.rubyforge.org/
+- ...
+
+aspects
+
+- http://github.com/gcao/aspect4r
+- http://github.com/teejayvanslyke/gazer
+- http://github.com/nakajima/aspectory
+- http://github.com/matthewrudy/aspicious
+- http://aquarium.rubyforge.org/
+
+- http://github.com/search?type=Repositories&language=ruby&q=aspect&repo=&langOverride=&x=15&y=25&start_value=1 (github search)
+
+hooks and callbacks
+
+- http://github.com/apotonick/hooks
+- http://github.com/avdi/hookr
+- http://github.com/auser/backcall
+- ...
+
+
+== author
+
+John Mettraux - http://github.com/jmettraux/
+
+
+== feedback
+
+* IRC freenode #ruote
+* jmettraux@gmail.com
== license
MIT, see LICENSE.txt