Upload
others
View
6
Download
0
Embed Size (px)
Citation preview
Software Engineering
Design by Contract
Software Engineering 2012 - 2013 Department of Computer Science Ben-Gurion university
Based on slides of: Mira Balaban Department of Computer Science Ben-Gurion university
R. Mitchell and J. McKim: Design by Contract by Example
Design By Contract The term Design by Contract was coined by Bertrand
Meyer while designing the Eiffel programming language within Eiffel Software company.
Eiffel implements the Design by Contract principles.
Bertrand Meyer won the 2006 ACM Software System Award for Eiffel
Software Engineering, 2012 Design by Contract 2
The Eiffel Tower, built in 1887 for the 1889 World Fair, was completed on time and within budget, as will software projects written in Eiffel.
A contract There are two parties
A Client - requests a service A Supplier - supplies the service
A Contract is the agreement between the client and the supplier
Two major characteristics of a contract Each party expects some benefits from the contract and is prepared
to incur some obligations to obtain them These benefits and obligations are documented in a contract
document
Benefit of the client is the obligation of the supplier, and vice versa.
Software Engineering, 2012 Design by Contract 3
DbC – The idea and metaphor
Motivation:
Organize communication between software elements
By organizing mutual obligations and benefits
Do it by a metaphor of
clients – request services from suppliers
suppliers – supply services
Client Supplier
Obligation Satisfy supplier
requirement
Guarantee service
Benefit Get service Impose requirements
Software Engineering, 2012 Design by Contract 4
DbC – The metaphor realization
Obligations and benefits are specified using contracts
Write contracts for classes and methods
Methods:
Preconditions
Post-conditions
Classes: Invariants
Client Supplier
Obligation Precondition Post-condition
Benefit Post-condition Precondition
Software Engineering, 2012 Design by Contract 5
What happens when a Contract
Breaks?
If everyone does their job, there is no problem
If the precondition is not satisfied –
the Customer is wrong! (The client has a bug).
If the precondition is satisfied, but the postcondition is not
the Service is wrong (The service has a bug).
From the Client’s perspective, “true” is the best precondition.
In general, weaker preconditions are better.
From the Server’s perspective, “false” is the best precondition.
In general, stronger preconditions mean an easier job with the
implementation.
Software Engineering, 2012 Design by Contract 6
DbC Nature
Dbc promotes software specification together with or prior
to code writing
Writing contracts needs some principles and guidelines
The DbC principles tell us how to organize a class features
(attributes, methods)
The contract of a class is its interface
Software Engineering, 2012 Design by Contract 7
Design by Contract, by Example
Software Engineering, 2012 Design by Contract 8
COMMANDS
Notations features
attributes routines
procedures
creation other
functions
Software Engineering, 2012 Design by Contract 9
Six Principles –
the SIMPLE_STACK example
Software Engineering, 2012 Design by Contract 10
SIMPLE_STACK example – initial try
SIMPLE_STACK is a generic class, with type parameter G:
Simple_stack of G.
Features:
Queries – functions; No side effect:
count(): Integer
is_empty(): Boolean
Initialization:
initialize()
Commands:
push(g:G) no return value.
pop(): out parameter g:G; no return value.
SIMPLE_STACK
count()
is_empty()
initialize()
push(g:G)
pop()
G
Software Engineering, 2012 Design by Contract 11
Separate commands from queries (1)
Writing a contract for push: Takes a parameter g.
Places g on the top of the stack.
but pop does not return a value. Just removes. Redesign pop:
New push contract:
push(g:G)
Purpose: Push g onto the top of the stack.
ensure:
g = pop ???
pop(): G
purpose: Remove top item and return it.
push(g:G)
Purpose: Push g onto the top of the stack.
ensure:
g = pop
Software Engineering, 2012 Design by Contract 12
Separate commands from queries (2) Serious problem:
Evaluation of the post-condition changes the stack!
Solution: Split pop into two operations: 1. Query:
2. Command:
push contract:
top(): G
purpose: return the item at the top of the stack.
delete()
purpose: deletes the item at the top of the stack.
push(g:G)
purpose: Push g onto the top of the stack.
ensure:
top = g
Software Engineering, 2012 Design by Contract 13
Separate commands from queries (3) Standardize names:
Class: SIMPLE_STACK Queries:
count(): Integer
purpose: No of items on the stack.
item(): G
purpose: The top item
is_empty(): Boolean
purpose: Is the stack empty?
Creation commands: initialize()
purpose: Initialize a stack (new or old) to be empty.
Operations (other commands): put(g:G)
purpose: Push g on top of the stack.
remove()
purpose: Removes the top item of the stack.
A standard name to add
/delete item from any
container class
Boolean queries have names
that invite a yes/no question
Software Engineering, 2012 Design by Contract 14
Separate commands from queries (4)
Principle 1: Separate commands from queries.
Queries: Return a result. No side effects. Pure functions.
Commands: Might have side effects. No return value.
Some operations are a mixture: pop() – removes the top item and returns it
Separate into two pore primitive command and query of which it is
mixed
Software Engineering, 2012 Design by Contract 15
Separate basic queries from derived
queries (1) Post-condition of is_empty:
Result is a contract built-in variable that holds the result that a function returns to its caller.
The effect of is_empty() is defined in terms of the count query.
is_empty() is a derived query:
It can be replaced by the test:
count() = 0.
Contracts of other features can be defined in terms of basic queries alone.
No need to state the status of derived queries –
no need to state in the post-condition of put() that is_empty() is false.
state: count is increased, infer : is_empty=false from the contract of is_empty
is_empty(): Boolean
purpose: Is the stack empty?
ensure:
consistent_with_count:
Result = (count()=0)
Software Engineering, 2012 Design by Contract 16
Separate basic queries from derived
queries (2)
Principle 2:
Separate basic queries from derived queries.
Derived queries can be specified in terms of basic queries.
Principle 3:
For each derived query, write a post-condition that defines
the query in terms of basic queries.
Software Engineering, 2012 Design by Contract 17
Specify how commands affect basic
queries (1) Queries provide the interface of an object:
all information about an object is obtained by querying it.
Derived queries are defined in terms of basic queries (principle3).
The effect of a command on an object should be specified in terms of basic queries.
For Simple_stack, define the effects of
put()
initialize()
remove()
in terms of the basic queries
count()
item()
Software Engineering, 2012 Design by Contract 18
Specify how commands affect basic
queries (2) The put command:
put() increases count by 1. put() affects the top item.
“@pre” is borrowed from OCL (Object Constraint Language)
count()@pre refers to the value of the query count() when put() is called.
put(g:G) Purpose: Push g onto the top of the stack.
ensure:
count_increased:
count() = count()@pre + 1
g_on_top:
item() = g
Software Engineering, 2012 Design by Contract 19
Specify how commands affect basic
queries (3) The initialize() command:
Turns count() to 0: post-condition count() = 0.
Following initialization the stack includes no items. Therefore, no top item The query item() cannot be applied.
Implies a pre-condition for the query item:
Together, the 2 contracts, guarantee that applying item after initialize is illegal!
initialize()
purpose: Turns a stack (new or old) to be empty. ensure:
stack_is_empty:
count() = 0
item() : G
purpose: The top item on the stack.
require:
stack_is_not_empty:
count() > 0
Software Engineering, 2012 Design by Contract 20
Specify how commands affect basic
queries (4) The remove() command:
Two effects:
Reduces the number of items by one.
Removes the top item, and uncovers the item pushed before the top one.
Pre-condition: Stack is not empty.
Problem: How to express the 2nd post-condition?
The only queries are count and item. No way to refer to previous items.
remove() purpose: The top item on the stack.
require: stack_not_empty: count() > 0 ensure: count_decreased:
count() = count()@pre - 1
Software Engineering, 2012 Design by Contract 21
Specify how commands affect basic
queries (5) Rethink the basic queries.
Needed: A basic query that enables querying any item on the stack.
New basic query:
The new basic queries are:
count()
item_at()
The contracts of all other queries and commands need to be redefined!
item_at(i : Integer) : G
purpose: The i-th item on the stack.
item_at(1) is the oldest;
item_at(count) is the youngest, and the stack top. require: i_large_enough: i > 0 i_small_enough: i <= count
Software Engineering, 2012 Design by Contract 22
Specify how commands affect basic
queries (6)
The item query is no longer a basic query
It turns into a derived query: item = item_at(count)
item() : G
purpose: The top of the stack.
require:
stack_not_empty:
count() > 0
ensure:
consistent_with_item_at:
Result = item_at(count)
Software Engineering, 2012 Design by Contract 23
Specify how commands affect basic
queries (7)
The put command revisited:
put(g : G)
purpose: Push g on top of the stack.
ensure:
count_increased:
count() = count()@pre + 1
g_on_top:
item_at(count() ) = g
Software Engineering, 2012 Design by Contract 24
Specify how commands affect basic
queries (8) The initialize creation command revisited:
The precondition of item_at(i) together with count()=0 implies the second post-condition – this is summarized as a comment
initialize()
purpose: Initialize the stack to be empty.
ensure:
empty_stack:
count() = 0
item_at is undefined:
--For item_at(i), i must be in the interval [1,count],
which is empty, since count = 0
-- there are no values of i for which item_at(i)_ is defined
Software Engineering, 2012 Design by Contract 25
Specify how commands affect basic
queries (9) The remove command revisited:
No need for another post-condition about the new top:
Once count is decreased, the new top of the stack: item() - is item_at(count() )
remove()
purpose: Remove the top item from the stack.
The new top is the one, put before the last one.
require:
stack_not_empty: count() > 0
ensure:
count_decreased:
count() = count()@pre - 1
Software Engineering, 2012 Design by Contract 26
Specify how commands affect basic
queries (10)
Principle 4:
For each command, specify its effect on basic queries.
Implies its effect on derived queries.
Usually: Avoid specifying queries that do not change.
Principle 5:
For each query and command, determine a pre-condition.
Constrains clients.
Software Engineering, 2012 Design by Contract 27
summary Every command specifies its effect on every basic query
Sometimes the specification is direct – an explicit assertion in the post condition
Sometimes the specification is indirect
Initialize specifies that count =0. The precondition of item_at() implies that there are no valid values of i for which item_at can be called
Indirectly initialize specifies the effect on item_at: it makes it invalid to call item_at()
All the derived queries have post conditions that specify their results in terms of the basic queries
Software Engineering, 2012 Design by Contract 28
Class invariants and class correctness
A class invariant is an assertion that holds for all instances
(objects) of the class
A class invariant must be satisfied after creation of every
instance of the class
The invariant must be preserved by every method of the class,
i.e., if we assume that the invariant holds at the method entry it
should hold at the method exit
We can think of the class invariant as conjunction added to
the precondition and post-condition of each method in the class
Software Engineering, 2012 Design by Contract 29
Class invariants Capture unchanging properties of a class objects by invariants
For the SIMPLE_STACK class, the non-negative value of count is an invariant:
Argument (proof):
For an initialized object: count() = 0
Count() is decreased by remove, but it has the precondition:
count() > 0
Principle 6: Write invariants to define unchanging properties of objects.
Provide a proof for each invariant.
A good collection of class invariants might involve all method contracts (in their proofs)
(if the invariant can be inferred from the contracts of the features it is redundant.
Include it ?
invariant:
count_is_never_negative:
count() >= 0
Software Engineering, 2012 Design by Contract 30
The SIMPLE_STACK class interface (1)
Class SIMPLE_STACK(G)
1. Basic queries:
2. Derived queries:
count(): Integer purpose: The number of items on the stack item_at(i : Integer) : G purpose: The i-th item on the stack. item_at(1) is the oldest;
item_at(count) is the youngest, and the stack top. require: i_large_enough: i > 0 i_small_enough: i <= count()
item() : G purpose: The top of the stack.
require: stack_not_empty: count() > 0 ensure: consistent_with_item_at: Result = item_at( count () )
is_empty: Boolean purpose: Is the stack empty from items?
Software Engineering, 2012 Design by Contract 31
The SIMPLE_STACK class interface (2)
Class SIMPLE_STACK(G)
…
3. Creation commands:
initialize()
purpose: Initialize the stack to be empty. ensure: empty_stack: count() = 0 item_at is undefined: For item_at(i), i must be in the interval
[1,count], which is empty, since count = 0
Software Engineering, 2012 Design by Contract 32
The SIMPLE_STACK class interface (3)
4. Other commands:
5. Invariant:
put(g : G) purpose: Push g on top of the stack.
ensure: count_increased: count() = count()@pre + 1 g_on_top: item_at(count() ) = g
remove purpose: Remove the top item from the stack. The new top is the
one, put before the last one. require: stack_not_empty: count() > 0
ensure: count_decreased: count() = count()@pre – 1
count_is-never_negative: count() >= 0
Software Engineering, 2012 Design by Contract 33
The basic queries form a conceptual
model The two basic queries count and item_at give us a model of a
stack object
Using this model we can say all there is to say about stacks:
What the stuck looks like when it is just been initialized:
Count = 0 and there are no items since there is no i for which items_at(i) is valid.
What the effect of put(g) is : count is increased a g is item_at(count)
What the effect of remove is: count has decreased
What the result of is_empty is: The same as count=0
What the result of item is: the same as item_at(count)
Software Engineering, 2012 Design by Contract 34
The basic queries form a conceptual
model We have devised a conceptual model of stacks.
Stacks have an ordered set of items (item_at(1), item_at(2),item_at(3), and so on)
We know how many items there are Count
The class designer devises the model and uses it as the basis of the contracts that specify the features of the class.
The programmer of the class can see the model and devise a suitable implementation model to represent it.
30
20
10
Count=3
item_at(3)=30
item_at(2)=20
item_at(1)=10
Software Engineering, 2012 Design by Contract 35
The 6 principles 1. Separate commands from queries.
2. Separate basic queries from derived queries.
3. For each derived query, write a post-condition that defines
the query in terms of basic queries.
4. For each command, specify its effect on basic queries.
5. For each query and command, determine a pre-condition.
6. Write invariants to define unchanging properties of objects.
Software Engineering, 2012 Design by Contract 36
Building Support for Contracts -
Immutable (Value) Lists
Software Engineering, 2012 Design by Contract 37
Contracts for Immutable (Value) Lists
Contracts are written in expression languages, without side effects (functional languages). (Why? -- recall the Simple_stack class)
Contracts for clients of collection classes need to inspect the members of their collections.
Such contracts need side-effect protected operations on collections.
A conventional approach: Contracts that handle collection objects create them as value (immutable objects). A client can hold a regular collection object, like a hash table, but create an
immutable copy for the purpose of contract evaluation.
We start by defining the code for Immutable list
Software Engineering, 2012 Design by Contract 38
The IMMUTABLE_LIST class interface (1)
IMMUTABLE_LIST is a generic class, with type parameter G: IMMUTABLE_LIST of G.
Features: Basic Queries:
Derived queries:
Head (): G
Purpose: The first item on the list
Tail (): IMMUTABLE_LIST(G)
Purpose: A new list, formed from the current list (termed ‘self’, minus the ‘head’
is_empty (): Boolean Purpose: Does the list contain no items?
count (): INTEGER
Purpose: The number of items in the list
Software Engineering, 2012 Design by Contract 39
The IMMUTABLE_LIST class interface (2)
Features: derived Queries:
Creation commend:
…
cons(g:G): IMMUTABLE_LIST(G)
Purpose: A new list, formed from g as a head and the self list as a tail
is_equal( other: IMMUTABLE_LIST(G) ) : BOOLEAN
Purpose: Compare all ordered elements in self and in other
item( i:INTEGER ): G Purpose: The i-th item in the list (starting from 1)
sublist( from_position:INTEGER, to_position:INTEGER ) : IMMUTABLE_LIST(G)
Purpose: A new list, formed from the self items at from_position to to_position
initialize ()
Purpose: Initialize a list to be empty
Software Engineering, 2012 Design by Contract 40
Contracts of the basic queries
Basic Queries:
No post conditions: regular for basic queries
head (): G
Purpose: The first item on the list
require:
not_empty: not is_empty()
tail (): IMMUTABLE_LIST(G)
Purpose: A new list, formed from the self list, minus the ‘head’
require:
not_empty: not is_empty()
is_empty: Boolean
Purpose: Does the list contain no items?
Software Engineering, 2012 Design by Contract 41
Contract of the creation command
The creation command initialize empties a list (either new or old).
It takes no arguments – no precondition
Following its application: 1. The list should be empty. 2. head() and tail() should not be applicable 3. is_empty() should be true
Arguments for the post conditions on head() and tail():
Their pre-condition require: not is_empty(), which is false following initialize()
Creation command: initialize ()
Purpose: Initialize a list to be empty
ensure:
empty: is_empty()
Software Engineering, 2012 Design by Contract 42
Contracts of the derived queries: count
The post-condition of count():INTEGER 1. If the list is empty, count() is 0. 2. If the list is not empty, count() is the count() on the tail of the
list + 1. Derived query:
count ():INTEGER Purpose: The number of items in the list
ensure:
count_zero_for_an_empty_list:
is_empty() implies (Result = 0)
count_for_a_non_empty_list:
not is_empty() implies (Result = tail().count() + 1) Evaluated recursively
Software Engineering, 2012 Design by Contract 43
Contracts of the derived queries: cons
The post-condition of cons(g:G ):IMMUTABLE_LIST):
1. The new list has, g as its head.
2. The new list has self as its tail.
3. The new list is not empty
Derived query:
cons ( g:G ):IMMUTABLE_LIST)
Purpose: A new list, formed from self and g, as its head
ensure:
not_empty: not Result.is_empty()
head_is_g: Result.head() = g
tail_is_self: Result.tail ().is_equal(self)
Software Engineering, 2012 Design by Contract 44
Contracts of the derived queries: item
1. The pre-condition of item(i:INTEGER):G is that i is in the range [1..count()]
2. The post-condition of item(i:INTEGER):G: 1. The 1st item is the head
2. For i>=1, the i-th item is the (i-1)-th item of the tail
Derived query:
The if operator evaluates its else component only if its predicate evaluates to false (or else – eiffel)
item ( i:INTEGER ):G
Purpose: The i-th item on the list
require:
i_large_enough: i >= 1
i_small_enough: i <= count()
ensure:
correct_item: if i=1
then Result = head()
else Result = tail().item(i-1)
( i=1 and Result = head()
orelse
Result = tail().item(i-1)
Evaluated recursively
Software Engineering, 2012 Design by Contract 45
Contracts of the derived queries: item
1. The pre-condition of item(i:INTEGER):G is that i is in the range [1..count()]
2. The post-condition of item(i:INTEGER):G: 1. The 1st item is the head
2. For i>=1, the i-th item is the (i-1)-th item of the tail
Derived query:
The if operator evaluates its else component only if its predicate evaluates to false (or else – eiffel)
item ( i:INTEGER ):G
Purpose: The i-th item on the list
require:
i_large_enough: i >= 1
i_small_enough: i <= count()
ensure:
correct_item: if i=1
then Result = head()
else Result = tail().item(i-1)
( i=1 and Result = head()
orelse
Result = tail().item(i-1)
Evaluated recursively
Evaluation of postcondition for the list [5,4,7,2] with i=3
[5,4,7,2].item(3)=
[4,7,2].item(2)= --i.e. item(i-1) of tail
[7,2].item(1)= --again item(i-1) of tail
7 --this time i=1 so the result is the head of the list
Software Engineering, 2012 Design by Contract 46
Contracts of the derived queries: is_equal
1. The pre-condition of is_equal(other:IMMUTABLE_LIST):BOOLEAN is that the argument list exists (e.g., is not a null pointer).
2. The post-condition of is_equal(other:IMMUTABLE_LIST):BOOLEAN : 1. Both lists might be empty
2. Both lists are not empty, and their heads and tails are equal.
Derived query:
or_else and and_then are Eiffel’s operators for optimized evaluation of or and and
is_equal(other:IMMUTABLE_LIST):BOOLEAN
Purpose: Are the 2 lists, self and other equal in elements and order?
require:
other_exists: other /= null
ensure:
same_content: Result = ( is_empty() and other.is_empty() )
or_else
( ( (not is_empty()) and (not other.is_empty()) )
and then
( head() = other.head()
and
tail().is_equal( other.tail() ) ) )
Evaluated recursively
Software Engineering, 2012 Design by Contract 47
Contracts of the derived queries: sublist
sublist( from_position:INTEGER, to_position:INTEGER ):IMMUTABLE_LIST:
1. The pre-condition requires that the positions are in the [1..count()] range,
and are consistent: from_position is <= to_position+1 (to enable extracting an
empty sublist).
2. The post-condition:
1. If to_position is smaller than from_position
the extracted sublist is empty
2. Otherwise, the extracted list consists of the list elements at positions
from_position to to_position
A new list is constructed
The contract:
Software Engineering, 2012 Design by Contract 48
Contracts of the derived queries: sublist
sublist( from_position:INTEGER, to_position:INTEGER ):IMMUTABLE_LIST
Purpose: A new list, formed from the items at positions from_position to to_position
require:
from_position_large_enough: from_position >= 1
from_position_small_enough: from_position <= to_position + 1
to_position_small_enough: to_position <= count()
ensure:
is_empty_is _consistent_with_from_and_to_position:
Result.is_empty() = ( from_position > to_position )
result_head _is_at _from_position_in_current:
(from_position <= to_position) implies
(Result.head() = item( from_position ) )
result_tail _is_correct_sublist_within_current:
(from_position <= to_position) implies
(Result.tail().is_equal( sublist( from_position+1, to_position ) ) )
To allow zero
sized sublist []
Software Engineering, 2012 Design by Contract 49
Assertion checking modes
In full assertion checking mode, pre-conditions, post-conditions and invariants are
always checked.
For expensive contracts like those of the IMMUTABLE_LIST class, full contract
evaluation is a performance penalty.
Therefore, there are different modes of contract evaluation.
Testing mode: Full evaluation
Working mode: Only pre-condition evaluation
Argument: Once the class is believed to be bug-free,
the post-conditions and invariants are believed to hold as stated.
checking pre-conditions is needed to protect against irresponsible clients
Software Engineering, 2012 Design by Contract 50
Contracts for the class QUEUE
Software Engineering, 2012 Design by Contract 51
The Queue class Queue is a generic class, with type parameter G: QUEUE of G.
The key features of a SIMPLE_QUEUE class:
SIMPLE_QUEUE
Initialize(capacity:INTEGER)
Put(g:G)
Head:G
remove
G
Software Engineering, 2012 Design by Contract 52
Contracts for the class QUEUE
The contracts for QUEUE are based on a query that returns an immutable list of the elements in the queue, in the same order.
Initial class interface:
Features:
Query:
Creation command:
Other commands:
head (): G
Purpose: The item at the head of the queue
initialize ( a_capacity:INTEGER )
Purpose: Initialize a queue to be empty and to have the given capacity (no changing capacities)
put ( g:G )
Purpose: Add g to the tail of the queue
remove ()
Purpose: Remove the item from the head of the queue
Software Engineering, 2012 Design by Contract 53
Contract for the remove command(1)
1. The pre-condition of remove() is that the queue is not empty:
Requires a new (basic) query for checking emptiness.
Options: count() or is_empty().
We pick count() – useful for the post-condition
(..The decision is really a matter of class design rather than contracts)
2. The post-condition of remove(): 1. The number of queue items decreases by 1 -is it enough?
2. The i-th element is the (i + 1) -th element in the original queue:
Requires a new items inspection query:
items():IMMUTABLE_LIST that creates an immutable_list of the queue items
Queries:
count()
Purpose: The number of items in the queue
items():IMMUTABLE_LIST Purpose: A new list, created from the queue items, in the same order
Software Engineering, 2012 Design by Contract 54
Contract for the remove command (2)
Command:
remove () Purpose: remove the item at the head require: not_empty:
count () > 0
ensure: count_decreased:
count() = count()@pre -1
items_shifted:
items().is_equal( items().tail()) @pre )
Items: [“a”,”b”,”c”].tail : [“b”,”c”] a new list of the right size (2)
Our queue maybe implemented as an array (circular structure)
1
2
3
“a”
“b”
“c”
1
2
3
“b”
“c”
Software Engineering, 2012 Design by Contract 55
The status of count (1) 1. count() can be expressed in terms of the count feature of the items() list:
count() = items().count()
Therefore, it can be made a derived query:
2. But count() is used in the pre-condition of remove(). Therefore:
count()
Purpose: The number of items in the queue
ensure:
consistent_with_items:
count() = items().count()
require:
not empty:
items().count () > 0
require:
not empty:
count () > 0
Software Engineering, 2012 Design by Contract 56
The status of count (2) 3. The evaluation of the new not_empty pre-condition implies
evaluation of items().count(), i.e., creation of a new items list and counting its length.
4. Recall: Pre-conditions are evaluated also in working mode: Guideline: Use cheap-to-evaluate queries in pre-conditions.
3. Conclusion: for performance reasons, count should be kept.
4. If count() is implemented as an attribute, it can be constrained
by an invariant:
invariant: count = items().count()
Software Engineering, 2012 Design by Contract 57
Contract of the creation command
The QUEUE creation initialize( a_capacity:INTEGER ) command empties a queue (either new or old), with a capacity given by a_capacity
(for simplicity the capacity of a queue cannot be changed).
1. Pre-condition: Positive capacity.
2. Post-condition: 1. The queue should be empty.
2. The queue capacity is the a_capacity.
This post-condition requires a new query about the capacity of a queue:
Creation command:
capacity()
Purpose: The number of items that self can hold
initialize ( a_capacity:INTEGER )
Purpose: Initialize a queue to be empty, with capacity a_capacity
require:
positive_a_capacity: a_capacity > 0
ensure:
empty: count() = 0
capacity_set: capacity() = a_capacity
Software Engineering, 2012 Design by Contract 58
Contract for the head query
The head():G - query returns the head item
1. Pre-condition: Queue is not empty
2. Post-condition: Returns the head item.
This is expressed in terms of the head of the items() list.
Therefore, head() is a derived query.
Derived query:
head():G
Purpose: The head item
require:
not_empty: count () > 0 // count>0
ensure:
consistent_with_items:
Result = items().head()
Post condition is expensive
Precondition is cheap
Software Engineering, 2012 Design by Contract 59
Contract for the put command
The put( g:G ) command adds g to the tail of the queue
1. Pre-condition: Queue is not full
2. Post-condition: the number of queue items increases by 1,
the added item is at position count()
Command:
put( g:G )
Purpose: Add g to the tail
require:
not_full: count () < capacity()
ensure:
number_of_items_increases: count() = count()@pre + 1
g_at_tail: items.item( count() ) = g
Software Engineering, 2012 Design by Contract 60
More derived queries
Explicit queries for emptiness and being full can be added:
Derived queries:
is_empty()
Purpose: Is the queue empty?
ensure:
consistent_with_items.count: Result = (items.count() = 0 )
is_full()
Purpose: Is the queue full?
ensure:
consistent_with_items.count: Result = (items.count() = capacity() )
Software Engineering, 2012 Design by Contract 61
The final class QUEUE interface
Basic queries: capacity():INTEGER
items() : IMMUTABLE_LIST
Derived queries: head (): G
count(): INTEGER
is_empty(): BOOLEAN
is_full(): BOOLEAN
Creation command: initialize ( a_capacity:INTEGER )
Other commands: put ( g:G )
remove ()
Software Engineering, 2012 Design by Contract 62
Design by Contract and Inheritance
Software Engineering, 2012 Design by Contract 63
Design by Contract and inheritance
Subclasses inherit features of their super-classes
Subclasses also inherit the contracts of their inherited features
Subclasses can redefine features of their super-classes.
Subclasses can redefine the contracts of their inherited features,
BUT: They must respect the inherited contracts
Class COURIER
Features:
…
Mixed Command:
deliver( p: Package, t: Time, d: Destination ) : Time
Purpose: Deliver package p accepted at time t to destination d.
result is the delivery time.
require:
package_small_enough: p.weight() < 5
ensure:
fast_delivery: Result < t + 3
…
Software Engineering, 2012 Design by Contract 64
Redefining a pre-condition (1)
Consider a subclass of COURIER that redefines the pre-condition on the deliver feature:
class SPECIAL_COURIER extends COURIER redefine deliver
and a COURIER client holding an object of SPECIAL_COURIER
If the new pre-condition is:
feature
then the client will have no problem:
A client of COURIER knows the pre-condition of COURIER on deliver:
package_small_enough: p.weight() < 5
Therefore, if it respects the COURIER pre-condition, its concrete SPECIAL_COURIER object will perform the delivery.
deliver( p: Package, t: Time, d: Destination ) : Time
require:
package_small_enough: p.weight() < 8
Software Engineering, 2012 Design by Contract 65
Redefining a pre-condition (2)
If the new pre-condition is:
feature
a COURIER client that actually holds a SPECIAL_COURIER object might have a problem:
A client of COURIER knows the pre-condition of COURIER on deliver:
package_small_enough: p.weight() < 5
But respecting the COURIER pre-condition might still invalidate the pre-condition of the concrete SPECIAL_COURIER object on the deliver, and the delivery would be rejected.
Conclusion: A subclass can only weaken a pre-condition:
Pre-condition of super implies pre-condition of sub-class
deliver( p: Package, t: Time, d: Destination ) : Time
require:
package_small_enough: p.weight() < 3
Software Engineering, 2012 Design by Contract 66
Redefining a post-condition (1)
If the new post-condition is:
feature
then the client will have no problem:
A client of COURIER knows the post-condition of COURIER on deliver:
fast_delivery: Result < t + 3
Therefore, its concrete SPECIAL_COURIER object satisfies its expected benefit.
deliver( p: Package, t: Time, d: Destination ) : Time
ensure:
fast_delivery: Result < t + 2
Software Engineering, 2012 Design by Contract 67
Redefining a post-condition (2)
If the new post-condition is:
feature
then a COURIER client that actually holds a SPECIAL_COURIER object might have a problem:
A client of COURIER knows the post-condition of COURIER on deliver:
fast_delivery: Result < t + 3
But its concrete SPECIAL_COURIER object might not satisfy its expected benefit from the delivery
service.
Conclusion: A subclass can only strengthen a post-condition:
Post-condition of sub-class implies post-condition of super-class
deliver( p: Package, t: Time, d: Destination ) : Time
ensure:
fast_delivery: Result < t + 5
Software Engineering, 2012 Design by Contract 68
Redefining a contract pre-condition
Redefined pre-conditions are combined with their super-class assertions.
A redefined pre-condition is or-ed with its super pre-condition:
which indeed is within the obligation of the delivery service of the sub-class
while
which might not be within the obligation of the delivery service of the sub-class!
DbC languages do not enable strengthening a pre-condition
package_small_enough: p.weight() < 5
or package_small_enough: p.weight() < 8 package_small_enough: p.weight() < 8 reduces to
package_small_enough: p.weight() < 5
or package_small_enough: p.weight() < 3 package_small_enough: p.weight() < 5 reduces to
Software Engineering, 2012 Design by Contract 69
Redefining a contract post-condition
Redefined post-conditions are combined with their super-class assertions.
A redefined post-condition is and-ed with its super post-condition:
which indeed, is provided by the delivery service of the sub-class
while
which might not be provided by the delivery service of the sub-class!
DbC languages do not enable weakening a post-condition
fast_delivery: Result < t + 3
and fast_delivery: Result < t + 2 fast_delivery: Result < t + 2 reduces to
fast_delivery: Result < t + 3
and fast_delivery: Result < t + 5 fast_delivery: Result < t + 3 reduces to
Software Engineering, 2012 Design by Contract 70
Redefining a contract
Redefined assertions are marked explicitly:
class SPECIAL_COURIER extends COURIER redefine deliver
feature
deliver( p: Package, t: Time, d: Destination ) : Time
Purpose: Deliver package p accepted at time t to destination d. result is
the delivery time.
require else:
package_small_enough: p.weight() < 8
ensure then:
fast_delivery: Result < t + 2
Require of super class
or else require this
ensure of super class
and then ensure this
Software Engineering, 2012 Design by Contract 71
Invariants and inheritance
Invariants of a super-class are respected by its sub-classes
Objects of a sub-class must satisfy the inherited invariants
class COURIER
…
invariant
insurance > 1,000,000
class SPECIAL_COURIER extends COURIER
…
invariant
Good: insurance > 2,000,000
Bad: insurance > 800,000
Invariants of super-classes are anded with the invariants of the sub-classes
A sub-class can only strengthen an invariant
Software Engineering, 2012 Design by Contract 72
Guarded post-conditions in super-classes
If post-conditions in a super-class are guarded (conditioned) by some pre-conditions, then sub-classes can relax (weaken) post-conditions class C
feature put( g: G ) Purpose: Add g require g_not_in_aggregate: not has( g ) ensure g_added: has( g ) number_of_items_increases: count() = count()@pre + 1 class RELAXED_C extends C feature put( g: G ) Purpose: Add g; if g exists, do nothing require else g_in_aggregate: has( g ) ensure then g_added: has( g ) number_of_items_increases: count() = count()@pre
PROBLEM
Software Engineering, 2012 Design by Contract 73
Guarded post-conditions in super-classes
A version with guarded post-conditions:
Now the relaxed subclass can be properly defined:
class C put( g: G ) require g_not_in_aggregate: not has( g ) ensure g_added: ( ( not has(g) )@pre ) implies has( g ) number_of_items_increases: ( ( not has(g) )@pre ) implies count() = count()@pre + 1
class RELAXED_C extends C put( g: G ) require else g_in_aggregate: has( g ) ensure then g_added: has( g ) number_of_items_unchanged: ( has(g)@pre ) implies count() = count()@pre
OK
Software Engineering, 2012 Design by Contract 74
Guidelines To support redefinition of features, guard each postcondition
clause with its corresponding precondition.
This allows unexpected redefinitions by those developing
subclasses.
Software Engineering, 2012 Design by Contract 75
Frame Rules
Software Engineering, 2012 Design by Contract 76
Change specifications and FrameRules
Frame rules specify what does not change.
Added to post conditions
Post conditions include2 kinds of assertions:
Change specifications – assert that certain thing changes
Frame rules – assert that certain thing remain unchanged put( g:G )
Purpose: Add g to the tail
require:
not_full: count () < capacity()
ensure:
number_of_items_increases:
count() = count()@pre + 1
g_at_tail:
items( count() ) = g
capacity _unchanged:
capacity() = capacity()@pre Software Engineering, 2012 Design by Contract 77
Frame Rules for put Using Immutable
Lists We can add a second frame rule concerning the items in the
queue by adding a 4th assertion to the postcondition on put.
We assert that sublist containing the first 10 items (i.e.,
[email protected]) of 11 items must equal the list containing all
the items that were there before the call (i.e., items@pre).
The frame rule makes use of the is_equal query on lists.
Two lists are equal if they hold the same items.
What if two lists hold references to the same objects but these
objects are in different states?
original_items_unchanged:
items.sublist(1,[email protected]).is_equal(items@pre)
Software Engineering, 2012 Design by Contract 78
Frame Rules for put Using Immutable
Lists Frame rules constrain objects not to change between the time just
before call to a feature and the time just after that call.
There are 2 properties of a list that can change independently:
One object in the list might be replaced by another
The contents of an object might change.
The problem is not solved by changing the meaning of is_equal in
class IMMUTABLE_LIST to check that elements are equal both by
“=“ and by is_equal.
head = other.head and head.is_equal(other.head) and tail.is_equal(other.tail)
Software Engineering, 2012 Design by Contract 79
Frame Rules for put Using Immutable
Lists
The item query returns a list of addresses of the objects that
are the items in the queue.
If the body of put contains a bug and changes the contents of
an item already in the queue the list of addresses will not
have changed and the test that the head of the list after the
call to put is equal to the head before the call succeeds.
1001
2002
3003
“a”
“b”
“c”
1001
2002
3003
“z”
“b”
“c”
500 400
equal
Software Engineering, 2012 Design by Contract 80
Frame Rules for put Using Immutable
Lists At the time of the test, both head and other.head are pointing to the same
object.
We need 2 separate checks:
1. Does queue after put contains the same objects as before put?
2. Do these objects have the same content as before put?
At present, we can only perform the first check.
To be able to perform the second check we need to keep a copy of the
object’s content in the queue before the put, and to compare it with the
content of the object after put.
Use Eiffel’s library feature deep_clone and deep_equal.
The post condition:
Original_items_unchange:
(deep_equal(item.sublist(1, items.count-1), old deep_clone (items)))
Software Engineering, 2012 Design by Contract 81
Frame Rules for put Using “Forall” In the postcondition of put, make sure that the items that
were in the queue are still there and in their logical positions:
Forall i in the range 1 to count, The item at position i is what was at position i before the call
A preprocessor can perform the following tasks to transform “forall” into Eiffel automatically:
Create a collection object of the right size and type
On entry to the put feature, store values of item(i) in this collection
In the postcondition of the put feature, call a Boolean-valued function to compare the stored values of item(i) with values calculated now.
Software Engineering, 2012 Design by Contract 82
Kinds of Frame Rules Regular commands - change the state of the object
A frame rule for put may assert that the items already in a queue are unchanged by putting a new item at the tail.
Creation commands – before these command the object has no particular state
Cannot assert that the state remain unchanged
Basic queries – return a result but not change the state
Assert that calling the query (capacity) does not change the object state ( increment the capacity, change the queue’s values)
Parameters – usually, a routine that is passed a parameter is not supposed to modify the parameter
Assert a postcondition to express this effect.
Software Engineering, 2012 Design by Contract 83
Frame Rules – a practical viewpoint Is it realistic to develop frame rules to cover all these issues?
Suggested approach:
Add frame rules occasionally, particularly where there is evidence that client programmers misunderstand what a feature does and does not do.
Adopt a convention that all classes come with an implicit frame rule, which states that, unless a postcondition asserts that some property changes, it does not change. Unexpected changes then indicates either poor documentation or bugs
In design reviews – check the code against both explicit and implicit frame rules
Software Engineering, 2012 Design by Contract 84
Design by Contract – pros and cons
Software Engineering, 2012 Design by Contract 85
Benefits of Design by Contract Better designs
More systematic designs
Modularity
No need to read whole class files
Read contracts
Implementations may change
Contract guarantees certain relevant behaviour
Helps ensure inheritance is properly handled
Improve reliability
Better understood and hence more reliable code
Better tested and hence more reliable code Assertions are checked at runtime thereby testing that routines fulfill their stated
contracts.
Software Engineering, 2012 Design by Contract 86
Benefits of Design by Contract Better documentation
Contracts form part of the public or client view of a class
Assertions are checked at runtime thereby testing that the stated
contracts are consistent with the actual routines
Help debugging
When an assertion fails its location is precisely known.
When assertions are switched on in production, customer may
provide the support developer with information regarding a failure.
Support reuse
Good documentation for library users
Avoid defensive programming
Software Engineering, 2012 Design by Contract 87
Meyer’s Perspective on Defensive
Programming
Defensive programming: leads to duplicate checks on preconditions and therefore code
bloat.
leads to implementers checking preconditions when they have
no idea what to do if the precondition is false.
leads to confusion over responsibility for ensuring certain
constraints.
Meyer’s advice is, “Don’t do it!” Think about this in the context of preconditions and exception
handling.
Software Engineering, 2012 Design by Contract 88
Efficiency and Defensive programming Avoid inefficient defensive checks.
For example, the sqrt method assumes that its argument is non-negative.
When this method is called from trusted code ( e.g. , their input is trusted)
this trust is validated by checking the preconditions during debugging, but these checks can be turned off for production use of the program.
Defensive checks are sometimes not possible to execute efficiently.
For example, a binary search method requires that its array argument be sorted.
Checking that an array is sorted requires time linear in the length of the array, but the binary search routine is supposed to execute a logarithmic time.
Therefore the defensive checks would slow down the method unacceptably.
contracts, are easier to automatically remove when the program goes into production, much more efficient.
Software Engineering, 2012 Design by Contract 89
Design by Contract Cons Cost of writing contracts
New language
Takes practice - writing good contracts is a skill.
False sense of security – contract improve programs – they don’t make them perfect.
Quality – not always the primary goal (e.g, early release)
Not all specifications can be described with existing facilities of DbC.
Example: DbC doesn’t support specifications that define performance issues such as execution time and required resources – performance contracts.
Checking contract violation may be more time consuming than the method execution. Example: Class that works on Hamiltonian cycle graphs.
Its preconditions need to solve NP-Complete problem.
Software Engineering, 2012 Design by Contract 90
Runtime checking In a programming environment that understands contracts, we would be told
something like the following (we’ll assume the CUSTOMER_MANAGER
component has been implemented by a class of the same name):
Stopped in object [0xE96978]
Class: CUSTOMER_MANAGER
Feature: add
Problem: Precondition violated
Tag: id_not_already_active
Arguments:
a_customer: BASIC_CUSTOMER_DETAILS [0xE9697C]
Call stack:
CUSTOMER_MANAGER add
was called by CUSTOMER_MANAGER_UIF change_customer
Software Engineering, 2012 Design by Contract 91
Runtime checking Working through this wealth of debugging information line by line, we can tell
1. That the application has stopped in some object (we could open the object
with an object browser and examine its attributes).
2. That this object is of the class CUSTOMER_MANAGER.
3. That a problem arose when that class’s add feature was called.
4. That the problem was that some part of the precondition on add was violated.
5. That if a precondition is violated, it means some client called the add feature
when it was not legal to do so. Specifically, it was the part of the precondition
with the id_not_already_active tag that was violated.
6. Which BASIC_CUSTOMER_DETAILS object was passed as an argument to
the call.
7. The sequence of calls that led up to the problem: A change_customer feature
in a CUSTOMER_MANAGER_UIF class (the user interface to the customer
manager application) called the add feature in the CUSTOMER_MANAGER
class.
Software Engineering, 2012 Design by Contract 92
JML – Java Modeling Language http://www.cs.iastate.edu/~leavens/JML/
An implementation of DBC for Java
One of many
combines the design by contract approach of Eiffel
JMLEclipse is a JML front-end implemented as an Eclipse
plugin.
Open source
Software Engineering, 2012 Design by Contract 93
JASS – Java with ASSertions
Pre-compiler tool written in Java.
Translates annotated contracts into dynamic checking code.
Violation of specification is indicated by Java exception.
Free of charge.
Website:
http://csd.informatik.uni-oldenburg.de/~jass/
Software Engineering, 2012 Design by Contract 94
jContractor –
implementation of Design By Contract Contracts in jContractor are written as Java methods that follow a
simple naming convention.
All contracts are written in standard Java
no need to learn a special contract specification language.
Assertions are written as Java methods that return a boolean value
jContractor provides runtime contract checking by instrumenting the bytecode of classes that define contracts.
jContractor can
either add contract checking code to class files to be executed later,
or it can instrument classes at runtime as they are loaded.
Contracts can be written in the class that they apply to, or in a separate contract class.
Software Engineering, 2012 Design by Contract 95
Software Engineering, 2012 Design by Contract 96