volute

set event bus, on set callbacks, business logic relocator, whatever (ruby)

MIT License

Downloads
7.7K
Stars
19
Committers
1

= 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 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.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

http://github.com/jmettraux/volute/tree/master/examples/

== alternatives

states

rules

aspects

hooks and callbacks

== author

John Mettraux - http://github.com/jmettraux/

== feedback

== license

MIT, see LICENSE.txt