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
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:
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
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
The SequenceContract module itself can be found
here for now.