Sunday, February 10, 2008

Sequential Contracts in Ruby - Design by Contract Extension


What?

Software Designed By Contract is constructed along with terms of use and guaranteed behavior.

There are (at least) three types of contracts:
  • Pre-conditional contracts specify conditions that have to be met before a function is used, e.g. the demand that a value passed into a function is greater than or equal to 0
  • Post-conditional contracts specify conditions that are guaranteed to be be fulfilled after a functionality is used, e.g. ruby's Array#flatten! method guarantees that the resulting array is either nil or one-dimensional.
  • Sequence contracts specify the sequence in which functionalities are allowed to be used. For example, a File class could specify the sequence File#open, multiple File#read's, File#close.
Why?

While documenting expected conditions gives the user of a software component an indicator what and what not to do, this cannot guarantee that conditions are actually met and can result in unexpected errors. Thus, we want a method to formally define contracts and ensure that they are met at run-time.

Ruby does not have built in support for contracts, but Ruby programmers Martin and Brian created a neat module for pre- and post-conditional contracts. However, it does not support sequential contracts. I recently had the need for sequential contracts in Ruby, and so decided to go ahead and implement it.

How?

To declare a sequential contract for a class, you first define the methods and then declare the sequence in which you expect the methods of any given object to be called. Let's say we have a Greeter class that creates conversing objects:

class Greeter

def greet
puts "Hello!"
end

def ignore
puts "Pss!"
end

def ask_question
puts "How are you?"
end

def talk
puts "Blah blah blah"
end

def give_answer
puts "Good thank you!"
end

def say_goodbye
puts "Bye!"
end

def leave
puts "Walking away..."
end
end


Obviously, we would not want our greeter to first greet and then ignore you, or leave before it says goodbye. So let's declare a sequence contract after the methods of the Greeter class:

g = Greeter.new
g.greet

g.ask_question
# Must say goodbye before you leave!
g.leave


Now, if we create a Greeter and have it behave in a bad manner (e.g. ask a question without saying hello, or leaving before saying goodbye), then an IllegalSequence exception is raised:


# Declare a sequential contract
extend SequenceContract
sequence_contract do
start do
transition 'greet' => :chat
transition 'ignore' => :walk_away
end
state :chat do
transition 'ask_question' => :chat
transition 'talk' => :chat
transition 'give_answer' => :chat
transition 'say_goodbye' => :walk_away
end
state :walk_away do
transition 'leave' => :done
end
end
Together with test cases, the whole baddabing looks like:

require 'test/unit'
class TestSequenceContract < Test::Unit::TestCase

class Greeter
# Declare a sequential contract
include SequenceContract

def greet
puts "Hello!"
end

def ignore
puts "Pss!"
end

def ask_question
puts "How are you?"
end

def talk
puts "Blah blah blah"
end

def give_answer
puts "Good thank you!"
end

def say_goodbye
puts "Bye!"
end

def leave
puts "Walking away..."
end

sequence_contract do
start do
transition 'greet' => :chat
transition 'ignore' => :walk_away
end
state :chat do
transition 'ask_question' => :chat
transition 'talk' => :chat
transition 'give_answer' => :chat
transition 'say_goodbye' => :walk_away
end
state :walk_away do
transition 'leave' => :done
end
end
end

def test_illegal_sequence
g = Greeter.new
assert_nothing_raised do
g.greet
g.ask_question
end
assert_raises SequenceContract::IllegalSequence do
g.leave
end
end

def test_legal_sequence
assert_nothing_raised do
g = Greeter.new
g.greet
g.ask_question
g.say_goodbye
g.leave
end
end

def test_multiple_instances
g = Greeter.new
assert_nothing_raised do
g.greet
g.ask_question
g.say_goodbye
end
g = Greeter.new
assert_raises SequenceContract::IllegalSequence do
g.leave
end
assert_nothing_raised do
g.greet
g.ask_question
g.say_goodbye
g.leave
end
end
end # TestSequenceContract

The SequenceContract module itself can be found here for now.

No comments: